├── .codecov.yml ├── .dockerignore ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug.yml │ ├── config.yml │ └── feature.yml ├── PULL_REQUEST_TEMPLATE ├── stale.yml └── workflows │ ├── build.yml │ ├── lint.yml │ └── potential-duplicates.yml ├── .gitignore ├── .markdownlint.json ├── .twosky.json ├── AGHTechDoc.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── HACKING.md ├── LICENSE.txt ├── Makefile ├── README.md ├── SECURITY.md ├── bamboo-specs ├── bamboo.yaml ├── release.yaml ├── snapcraft.yaml └── test.yaml ├── build └── gitkeep ├── changelog.config.js ├── client ├── .eslintrc.json ├── .gitattributes ├── .prettierrc ├── .stylelintrc.js ├── babel.config.cjs ├── constants.js ├── dev.eslintrc ├── global.d.ts ├── package-lock.json ├── package.json ├── playwright.config.ts ├── prod.eslintrc ├── public │ ├── assets │ │ ├── apple-touch-icon-180x180.png │ │ ├── favicon.png │ │ └── safari-pinned-tab.svg │ ├── index.html │ ├── install.html │ └── login.html ├── src │ ├── __locales │ │ ├── ar.json │ │ ├── be.json │ │ ├── bg.json │ │ ├── cs.json │ │ ├── da.json │ │ ├── de.json │ │ ├── en.json │ │ ├── es.json │ │ ├── fa.json │ │ ├── fi.json │ │ ├── fr.json │ │ ├── hr.json │ │ ├── hu.json │ │ ├── id.json │ │ ├── it.json │ │ ├── ja.json │ │ ├── ko.json │ │ ├── nl.json │ │ ├── no.json │ │ ├── pl.json │ │ ├── pt-br.json │ │ ├── pt-pt.json │ │ ├── ro.json │ │ ├── ru.json │ │ ├── si-lk.json │ │ ├── sk.json │ │ ├── sl.json │ │ ├── sr-cs.json │ │ ├── sv.json │ │ ├── th.json │ │ ├── tr.json │ │ ├── uk.json │ │ ├── vi.json │ │ ├── zh-cn.json │ │ ├── zh-hk.json │ │ └── zh-tw.json │ ├── __tests__ │ │ └── helpers.test.ts │ ├── actions │ │ ├── access.ts │ │ ├── clients.ts │ │ ├── dnsConfig.ts │ │ ├── encryption.ts │ │ ├── filtering.ts │ │ ├── index.tsx │ │ ├── install.ts │ │ ├── login.ts │ │ ├── queryLogs.ts │ │ ├── rewrites.ts │ │ ├── services.ts │ │ ├── stats.ts │ │ └── toasts.ts │ ├── api │ │ └── Api.ts │ ├── components │ │ ├── App │ │ │ ├── index.css │ │ │ └── index.tsx │ │ ├── Dashboard │ │ │ ├── BlockedDomains.tsx │ │ │ ├── Clients.tsx │ │ │ ├── Counters.tsx │ │ │ ├── Dashboard.css │ │ │ ├── DomainCell.tsx │ │ │ ├── QueriedDomains.tsx │ │ │ ├── Statistics.tsx │ │ │ ├── StatsCard.tsx │ │ │ ├── UpstreamAvgTime.tsx │ │ │ ├── UpstreamResponses.tsx │ │ │ └── index.tsx │ │ ├── Filters │ │ │ ├── Actions.tsx │ │ │ ├── Check │ │ │ │ ├── Info.tsx │ │ │ │ └── index.tsx │ │ │ ├── CustomRules.tsx │ │ │ ├── DnsAllowlist.tsx │ │ │ ├── DnsBlocklist.tsx │ │ │ ├── Examples.tsx │ │ │ ├── FiltersList.tsx │ │ │ ├── Form.tsx │ │ │ ├── Modal.tsx │ │ │ ├── Rewrites │ │ │ │ ├── Form.tsx │ │ │ │ ├── Modal.tsx │ │ │ │ ├── Table.tsx │ │ │ │ └── index.tsx │ │ │ ├── Services │ │ │ │ ├── Form.tsx │ │ │ │ ├── ScheduleForm │ │ │ │ │ ├── Modal.tsx │ │ │ │ │ ├── TimePeriod.tsx │ │ │ │ │ ├── TimeSelect.tsx │ │ │ │ │ ├── Timezone.tsx │ │ │ │ │ ├── helpers.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.css │ │ │ │ ├── ServiceField.tsx │ │ │ │ └── index.tsx │ │ │ └── Table.tsx │ │ ├── Header │ │ │ ├── Header.css │ │ │ ├── Menu.tsx │ │ │ └── index.tsx │ │ ├── Logs │ │ │ ├── AnonymizerNotification.tsx │ │ │ ├── Cells │ │ │ │ ├── ClientCell.tsx │ │ │ │ ├── DateCell.tsx │ │ │ │ ├── DomainCell.tsx │ │ │ │ ├── Header.tsx │ │ │ │ ├── HeaderCell.tsx │ │ │ │ ├── IconTooltip.css │ │ │ │ ├── IconTooltip.tsx │ │ │ │ ├── ResponseCell.tsx │ │ │ │ ├── helpers │ │ │ │ │ └── index.ts │ │ │ │ └── index.tsx │ │ │ ├── Disabled.tsx │ │ │ ├── Filters │ │ │ │ ├── Form.tsx │ │ │ │ ├── SearchField.tsx │ │ │ │ └── index.tsx │ │ │ ├── InfiniteTable.tsx │ │ │ ├── Logs.css │ │ │ └── index.tsx │ │ ├── ProtectionTimer │ │ │ └── index.ts │ │ ├── Settings │ │ │ ├── Clients │ │ │ │ ├── AutoClients.tsx │ │ │ │ ├── ClientsTable │ │ │ │ │ ├── ClientsTable.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── Form │ │ │ │ │ ├── components │ │ │ │ │ │ ├── BlockedServices.tsx │ │ │ │ │ │ ├── ClientIds.tsx │ │ │ │ │ │ ├── MainSettings.tsx │ │ │ │ │ │ ├── ScheduleServices.tsx │ │ │ │ │ │ ├── UpstreamDns.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── types.ts │ │ │ │ ├── Modal.tsx │ │ │ │ ├── Service.css │ │ │ │ ├── index.tsx │ │ │ │ └── whoisCell.tsx │ │ │ ├── Dhcp │ │ │ │ ├── FormDHCPv4.tsx │ │ │ │ ├── FormDHCPv6.tsx │ │ │ │ ├── Interfaces.tsx │ │ │ │ ├── Leases.tsx │ │ │ │ ├── StaticLeases │ │ │ │ │ ├── Form.tsx │ │ │ │ │ ├── Modal.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── index.css │ │ │ │ └── index.tsx │ │ │ ├── Dns │ │ │ │ ├── Access │ │ │ │ │ ├── Form.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── Cache │ │ │ │ │ ├── Form.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── Config │ │ │ │ │ ├── Form.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── Upstream │ │ │ │ │ ├── Examples.tsx │ │ │ │ │ ├── Form.tsx │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ ├── Encryption │ │ │ │ ├── CertificateStatus.tsx │ │ │ │ ├── Form.tsx │ │ │ │ ├── KeyStatus.tsx │ │ │ │ └── index.tsx │ │ │ ├── FiltersConfig │ │ │ │ └── index.tsx │ │ │ ├── FormButton.css │ │ │ ├── LogsConfig │ │ │ │ ├── Form.tsx │ │ │ │ └── index.tsx │ │ │ ├── Settings.css │ │ │ ├── StatsConfig │ │ │ │ ├── Form.tsx │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── SetupGuide │ │ │ ├── Guide.css │ │ │ └── index.tsx │ │ ├── Toasts │ │ │ ├── Toast.css │ │ │ ├── Toast.tsx │ │ │ └── index.tsx │ │ └── ui │ │ │ ├── Card.css │ │ │ ├── Card.tsx │ │ │ ├── Cell.tsx │ │ │ ├── CellWrap.tsx │ │ │ ├── Controls │ │ │ ├── Checkbox │ │ │ │ ├── checkbox.css │ │ │ │ └── index.tsx │ │ │ ├── Input.tsx │ │ │ ├── Radio.tsx │ │ │ ├── Select.tsx │ │ │ └── Textarea.tsx │ │ │ ├── Dropdown.css │ │ │ ├── Dropdown.tsx │ │ │ ├── EncryptionTopline.tsx │ │ │ ├── Footer.css │ │ │ ├── Footer.tsx │ │ │ ├── Guide │ │ │ ├── Guide.tsx │ │ │ ├── MobileConfigForm.tsx │ │ │ └── index.ts │ │ │ ├── Icons.css │ │ │ ├── Icons.tsx │ │ │ ├── Line.css │ │ │ ├── Line.tsx │ │ │ ├── Loading.css │ │ │ ├── Loading.tsx │ │ │ ├── LogsSearchLink.tsx │ │ │ ├── Modal.css │ │ │ ├── Overlay.css │ │ │ ├── PageTitle.css │ │ │ ├── PageTitle.tsx │ │ │ ├── ReactTable.css │ │ │ ├── Select.css │ │ │ ├── Status.tsx │ │ │ ├── Tab.tsx │ │ │ ├── Tabler.css │ │ │ ├── Tabs.css │ │ │ ├── Tabs.tsx │ │ │ ├── Tooltip.css │ │ │ ├── Tooltip.tsx │ │ │ ├── Topline.css │ │ │ ├── Topline.tsx │ │ │ ├── UpdateOverlay.tsx │ │ │ ├── UpdateTopline.tsx │ │ │ ├── Version.css │ │ │ ├── Version.tsx │ │ │ ├── svg │ │ │ ├── chevron-down.svg │ │ │ ├── globe.svg │ │ │ ├── help-circle-gray.svg │ │ │ ├── help-circle.svg │ │ │ ├── logo.tsx │ │ │ ├── trash-2.svg │ │ │ └── x.svg │ │ │ └── texareaCommentsHighlight.css │ ├── configureStore.ts │ ├── containers │ │ ├── Clients.ts │ │ ├── CustomRules.ts │ │ ├── Dashboard.ts │ │ ├── Dhcp.ts │ │ ├── Dns.ts │ │ ├── DnsAllowlist.ts │ │ ├── DnsBlocklist.ts │ │ ├── DnsRewrites.ts │ │ ├── Encryption.ts │ │ ├── Settings.ts │ │ └── SetupGuide.ts │ ├── helpers │ │ ├── constants.ts │ │ ├── filters │ │ │ └── filters.ts │ │ ├── form.tsx │ │ ├── helpers.tsx │ │ ├── highlightTextareaComments.tsx │ │ ├── localStorageHelper.ts │ │ ├── renderFormattedClientCell.tsx │ │ ├── trackers │ │ │ ├── trackers.json │ │ │ ├── trackers.ts │ │ │ └── whotracksme_web.json │ │ ├── twosky.ts │ │ ├── useDebounce.ts │ │ ├── validators.ts │ │ └── version.ts │ ├── i18n.ts │ ├── index.tsx │ ├── initialState.ts │ ├── install │ │ ├── Setup │ │ │ ├── AddressList.tsx │ │ │ ├── Auth.tsx │ │ │ ├── Controls.tsx │ │ │ ├── Devices.tsx │ │ │ ├── Greeting.tsx │ │ │ ├── Progress.tsx │ │ │ ├── Settings.tsx │ │ │ ├── Setup.css │ │ │ ├── Submit.tsx │ │ │ └── index.tsx │ │ └── index.tsx │ ├── login │ │ ├── Login │ │ │ ├── Form.tsx │ │ │ ├── Login.css │ │ │ └── index.tsx │ │ └── index.tsx │ ├── reducers │ │ ├── access.ts │ │ ├── clients.ts │ │ ├── dashboard.ts │ │ ├── dhcp.ts │ │ ├── dnsConfig.ts │ │ ├── encryption.ts │ │ ├── filtering.ts │ │ ├── index.ts │ │ ├── install.ts │ │ ├── login.ts │ │ ├── queryLogs.ts │ │ ├── rewrites.ts │ │ ├── services.ts │ │ ├── settings.ts │ │ ├── stats.ts │ │ └── toasts.ts │ └── types.d.ts ├── tests │ ├── constants.ts │ ├── e2e │ │ ├── control-panel.spec.ts │ │ ├── dhcp.spec.ts │ │ ├── dns-settings.spec.ts │ │ ├── filtering.spec.ts │ │ ├── general-settings.spec.ts │ │ ├── globalSetup.ts │ │ ├── globalTeardown.ts │ │ ├── login.spec.ts │ │ └── querylog.spec.ts │ └── helpers │ │ └── network.ts ├── tsconfig.json ├── vitest.config.ts ├── webpack.common.js ├── webpack.dev.js └── webpack.prod.js ├── doc ├── adguard_home_darkmode.svg ├── adguard_home_lightmode.svg ├── agh-arch.png └── agh-filtering.png ├── docker └── Dockerfile ├── go.mod ├── go.sum ├── internal ├── aghalg │ ├── aghalg.go │ ├── nullbool.go │ ├── nullbool_test.go │ ├── sortedmap.go │ └── sortedmap_test.go ├── aghhttp │ ├── aghhttp.go │ ├── header.go │ ├── json.go │ └── json_test.go ├── aghnet │ ├── addr.go │ ├── dhcp.go │ ├── dhcp_unix.go │ ├── dhcp_windows.go │ ├── hostgen.go │ ├── hostgen_test.go │ ├── hostscontainer.go │ ├── hostscontainer_internal_test.go │ ├── hostscontainer_test.go │ ├── ignore.go │ ├── ignore_test.go │ ├── interfaces.go │ ├── interfaces_bsd.go │ ├── interfaces_linux.go │ ├── interfaces_test.go │ ├── ipmut.go │ ├── ipmut_test.go │ ├── net.go │ ├── net_bsd.go │ ├── net_darwin.go │ ├── net_darwin_internal_test.go │ ├── net_freebsd.go │ ├── net_freebsd_internal_test.go │ ├── net_internal_test.go │ ├── net_linux.go │ ├── net_linux_internal_test.go │ ├── net_openbsd.go │ ├── net_openbsd_internal_test.go │ ├── net_test.go │ ├── net_unix.go │ ├── net_windows.go │ ├── upstream.go │ └── upstream_test.go ├── aghos │ ├── aghos_test.go │ ├── filewalker.go │ ├── filewalker_internal_test.go │ ├── filewalker_test.go │ ├── fswatcher.go │ ├── os.go │ ├── os_bsd.go │ ├── os_freebsd.go │ ├── os_internal_test.go │ ├── os_linux.go │ ├── os_unix.go │ ├── os_windows.go │ ├── service.go │ ├── service_darwin.go │ ├── service_others.go │ ├── syslog.go │ ├── syslog_others.go │ ├── syslog_windows.go │ ├── user.go │ ├── user_unix.go │ └── user_windows.go ├── aghrenameio │ ├── renameio.go │ ├── renameio_test.go │ ├── renameio_unix.go │ └── renameio_windows.go ├── aghtest │ ├── aghtest.go │ ├── interface.go │ ├── interface_test.go │ └── upstream.go ├── aghtls │ ├── aghtls.go │ ├── aghtls_test.go │ ├── root.go │ ├── root_linux.go │ └── root_others.go ├── aghuser │ ├── aghuser.go │ ├── aghuser_test.go │ ├── db.go │ ├── db_test.go │ ├── session.go │ ├── sessionstorage.go │ ├── sessionstorage_test.go │ └── user.go ├── arpdb │ ├── arpdb.go │ ├── arpdb_bsd.go │ ├── arpdb_bsd_internal_test.go │ ├── arpdb_internal_test.go │ ├── arpdb_linux.go │ ├── arpdb_linux_internal_test.go │ ├── arpdb_openbsd.go │ ├── arpdb_openbsd_internal_test.go │ ├── arpdb_windows.go │ ├── arpdb_windows_internal_test.go │ └── testdata │ │ └── proc_net_arp ├── client │ ├── addrproc.go │ ├── addrproc_test.go │ ├── client.go │ ├── client_test.go │ ├── index.go │ ├── index_internal_test.go │ ├── persistent.go │ ├── persistent_internal_test.go │ ├── runtimeindex.go │ ├── storage.go │ ├── storage_test.go │ └── upstreammanager.go ├── configmigrate │ ├── configmigrate.go │ ├── migrations_internal_test.go │ ├── migrator.go │ ├── migrator_test.go │ ├── testdata │ │ └── TestMigrateConfig_Migrate │ │ │ ├── v1 │ │ │ ├── input.yml │ │ │ └── output.yml │ │ │ ├── v10 │ │ │ ├── input.yml │ │ │ └── output.yml │ │ │ ├── v11 │ │ │ ├── input.yml │ │ │ └── output.yml │ │ │ ├── v12 │ │ │ ├── input.yml │ │ │ └── output.yml │ │ │ ├── v13 │ │ │ ├── input.yml │ │ │ └── output.yml │ │ │ ├── v14 │ │ │ ├── input.yml │ │ │ └── output.yml │ │ │ ├── v15 │ │ │ ├── input.yml │ │ │ └── output.yml │ │ │ ├── v16 │ │ │ ├── input.yml │ │ │ └── output.yml │ │ │ ├── v17 │ │ │ ├── input.yml │ │ │ └── output.yml │ │ │ ├── v18 │ │ │ ├── input.yml │ │ │ └── output.yml │ │ │ ├── v19 │ │ │ ├── input.yml │ │ │ └── output.yml │ │ │ ├── v2 │ │ │ ├── input.yml │ │ │ └── output.yml │ │ │ ├── v20 │ │ │ ├── input.yml │ │ │ └── output.yml │ │ │ ├── v21 │ │ │ ├── input.yml │ │ │ └── output.yml │ │ │ ├── v22 │ │ │ ├── input.yml │ │ │ └── output.yml │ │ │ ├── v23 │ │ │ ├── input.yml │ │ │ └── output.yml │ │ │ ├── v24 │ │ │ ├── input.yml │ │ │ └── output.yml │ │ │ ├── v25 │ │ │ ├── input.yml │ │ │ └── output.yml │ │ │ ├── v26 │ │ │ ├── input.yml │ │ │ └── output.yml │ │ │ ├── v27 │ │ │ ├── input.yml │ │ │ └── output.yml │ │ │ ├── v29 │ │ │ ├── input.yml │ │ │ └── output.yml │ │ │ ├── v3 │ │ │ ├── input.yml │ │ │ └── output.yml │ │ │ ├── v4 │ │ │ ├── input.yml │ │ │ └── output.yml │ │ │ ├── v5 │ │ │ ├── input.yml │ │ │ └── output.yml │ │ │ ├── v6 │ │ │ ├── input.yml │ │ │ └── output.yml │ │ │ ├── v7 │ │ │ ├── input.yml │ │ │ └── output.yml │ │ │ ├── v8 │ │ │ ├── input.yml │ │ │ └── output.yml │ │ │ └── v9 │ │ │ ├── input.yml │ │ │ └── output.yml │ ├── v1.go │ ├── v10.go │ ├── v11.go │ ├── v12.go │ ├── v13.go │ ├── v14.go │ ├── v15.go │ ├── v16.go │ ├── v17.go │ ├── v18.go │ ├── v19.go │ ├── v2.go │ ├── v20.go │ ├── v21.go │ ├── v22.go │ ├── v23.go │ ├── v24.go │ ├── v25.go │ ├── v26.go │ ├── v27.go │ ├── v28.go │ ├── v29.go │ ├── v3.go │ ├── v4.go │ ├── v5.go │ ├── v6.go │ ├── v7.go │ ├── v8.go │ ├── v9.go │ └── yaml.go ├── dhcpd │ ├── README.md │ ├── bitset.go │ ├── bitset_internal_test.go │ ├── broadcast_bsd.go │ ├── broadcast_bsd_internal_test.go │ ├── broadcast_others.go │ ├── broadcast_others_internal_test.go │ ├── config.go │ ├── conn_bsd.go │ ├── conn_bsd_internal_test.go │ ├── conn_linux.go │ ├── conn_linux_internal_test.go │ ├── conn_unix.go │ ├── db.go │ ├── dhcpd.go │ ├── dhcpd_unix_internal_test.go │ ├── http_unix.go │ ├── http_unix_internal_test.go │ ├── http_windows.go │ ├── http_windows_internal_test.go │ ├── iprange.go │ ├── iprange_internal_test.go │ ├── migrate.go │ ├── migrate_internal_test.go │ ├── options_unix.go │ ├── options_unix_internal_test.go │ ├── routeradv.go │ ├── routeradv_internal_test.go │ ├── v46_windows.go │ ├── v4_unix.go │ ├── v4_unix_internal_test.go │ ├── v6_unix.go │ └── v6_unix_internal_test.go ├── dhcpsvc │ ├── config.go │ ├── config_test.go │ ├── db.go │ ├── db_internal_test.go │ ├── dhcpsvc.go │ ├── dhcpsvc_test.go │ ├── errors.go │ ├── interface.go │ ├── iprange.go │ ├── iprange_internal_test.go │ ├── lease.go │ ├── leaseindex.go │ ├── server.go │ ├── server_test.go │ ├── testdata │ │ ├── TestDHCPServer_RemoveLease │ │ │ └── leases.json │ │ ├── TestDHCPServer_Reset │ │ │ └── leases.json │ │ ├── TestDHCPServer_UpdateStaticLease │ │ │ └── leases.json │ │ ├── TestDHCPServer_index │ │ │ └── leases.json │ │ └── TestServer_Leases │ │ │ └── leases.json │ ├── v4.go │ ├── v4_internal_test.go │ └── v6.go ├── dnsforward │ ├── access.go │ ├── access_internal_test.go │ ├── beforerequest.go │ ├── beforerequest_internal_test.go │ ├── clientid.go │ ├── clientid_internal_test.go │ ├── clientscontainer.go │ ├── config.go │ ├── config_internal_test.go │ ├── configvalidator.go │ ├── dialcontext.go │ ├── dns64.go │ ├── dns64_internal_test.go │ ├── dnsforward.go │ ├── dnsforward_internal_test.go │ ├── dnsrewrite.go │ ├── dnsrewrite_internal_test.go │ ├── filter.go │ ├── filter_internal_test.go │ ├── http.go │ ├── http_internal_test.go │ ├── ipset.go │ ├── ipset_internal_test.go │ ├── msg.go │ ├── process.go │ ├── process_internal_test.go │ ├── stats.go │ ├── stats_internal_test.go │ ├── svcbmsg.go │ ├── svcbmsg_internal_test.go │ ├── testdata │ │ ├── TestDNSForwardHTTP_handleGetConfig.json │ │ └── TestDNSForwardHTTP_handleSetConfig.json │ ├── upstreams.go │ └── upstreams_internal_test.go ├── filtering │ ├── blocked.go │ ├── dnsrewrite.go │ ├── dnsrewrite_test.go │ ├── filter.go │ ├── filter_internal_test.go │ ├── filtering.go │ ├── filtering_internal_test.go │ ├── hashprefix │ │ ├── cache.go │ │ ├── cache_internal_test.go │ │ ├── hashprefix.go │ │ └── hashprefix_internal_test.go │ ├── hosts.go │ ├── hosts_test.go │ ├── http.go │ ├── http_internal_test.go │ ├── idgenerator.go │ ├── idgenerator_internal_test.go │ ├── path.go │ ├── path_unix_internal_test.go │ ├── path_windows_internal_test.go │ ├── rewrite │ │ ├── item.go │ │ ├── item_internal_test.go │ │ ├── storage.go │ │ └── storage_internal_test.go │ ├── rewritehttp.go │ ├── rewritehttp_test.go │ ├── rewrites.go │ ├── rewrites_internal_test.go │ ├── rulelist │ │ ├── engine.go │ │ ├── engine_test.go │ │ ├── error.go │ │ ├── filter.go │ │ ├── filter_test.go │ │ ├── parser.go │ │ ├── parser_test.go │ │ ├── rulelist.go │ │ ├── rulelist_test.go │ │ ├── storage.go │ │ ├── storage_test.go │ │ ├── textengine.go │ │ └── textengine_test.go │ ├── safesearch.go │ ├── safesearch │ │ ├── rules.go │ │ ├── rules │ │ │ ├── bing.txt │ │ │ ├── duckduckgo.txt │ │ │ ├── ecosia.txt │ │ │ ├── google.txt │ │ │ ├── pixabay.txt │ │ │ ├── yandex.txt │ │ │ └── youtube.txt │ │ ├── safesearch.go │ │ ├── safesearch_internal_test.go │ │ └── safesearch_test.go │ ├── safesearchhttp.go │ ├── servicelist.go │ └── tests │ │ └── dns.txt ├── home │ ├── auth.go │ ├── auth_internal_test.go │ ├── authglinet.go │ ├── authglinet_internal_test.go │ ├── authhttp.go │ ├── authhttp_internal_test.go │ ├── authratelimiter.go │ ├── authratelimiter_internal_test.go │ ├── clients.go │ ├── clients_internal_test.go │ ├── clientshttp.go │ ├── clientshttp_internal_test.go │ ├── config.go │ ├── context.go │ ├── control.go │ ├── controlinstall.go │ ├── controlupdate.go │ ├── dns.go │ ├── home.go │ ├── home_internal_test.go │ ├── httpclient.go │ ├── i18n.go │ ├── log.go │ ├── middlewares.go │ ├── middlewares_internal_test.go │ ├── mobileconfig.go │ ├── mobileconfig_internal_test.go │ ├── options.go │ ├── options_internal_test.go │ ├── profilehttp.go │ ├── service.go │ ├── service_linux.go │ ├── service_openbsd.go │ ├── service_others.go │ ├── signal.go │ ├── tls.go │ ├── tls_internal_test.go │ └── web.go ├── ipset │ ├── ipset.go │ ├── ipset_linux.go │ ├── ipset_linux_internal_test.go │ └── ipset_others.go ├── next │ ├── AdGuardHome.example.yaml │ ├── agh │ │ └── agh.go │ ├── changelog.md │ ├── cmd │ │ ├── cmd.go │ │ ├── log.go │ │ ├── opt.go │ │ └── signal.go │ ├── configmgr │ │ ├── config.go │ │ └── configmgr.go │ ├── dnssvc │ │ ├── config.go │ │ ├── dnssvc.go │ │ └── dnssvc_test.go │ ├── jsonpatch │ │ ├── jsonpatch.go │ │ └── jsonpatch_test.go │ └── websvc │ │ ├── config.go │ │ ├── dns.go │ │ ├── dns_test.go │ │ ├── http.go │ │ ├── http_test.go │ │ ├── middleware.go │ │ ├── route.go │ │ ├── server.go │ │ ├── settings.go │ │ ├── settings_test.go │ │ ├── system.go │ │ ├── system_test.go │ │ ├── websvc.go │ │ └── websvc_test.go ├── permcheck │ ├── check_unix.go │ ├── check_windows.go │ ├── migrate_unix.go │ ├── migrate_windows.go │ ├── permcheck.go │ ├── security_unix.go │ └── security_windows.go ├── querylog │ ├── client.go │ ├── decode.go │ ├── decode_internal_test.go │ ├── entry.go │ ├── http.go │ ├── json.go │ ├── qlog.go │ ├── qlog_internal_test.go │ ├── qlogfile.go │ ├── qlogfile_internal_test.go │ ├── qlogreader.go │ ├── qlogreader_internal_test.go │ ├── querylog.go │ ├── querylogfile.go │ ├── search.go │ ├── search_internal_test.go │ ├── searchcriterion.go │ └── searchparams.go ├── rdns │ ├── rdns.go │ └── rdns_test.go ├── schedule │ ├── schedule.go │ └── schedule_internal_test.go ├── stats │ ├── http.go │ ├── http_internal_test.go │ ├── stats.go │ ├── stats_internal_test.go │ ├── stats_test.go │ ├── unit.go │ └── unit_internal_test.go ├── updater │ ├── check.go │ ├── check_test.go │ ├── testdata │ │ ├── AdGuardHome.tar.gz │ │ ├── AdGuardHome.zip │ │ └── AdGuardHome_unix.tar.gz │ ├── updater.go │ ├── updater_internal_test.go │ └── updater_test.go ├── version │ ├── norace.go │ ├── race.go │ └── version.go └── whois │ ├── whois.go │ └── whois_test.go ├── main.go ├── main_next.go ├── openapi ├── CHANGELOG.md ├── README.md ├── index.html ├── next.yaml └── openapi.yaml ├── scripts ├── README.md ├── blocked-services │ └── main.go ├── companiesdb │ └── download.sh ├── hooks │ └── pre-commit ├── install.sh ├── make │ ├── build-docker.sh │ ├── build-release.sh │ ├── go-bench.sh │ ├── go-build.sh │ ├── go-deps.sh │ ├── go-fuzz.sh │ ├── go-lint.sh │ ├── go-test.sh │ ├── go-tools.sh │ ├── go-upd-tools.sh │ ├── helper.sh │ ├── md-lint.sh │ ├── sh-lint.sh │ ├── txt-lint.sh │ └── version.sh ├── querylog │ ├── anonymize.js │ ├── package-lock.json │ ├── package.json │ └── test │ │ └── querylog.json ├── snap │ ├── build.sh │ ├── download.sh │ └── upload.sh ├── translations │ ├── download.go │ ├── main.go │ └── upload.go └── vetted-filters │ └── main.go ├── snap ├── gui │ ├── adguard-home-web.desktop │ └── adguard-home-web.png ├── local │ └── adguard-home-web.sh └── snap.tmpl.yaml └── staticcheck.conf /.codecov.yml: -------------------------------------------------------------------------------- 1 | 'coverage': 2 | 'status': 3 | 'project': 4 | 'default': 5 | 'target': '40%' 6 | 'threshold': null 7 | 'patch': false 8 | 'changes': false 9 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Ignore everything except for explicitly allowed stuff. 2 | * 3 | !dist/docker 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | client/* linguist-vendored 2 | # This file contains a lot of inline SVG data, which often interferes with 3 | # grepping. Technically, this file must be reviewed when new icons appear, but 4 | # that happens fairly rarely. 5 | client/src/components/ui/Icons.js -diff 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | 'blank_issues_enabled': false 2 | 'contact_links': 3 | - 'about': > 4 | Please report filtering issues, for example advertising filters 5 | misfiring or safe browsing false positives, using the form on our 6 | website 7 | 'name': 'AdGuard filters issues' 8 | 'url': 'https://link.adtidy.org/forward.html?action=report&app=home&from=github' 9 | - 'about': > 10 | Please send requests for new blocked services and vetted filtering lists 11 | to the Hostlists Registry repository 12 | 'name': 'Blocked services and vetted filtering rule lists: AdGuard Hostlists Registry' 13 | 'url': 'https://github.com/AdguardTeam/HostlistsRegistry' 14 | - 'about': > 15 | Please use GitHub Discussions for questions 16 | 'name': 'Q&A Discussions' 17 | 'url': 'https://github.com/AdguardTeam/AdGuardHome/discussions' 18 | - 'about': > 19 | Please check our Wiki for configuration file description, frequently 20 | asked questions, and other documentation 21 | 'name': 'Wiki' 22 | 'url': 'https://github.com/AdguardTeam/AdGuardHome/wiki' 23 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE: -------------------------------------------------------------------------------- 1 | Before submitting a PR please make sure that: 2 | 3 | 1. You have discussed your solution in an issue and have got an 4 | approval from a maintainer. See our 5 | [contribution guide](https://github.com/AdguardTeam/AdGuardHome/blob/master/CONTRIBUTING.md). 6 | 7 | 2. This isn't a localization fix; please send those to our 8 | [CrowdIn](https://crowdin.com/project/adguard-applications/en#/adguard-home) 9 | page. 10 | 11 | 3. Your code follows our 12 | [code guidelines](https://github.com/AdguardTeam/CodeGuidelines/blob/master/Go/Go.md). 13 | 14 | Add a short description here. The description should include: 15 | 16 | 1. Which issue this PR closes (`Closes #NNNN.`) or updates (`Updates 17 | #NNNN.`). Please do not open PRs without filing an issue first. 18 | 19 | 2. A short description of how the change achieves that. 20 | 21 | Do not forget to remove these instructions! 22 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale. 2 | 'daysUntilStale': 90 3 | # Number of days of inactivity before a stale issue is closed. 4 | 'daysUntilClose': 15 5 | # Issues with these labels will never be considered stale. 6 | 'exemptLabels': 7 | - 'bug' 8 | - 'documentation' 9 | - 'enhancement' 10 | - 'feature request' 11 | - 'help wanted' 12 | - 'localization' 13 | - 'needs investigation' 14 | - 'recurrent' 15 | - 'research' 16 | # Set to true to ignore issues in a milestone. 17 | 'exemptMilestones': true 18 | # Label to use when marking an issue as stale. 19 | 'staleLabel': 'wontfix' 20 | # Comment to post when marking an issue as stale. Set to `false` to disable. 21 | 'markComment': > 22 | This issue has been automatically marked as stale because it has not had 23 | recent activity. It will be closed if no further activity occurs. Thank you 24 | for your contributions. 25 | # Comment to post when closing a stale issue. Set to `false` to disable. 26 | 'closeComment': false 27 | # Limit the number of actions per hour. 28 | 'limitPerRun': 1 29 | -------------------------------------------------------------------------------- /.github/workflows/potential-duplicates.yml: -------------------------------------------------------------------------------- 1 | 'name': 'potential-duplicates' 2 | 'on': 3 | 'issues': 4 | 'types': 5 | - 'opened' 6 | 'jobs': 7 | 'run': 8 | 'runs-on': 'ubuntu-latest' 9 | 'steps': 10 | - 'uses': 'wow-actions/potential-duplicates@v1' 11 | 'with': 12 | 'GITHUB_TOKEN': '${{ secrets.GITHUB_TOKEN }}' 13 | 'state': 'all' 14 | 'threshold': 0.6 15 | 'comment': | 16 | Potential duplicates: {{#issues}} 17 | * [#{{ number }}] {{ title }} ({{ accuracy }}%) 18 | {{/issues}} 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This comment is used to simplify checking local copies of the file. Bump 2 | # this number every time a significant change is made to this file. 3 | # 4 | # AdGuard-Project-Version: 1 5 | 6 | # Please, DO NOT put your text editors' temporary files here. The more are 7 | # added, the harder it gets to maintain and manage projects' gitignores. Put 8 | # them into your global gitignore file instead. 9 | # 10 | # See https://stackoverflow.com/a/7335487/1892060. 11 | # 12 | # Only build, run, and test outputs here. Sorted. With negations at the 13 | # bottom to make sure they take effect. 14 | *.db 15 | *.log 16 | *.out 17 | *.snap 18 | *.test 19 | /agh-backup/ 20 | /bin/ 21 | /build/* 22 | /client/blob-report/ 23 | /client/playwright-report/ 24 | /client/playwright/.cache/ 25 | /client/test-results/ 26 | /data/ 27 | /dist/ 28 | /filtering/tests/filtering.TestLotsOfRules*.pprof 29 | /filtering/tests/top-1m.csv 30 | /internal/next/AdGuardHome.yaml 31 | /launchpad_credentials 32 | /querylog.json* 33 | /snapcraft_login 34 | /test-reports/ 35 | AdGuardHome 36 | AdGuardHome.exe 37 | AdGuardHome.yaml* 38 | coverage.txt 39 | node_modules/ 40 | 41 | !/build/gitkeep 42 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "ul-indent": { 3 | "indent": 4 4 | }, 5 | "ul-style": { 6 | "style": "dash" 7 | }, 8 | "emphasis-style": { 9 | "style": "asterisk" 10 | }, 11 | "no-duplicate-heading": { 12 | "siblings_only": true 13 | }, 14 | "no-inline-html": { 15 | "allowed_elements": [ 16 | "a" 17 | ] 18 | }, 19 | "no-trailing-spaces": { 20 | "br_spaces": 0 21 | }, 22 | "line-length": false, 23 | "no-bare-urls": false, 24 | "link-fragments": false 25 | } 26 | -------------------------------------------------------------------------------- /.twosky.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "project_id": "home", 4 | "base_locale": "en", 5 | "localizable_files": [ 6 | "client/src/__locales/en.json" 7 | ], 8 | "languages": { 9 | "ar": "العربية", 10 | "be": "Беларуская", 11 | "bg": "Български", 12 | "cs": "Český", 13 | "da": "Dansk", 14 | "de": "Deutsch", 15 | "en": "English", 16 | "es": "Español", 17 | "fa": "فارسی", 18 | "fi": "Suomi", 19 | "fr": "Français", 20 | "hr": "Hrvatski", 21 | "hu": "Magyar", 22 | "id": "Indonesian", 23 | "it": "Italiano", 24 | "ja": "日本語", 25 | "ko": "한국어", 26 | "nl": "Nederlands", 27 | "no": "Norsk", 28 | "pl": "Polski", 29 | "pt-br": "Português (BR)", 30 | "pt-pt": "Português (PT)", 31 | "ro": "Română", 32 | "ru": "Русский", 33 | "si-lk": "සිංහල", 34 | "sk": "Slovenčina", 35 | "sl": "Slovenščina", 36 | "sr-cs": "Srpski", 37 | "sv": "Svenska", 38 | "th": "ภาษาไทย", 39 | "tr": "Türkçe", 40 | "uk": "Українська", 41 | "vi": "Tiếng Việt", 42 | "zh-cn": "简体中文", 43 | "zh-hk": "繁體中文(香港)", 44 | "zh-tw": "正體中文(台灣)" 45 | } 46 | } 47 | ] 48 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting vulnerabilities 4 | 5 | Please send your vulnerability reports to . To make sure that your report reaches us, please: 6 | 7 | 1. Include the words “AdGuard Home” and “vulnerability” to the subject line as well as a short description of the vulnerability. For example: 8 | 9 | > AdGuard Home API vulnerability: possible XSS attack 10 | 11 | 1. Make sure that the message body contains a clear description of the vulnerability. 12 | 13 | If you have not received a reply to your email within 7 days, please make sure to follow up with us again at . Once again, make sure that the word “vulnerability” is in the subject line. 14 | -------------------------------------------------------------------------------- /bamboo-specs/bamboo.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | !include release.yaml 3 | 4 | --- 5 | !include snapcraft.yaml 6 | 7 | --- 8 | !include test.yaml 9 | -------------------------------------------------------------------------------- /build/gitkeep: -------------------------------------------------------------------------------- 1 | Keep this file non-hidden for Go's embedding to work. 2 | -------------------------------------------------------------------------------- /changelog.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "disableEmoji": true, 3 | "list": [ 4 | "+ ", 5 | "* ", 6 | "- ", 7 | ], 8 | "maxMessageLength": 64, 9 | "minMessageLength": 3, 10 | "questions": [ 11 | "type", 12 | "scope", 13 | "subject", 14 | "body", 15 | "issues", 16 | ], 17 | "scopes": [ 18 | "", 19 | "ui", 20 | "global", 21 | "filtering", 22 | "home", 23 | "dnsforward", 24 | "dhcpd", 25 | "querylog", 26 | "documentation", 27 | ], 28 | "types": { 29 | "+ ": { 30 | "description": "A new feature", 31 | "emoji": "", 32 | "value": "+ " 33 | }, 34 | "* ": { 35 | "description": "A code change that neither fixes a bug or adds a feature", 36 | "emoji": "", 37 | "value": "* " 38 | }, 39 | "- ": { 40 | "description": "A bug fix", 41 | "emoji": "", 42 | "value": "- " 43 | } 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /client/.gitattributes: -------------------------------------------------------------------------------- 1 | *.ts text eol=lf 2 | -------------------------------------------------------------------------------- /client/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "bracketSpacing": true, 6 | "bracketSameLine": true, 7 | "tabWidth": 4, 8 | "semi": true, 9 | "arrowParens": "always", 10 | } 11 | -------------------------------------------------------------------------------- /client/babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = (api) => { 2 | api.cache(false); 3 | return { 4 | presets: ['@babel/preset-env', '@babel/preset-typescript', '@babel/preset-react'], 5 | plugins: [ 6 | '@babel/plugin-transform-runtime', 7 | '@babel/plugin-transform-class-properties', 8 | '@babel/plugin-transform-object-rest-spread', 9 | '@babel/plugin-transform-nullish-coalescing-operator', 10 | '@babel/plugin-transform-optional-chaining', 11 | 'react-hot-loader/babel', 12 | ], 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /client/constants.js: -------------------------------------------------------------------------------- 1 | export const BUILD_ENVS = { 2 | dev: 'development', 3 | prod: 'production', 4 | }; 5 | 6 | export const BASE_URL = 'control'; 7 | -------------------------------------------------------------------------------- /client/dev.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ".eslintrc", 3 | "rules": { 4 | "no-debugger":"warn" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /client/global.d.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | declare module '*.svg' { 4 | const content: React.FunctionComponent>; 5 | export default content; 6 | } 7 | -------------------------------------------------------------------------------- /client/prod.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | // disallow the use of debugger 4 | "no-debugger": "error", 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /client/public/assets/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/7cf1600f1b2fe487a0034b03a5ff31bd7f0efb9f/client/public/assets/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /client/public/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/7cf1600f1b2fe487a0034b03a5ff31bd7f0efb9f/client/public/assets/favicon.png -------------------------------------------------------------------------------- /client/public/assets/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /client/public/install.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Setup AdGuard Home 14 | 15 | 16 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /client/public/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Login 14 | 15 | 16 | 19 |
20 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /client/src/actions/login.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from 'redux-actions'; 2 | 3 | import apiClient from '../api/Api'; 4 | import { addErrorToast } from './toasts'; 5 | import { HTML_PAGES } from '../helpers/constants'; 6 | 7 | export const processLoginRequest = createAction('PROCESS_LOGIN_REQUEST'); 8 | export const processLoginFailure = createAction('PROCESS_LOGIN_FAILURE'); 9 | export const processLoginSuccess = createAction('PROCESS_LOGIN_SUCCESS'); 10 | 11 | export const processLogin = (values: any) => async (dispatch: any) => { 12 | dispatch(processLoginRequest()); 13 | try { 14 | await apiClient.login(values); 15 | const dashboardUrl = 16 | window.location.origin + window.location.pathname.replace(HTML_PAGES.LOGIN, HTML_PAGES.MAIN); 17 | window.location.replace(dashboardUrl); 18 | dispatch(processLoginSuccess()); 19 | } catch (error) { 20 | dispatch(addErrorToast({ error })); 21 | dispatch(processLoginFailure()); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /client/src/actions/toasts.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from 'redux-actions'; 2 | 3 | export const addErrorToast = createAction('ADD_ERROR_TOAST'); 4 | export const addSuccessToast = createAction('ADD_SUCCESS_TOAST'); 5 | export const addNoticeToast = createAction('ADD_NOTICE_TOAST'); 6 | -------------------------------------------------------------------------------- /client/src/components/Dashboard/StatsCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { STATUS_COLORS } from '../../helpers/constants'; 4 | 5 | import { formatNumber } from '../../helpers/helpers'; 6 | 7 | import Card from '../ui/Card'; 8 | 9 | import Line from '../ui/Line'; 10 | 11 | interface StatsCardProps { 12 | total: number; 13 | lineData: unknown[]; 14 | title: object; 15 | color: string; 16 | percent?: number; 17 | } 18 | 19 | const StatsCard = ({ total, lineData, percent, title, color }: StatsCardProps) => ( 20 | 21 |
22 |
{formatNumber(total)}
23 | 24 |
{title}
25 |
26 | {percent >= 0 &&
{percent}
} 27 | 28 |
29 | 30 |
31 |
32 | ); 33 | 34 | export default StatsCard; 35 | -------------------------------------------------------------------------------- /client/src/components/Filters/Actions.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withTranslation, Trans } from 'react-i18next'; 3 | 4 | interface ActionsProps { 5 | handleAdd: (...args: unknown[]) => unknown; 6 | handleRefresh: (...args: unknown[]) => unknown; 7 | processingRefreshFilters: boolean; 8 | whitelist?: boolean; 9 | } 10 | 11 | const Actions = ({ handleAdd, handleRefresh, processingRefreshFilters, whitelist }: ActionsProps) => ( 12 |
13 | 16 | 17 | 24 |
25 | ); 26 | 27 | export default withTranslation()(Actions); 28 | -------------------------------------------------------------------------------- /client/src/components/Filters/Services/ScheduleForm/TimePeriod.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { getTimeFromMs } from './helpers'; 4 | 5 | interface TimePeriodProps { 6 | startTimeMs: number; 7 | endTimeMs: number; 8 | } 9 | 10 | export const TimePeriod = ({ startTimeMs, endTimeMs }: TimePeriodProps) => { 11 | const startTime = getTimeFromMs(startTimeMs); 12 | const endTime = getTimeFromMs(endTimeMs); 13 | 14 | return ( 15 |
16 | 19 |  –  20 | 23 |
24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /client/src/components/Filters/Services/ScheduleForm/Timezone.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ct from 'countries-and-timezones'; 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | import { LOCAL_TIMEZONE_VALUE } from '../../../../helpers/constants'; 6 | 7 | interface TimezoneProps { 8 | timezone: string; 9 | setTimezone: (...args: unknown[]) => unknown; 10 | } 11 | 12 | export const Timezone = ({ timezone, setTimezone }: TimezoneProps) => { 13 | const [t] = useTranslation(); 14 | 15 | const onTimeZoneChange = (event: any) => { 16 | setTimezone(event.target.value); 17 | }; 18 | 19 | const timezones = ct.getAllTimezones(); 20 | 21 | return ( 22 |
23 | 24 | 25 | 34 |
35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /client/src/components/Logs/AnonymizerNotification.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Trans } from 'react-i18next'; 3 | 4 | import { HashLink as Link } from 'react-router-hash-link'; 5 | 6 | const AnonymizerNotification = () => ( 7 |
8 | text, 11 | 12 | 13 | link 14 | , 15 | ]}> 16 | anonymizer_notification 17 | 18 |
19 | ); 20 | 21 | export default AnonymizerNotification; 22 | -------------------------------------------------------------------------------- /client/src/components/Logs/Cells/DateCell.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | 4 | import { formatDateTime, formatTime } from '../../../helpers/helpers'; 5 | import { DEFAULT_SHORT_DATE_FORMAT_OPTIONS, DEFAULT_TIME_FORMAT } from '../../../helpers/constants'; 6 | import { RootState } from '../../../initialState'; 7 | 8 | interface DateCellProps { 9 | time: string; 10 | } 11 | 12 | const DateCell = ({ time }: DateCellProps) => { 13 | const isDetailed = useSelector((state: RootState) => state.queryLogs.isDetailed); 14 | 15 | if (!time) { 16 | return <>–; 17 | } 18 | 19 | const formattedTime = formatTime(time, DEFAULT_TIME_FORMAT); 20 | 21 | const formattedDate = formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS); 22 | 23 | return ( 24 |
25 |
26 | {formattedTime} 27 |
28 | {isDetailed && ( 29 |
30 | {formattedDate} 31 |
32 | )} 33 |
34 | ); 35 | }; 36 | 37 | export default DateCell; 38 | -------------------------------------------------------------------------------- /client/src/components/Logs/Cells/HeaderCell.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React from 'react'; 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | interface HeaderCellProps { 6 | content: string | React.ReactElement; 7 | className?: string; 8 | } 9 | 10 | const HeaderCell = ({ content, className }: HeaderCellProps, idx: any) => { 11 | const { t } = useTranslation(); 12 | 13 | return ( 14 |
18 | {typeof content === 'string' ? t(content) : content} 19 |
20 | ); 21 | }; 22 | 23 | export default HeaderCell; 24 | -------------------------------------------------------------------------------- /client/src/components/Logs/Cells/helpers/index.ts: -------------------------------------------------------------------------------- 1 | import i18next from 'i18next'; 2 | 3 | export const BUTTON_PREFIX = 'btn_'; 4 | 5 | export const getBlockClientInfo = (ip: any, disallowed: any, disallowed_rule: any, allowedClients: any) => { 6 | let confirmMessage; 7 | 8 | if (disallowed) { 9 | confirmMessage = i18next.t('client_confirm_unblock', { ip: disallowed_rule || ip }); 10 | } else { 11 | confirmMessage = `${i18next.t('adg_will_drop_dns_queries')} ${i18next.t('client_confirm_block', { ip })}`; 12 | if (allowedClients.length > 0) { 13 | confirmMessage = confirmMessage.concat(`\n\n${i18next.t('filter_allowlist', { disallowed_rule })}`); 14 | } 15 | } 16 | 17 | const buttonKey = i18next.t(disallowed ? 'allow_this_client' : 'disallow_this_client'); 18 | const lastRuleInAllowlist = !disallowed && allowedClients === disallowed_rule; 19 | 20 | return { 21 | confirmMessage, 22 | buttonKey, 23 | lastRuleInAllowlist, 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /client/src/components/Logs/Disabled.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { Trans } from 'react-i18next'; 3 | 4 | import { HashLink as Link } from 'react-router-hash-link'; 5 | 6 | import Card from '../ui/Card'; 7 | 8 | const Disabled = () => ( 9 | 10 |
11 |

12 | query_log 13 |

14 |
15 | 16 | 17 |
18 | 21 | link 22 | , 23 | ]}> 24 | query_log_disabled 25 | 26 |
27 |
28 |
29 | ); 30 | 31 | export default Disabled; 32 | -------------------------------------------------------------------------------- /client/src/components/Settings/Clients/ClientsTable/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ClientsTable } from './ClientsTable'; 2 | -------------------------------------------------------------------------------- /client/src/components/Settings/Clients/Form/components/ScheduleServices.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Trans } from 'react-i18next'; 3 | import { useFormContext } from 'react-hook-form'; 4 | import { ScheduleForm } from '../../../../Filters/Services/ScheduleForm'; 5 | import { ClientForm } from '../types'; 6 | 7 | export const ScheduleServices = () => { 8 | const { watch, setValue } = useFormContext(); 9 | 10 | const blockedServicesSchedule = watch('blocked_services_schedule'); 11 | 12 | const handleScheduleSubmit = (values: any) => { 13 | setValue('blocked_services_schedule', values); 14 | }; 15 | 16 | return ( 17 | <> 18 |
19 | schedule_services_desc_client 20 |
21 | 22 | 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /client/src/components/Settings/Clients/Form/components/index.ts: -------------------------------------------------------------------------------- 1 | export { BlockedServices } from './BlockedServices'; 2 | export { ClientIds } from './ClientIds'; 3 | export { ScheduleServices } from './ScheduleServices'; 4 | export { MainSettings } from './MainSettings'; 5 | export { UpstreamDns } from './UpstreamDns'; 6 | -------------------------------------------------------------------------------- /client/src/components/Settings/Clients/Form/types.ts: -------------------------------------------------------------------------------- 1 | export type ClientForm = { 2 | name: string; 3 | tags: { value: string; label: string }[]; 4 | ids: { name: string }[]; 5 | use_global_settings: boolean; 6 | use_global_blocked_services: boolean; 7 | blocked_services_schedule: { 8 | time_zone: string; 9 | }; 10 | safe_search: { 11 | enabled: boolean; 12 | [key: string]: boolean; 13 | }; 14 | upstreams: string; 15 | upstreams_cache_enabled: boolean; 16 | upstreams_cache_size: number; 17 | blocked_services: Record; 18 | filtering_enabled: boolean; 19 | safebrowsing_enabled: boolean; 20 | parental_enabled: boolean; 21 | ignore_querylog: boolean; 22 | ignore_statistics: boolean; 23 | }; 24 | 25 | export type SubmitClientForm = Omit & { 26 | ids: string[]; 27 | tags: string[]; 28 | }; 29 | -------------------------------------------------------------------------------- /client/src/components/Settings/Dhcp/index.css: -------------------------------------------------------------------------------- 1 | .dhcp-form__button { 2 | margin: 0 1rem; 3 | } 4 | 5 | .page-title--dhcp { 6 | display: flex; 7 | align-items: center; 8 | } 9 | 10 | .col__dhcp { 11 | flex: 0 0 50%; 12 | max-width: 50%; 13 | padding-right: 0; 14 | } 15 | 16 | .dhcp__interfaces { 17 | padding-bottom: 1rem; 18 | } 19 | 20 | .dhcp__interfaces-info { 21 | padding: 0.5rem 0.75rem 0; 22 | line-break: anywhere; 23 | } 24 | 25 | @media (max-width: 991.98px) { 26 | .dhcp-form__button { 27 | margin: 0.5rem 0; 28 | display: block; 29 | } 30 | 31 | .page-title--dhcp { 32 | flex-direction: column; 33 | align-items: flex-start; 34 | padding-bottom: 0.5rem; 35 | } 36 | 37 | .col__dhcp { 38 | flex: 0 0 100%; 39 | max-width: 100%; 40 | padding-right: 0.75rem; 41 | } 42 | 43 | .dhcp__interfaces { 44 | flex-direction: column; 45 | padding-bottom: 0.5rem; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /client/src/components/Settings/Dns/Access/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | import { shallowEqual, useDispatch, useSelector } from 'react-redux'; 4 | 5 | import Form from './Form'; 6 | 7 | import Card from '../../../ui/Card'; 8 | import { setAccessList } from '../../../../actions/access'; 9 | import { RootState } from '../../../../initialState'; 10 | 11 | const Access = () => { 12 | const { t } = useTranslation(); 13 | const dispatch = useDispatch(); 14 | const { processingSet, ...values } = useSelector((state: RootState) => state.access, shallowEqual); 15 | 16 | const handleFormSubmit = (values: any) => { 17 | dispatch(setAccessList(values)); 18 | }; 19 | 20 | return ( 21 | 22 |
23 | 24 | ); 25 | }; 26 | 27 | export default Access; 28 | -------------------------------------------------------------------------------- /client/src/components/Settings/Encryption/KeyStatus.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { withTranslation, Trans } from 'react-i18next'; 3 | 4 | interface KeyStatusProps { 5 | validKey: boolean; 6 | keyType: string; 7 | } 8 | 9 | const KeyStatus = ({ validKey, keyType }: KeyStatusProps) => ( 10 | 11 |
12 | encryption_status: 13 |
14 | 15 |
    16 |
  • 17 | {validKey ? ( 18 | encryption_key_valid 19 | ) : ( 20 | encryption_key_invalid 21 | )} 22 |
  • 23 |
24 |
25 | ); 26 | 27 | export default withTranslation()(KeyStatus); 28 | -------------------------------------------------------------------------------- /client/src/components/Settings/FormButton.css: -------------------------------------------------------------------------------- 1 | .form__button { 2 | margin-left: 1.5rem; 3 | } 4 | 5 | @media (max-width: 500px) { 6 | .form__button { 7 | margin-left: 0; 8 | margin-top: 1rem; 9 | display: block; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /client/src/components/SetupGuide/Guide.css: -------------------------------------------------------------------------------- 1 | .guide { 2 | max-width: 768px; 3 | margin: 0 auto; 4 | } 5 | 6 | .guide__title { 7 | margin-bottom: 10px; 8 | font-size: 17px; 9 | font-weight: 700; 10 | } 11 | 12 | .guide__desc { 13 | margin-bottom: 20px; 14 | font-size: 15px; 15 | } 16 | 17 | .guide__list { 18 | margin-top: 16px; 19 | padding-left: 0; 20 | } 21 | 22 | @media screen and (min-width: 768px) { 23 | .guide__list { 24 | padding-left: 24px; 25 | } 26 | } 27 | 28 | .guide__address { 29 | display: block; 30 | margin-bottom: 7px; 31 | font-size: 13px; 32 | font-weight: 700; 33 | } 34 | 35 | @media screen and (min-width: 768px) { 36 | .guide__address { 37 | display: list-item; 38 | font-size: 15px; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /client/src/components/Toasts/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector, shallowEqual } from 'react-redux'; 3 | 4 | import { CSSTransition, TransitionGroup } from 'react-transition-group'; 5 | import { TOAST_TRANSITION_TIMEOUT } from '../../helpers/constants'; 6 | 7 | import Toast from './Toast'; 8 | import './Toast.css'; 9 | import { RootState } from '../../initialState'; 10 | 11 | const Toasts = () => { 12 | const toasts = useSelector((state: RootState) => state.toasts, shallowEqual); 13 | 14 | return ( 15 | 16 | {toasts.notices?.map((toast: any) => { 17 | const { id } = toast; 18 | 19 | return ( 20 | 21 | 22 | 23 | ); 24 | })} 25 | 26 | ); 27 | }; 28 | 29 | export default Toasts; 30 | -------------------------------------------------------------------------------- /client/src/components/ui/Card.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './Card.css'; 4 | 5 | interface CardProps { 6 | id?: string; 7 | title?: string; 8 | subtitle?: string; 9 | bodyType?: string; 10 | type?: string; 11 | refresh?: React.ReactNode; 12 | children: React.ReactNode; 13 | } 14 | 15 | const Card = ({ type, id, title, subtitle, refresh, bodyType, children }: CardProps) => ( 16 |
17 | {(title || subtitle) && ( 18 |
19 |
20 | {title &&
{title}
} 21 | 22 | {subtitle &&
} 23 |
24 | 25 | {refresh &&
{refresh}
} 26 |
27 | )} 28 | 29 |
{children}
30 |
31 | ); 32 | 33 | export default Card; 34 | -------------------------------------------------------------------------------- /client/src/components/ui/Cell.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import LogsSearchLink from './LogsSearchLink'; 4 | 5 | import { formatNumber } from '../../helpers/helpers'; 6 | 7 | interface CellProps { 8 | value: number; 9 | percent: number; 10 | color: string; 11 | search?: string; 12 | onSearchRedirect?: (...args: unknown[]) => string; 13 | } 14 | 15 | const Cell = ({ value, percent, color, search }: CellProps) => ( 16 |
17 |
18 | 19 | {search ? {formatNumber(value)} : formatNumber(value)} 20 | 21 | 22 | {percent}% 23 |
24 | 25 |
26 |
33 |
34 |
35 | ); 36 | 37 | export default Cell; 38 | -------------------------------------------------------------------------------- /client/src/components/ui/CellWrap.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface CellWrapProps { 4 | value?: string | number; 5 | formatValue?: (...args: unknown[]) => unknown; 6 | formatTitle?: (...args: unknown[]) => unknown; 7 | } 8 | 9 | const CellWrap = ({ value }: CellWrapProps, formatValue?: any, formatTitle = formatValue) => { 10 | if (!value) { 11 | return '–'; 12 | } 13 | const cellValue = typeof formatValue === 'function' ? formatValue(value) : value; 14 | const cellTitle = typeof formatTitle === 'function' ? formatTitle(value) : value; 15 | 16 | return ( 17 |
18 | 19 | {cellValue} 20 | 21 |
22 | ); 23 | }; 24 | 25 | export default CellWrap; 26 | -------------------------------------------------------------------------------- /client/src/components/ui/Controls/Select.tsx: -------------------------------------------------------------------------------- 1 | import React, { ComponentProps, forwardRef } from 'react'; 2 | import clsx from 'clsx'; 3 | 4 | type SelectProps = ComponentProps<'select'> & { 5 | label?: string; 6 | error?: string; 7 | }; 8 | 9 | export const Select = forwardRef( 10 | ({ name, label, className, error, children, ...rest }, ref) => ( 11 |
12 | {label && ( 13 | 16 | )} 17 |
18 | 21 |
22 | {error &&
{error}
} 23 |
24 | ), 25 | ); 26 | 27 | Select.displayName = 'Select'; 28 | -------------------------------------------------------------------------------- /client/src/components/ui/Guide/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Guide'; 2 | -------------------------------------------------------------------------------- /client/src/components/ui/Icons.css: -------------------------------------------------------------------------------- 1 | .icons { 2 | display: inline-block; 3 | vertical-align: middle; 4 | } 5 | 6 | .icon--24 { 7 | --size: 1.5rem; 8 | 9 | width: var(--size); 10 | height: var(--size); 11 | } 12 | 13 | .icon--20 { 14 | --size: 1.25rem; 15 | 16 | width: var(--size); 17 | height: var(--size); 18 | } 19 | 20 | .icon--18 { 21 | --size: 1.125rem; 22 | 23 | width: var(--size); 24 | height: var(--size); 25 | } 26 | 27 | .icon--15 { 28 | --size: 0.95rem; 29 | 30 | width: var(--size); 31 | height: var(--size); 32 | } 33 | 34 | .icon--gray { 35 | color: var(--gray-a5); 36 | } 37 | 38 | .icon--green { 39 | color: var(--green-74); 40 | } 41 | 42 | .icon--disabled { 43 | color: var(--gray-d8); 44 | } 45 | 46 | .icon--lightgray { 47 | color: var(--gray-8); 48 | } 49 | -------------------------------------------------------------------------------- /client/src/components/ui/Line.css: -------------------------------------------------------------------------------- 1 | .line__tooltip { 2 | padding: 2px 10px 7px; 3 | line-height: 1.1; 4 | color: var(--white); 5 | background-color: var(--gray-3); 6 | border-radius: 4px; 7 | opacity: 90%; 8 | } 9 | 10 | .line__tooltip-text { 11 | font-size: 0.7rem; 12 | } 13 | 14 | .card-chart-bg { 15 | color: var(--black); 16 | } 17 | 18 | .card-chart-bg path[d^='M0,32'] { 19 | transform: translateY(32px); 20 | } 21 | -------------------------------------------------------------------------------- /client/src/components/ui/Loading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import { useTranslation } from 'react-i18next'; 4 | import './Loading.css'; 5 | 6 | interface LoadingProps { 7 | className?: string; 8 | text?: string; 9 | } 10 | 11 | const Loading = ({ className, text }: LoadingProps) => { 12 | const { t } = useTranslation(); 13 | 14 | return
{t(text)}
; 15 | }; 16 | 17 | export default Loading; 18 | -------------------------------------------------------------------------------- /client/src/components/ui/LogsSearchLink.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Link } from 'react-router-dom'; 4 | import { useTranslation } from 'react-i18next'; 5 | 6 | import { getLogsUrlParams } from '../../helpers/helpers'; 7 | import { MENU_URLS } from '../../helpers/constants'; 8 | 9 | interface LogsSearchLinkProps { 10 | children: string | number | React.ReactElement; 11 | search?: string; 12 | response_status?: string; 13 | link?: string; 14 | } 15 | 16 | const LogsSearchLink = ({ 17 | search = '', 18 | response_status = '', 19 | children, 20 | link = MENU_URLS.logs, 21 | }: LogsSearchLinkProps) => { 22 | const { t } = useTranslation(); 23 | 24 | const to = 25 | link === MENU_URLS.logs 26 | ? `${MENU_URLS.logs}${getLogsUrlParams(search && `"${search}"`, response_status)}` 27 | : link; 28 | 29 | return ( 30 | 31 | {children} 32 | 33 | ); 34 | }; 35 | 36 | export default LogsSearchLink; 37 | -------------------------------------------------------------------------------- /client/src/components/ui/Modal.css: -------------------------------------------------------------------------------- 1 | .ReactModal__Overlay { 2 | -webkit-perspective: 600; 3 | perspective: 600; 4 | opacity: 0; 5 | overflow-x: hidden; 6 | overflow-y: auto; 7 | background-color: rgba(0, 0, 0, 0.5); 8 | z-index: 104; 9 | } 10 | 11 | .ReactModal__Overlay--after-open { 12 | opacity: 1; 13 | transition: opacity 150ms ease-out; 14 | background-color: var(--modal-overlay-bgcolor) !important; 15 | } 16 | 17 | .ReactModal__Content { 18 | -webkit-transform: scale(0.5) rotateX(-30deg); 19 | transform: scale(0.5) rotateX(-30deg); 20 | } 21 | 22 | .ReactModal__Content--after-open { 23 | -webkit-transform: scale(1) rotateX(0deg); 24 | transform: scale(1) rotateX(0deg); 25 | transition: all 150ms ease-in; 26 | } 27 | 28 | .ReactModal__Overlay--before-close { 29 | opacity: 0; 30 | } 31 | 32 | .ReactModal__Content--before-close { 33 | -webkit-transform: scale(0.5) rotateX(30deg); 34 | transform: scale(0.5) rotateX(30deg); 35 | transition: all 150ms ease-in; 36 | } 37 | 38 | .ReactModal__Content.modal-dialog { 39 | border: none; 40 | background-color: transparent; 41 | } 42 | 43 | @media (min-width: 576px) { 44 | .modal-dialog--clients, 45 | .modal-dialog--schedule { 46 | max-width: 650px; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /client/src/components/ui/PageTitle.css: -------------------------------------------------------------------------------- 1 | .page-header { 2 | flex-direction: column; 3 | align-items: flex-start; 4 | } 5 | 6 | .page-header--logs { 7 | flex-direction: row; 8 | align-items: flex-end; 9 | margin: 0.5rem 0 2.8rem; 10 | } 11 | 12 | .page-header--logs .page-title { 13 | display: inline-flex; 14 | align-items: center; 15 | } 16 | 17 | @media (max-width: 991px) { 18 | .page-header--logs { 19 | flex-direction: column; 20 | align-items: center; 21 | margin-bottom: 0 0 1.1rem; 22 | } 23 | 24 | .page-header--logs .page-title { 25 | margin-bottom: 1.1rem; 26 | font-size: 1.8rem; 27 | } 28 | } 29 | 30 | .page-subtitle { 31 | margin-left: 0; 32 | font-size: 0.9rem; 33 | } 34 | 35 | .page-title--large { 36 | font-size: 36px; 37 | line-height: 46px; 38 | } 39 | -------------------------------------------------------------------------------- /client/src/components/ui/PageTitle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './PageTitle.css'; 4 | 5 | interface PageTitleProps { 6 | title: string; 7 | subtitle?: string; 8 | children?: React.ReactNode; 9 | containerClass?: string; 10 | } 11 | 12 | const PageTitle = ({ title, subtitle, children, containerClass }: PageTitleProps) => ( 13 |
14 |
15 |

{title}

16 | {children} 17 |
18 | 19 | {subtitle &&
{subtitle}
} 20 |
21 | ); 22 | 23 | export default PageTitle; 24 | -------------------------------------------------------------------------------- /client/src/components/ui/Status.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withTranslation, Trans } from 'react-i18next'; 3 | 4 | import Card from './Card'; 5 | 6 | interface StatusProps { 7 | message: string; 8 | buttonMessage?: string; 9 | reloadPage?: (...args: unknown[]) => unknown; 10 | } 11 | 12 | const Status = ({ message, buttonMessage, reloadPage }: StatusProps) => ( 13 |
14 | 15 |
16 | {message} 17 |
18 | {buttonMessage && ( 19 | 22 | )} 23 |
24 |
25 | ); 26 | 27 | export default withTranslation()(Status); 28 | -------------------------------------------------------------------------------- /client/src/components/ui/Tab.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classnames from 'classnames'; 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | interface TabProps { 6 | activeTabLabel: string; 7 | label: string; 8 | onClick: (...args: unknown[]) => unknown; 9 | title?: string; 10 | } 11 | 12 | const Tab = ({ activeTabLabel, label, title, onClick }: TabProps) => { 13 | const [t] = useTranslation(); 14 | const handleClick = () => onClick(label); 15 | 16 | const tabClass = classnames({ 17 | tab__control: true, 18 | 'tab__control--active': activeTabLabel === label, 19 | }); 20 | 21 | return ( 22 |
23 | 24 | 25 | 26 | {t(title || label)} 27 |
28 | ); 29 | }; 30 | 31 | export default Tab; 32 | -------------------------------------------------------------------------------- /client/src/components/ui/Tooltip.css: -------------------------------------------------------------------------------- 1 | .tooltip-container { 2 | border: 0; 3 | padding: 0.7rem; 4 | box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.2); 5 | } 6 | 7 | [data-theme='dark'] .tooltip-container { 8 | background-color: var(--ctrl-select-bgcolor); 9 | color: var(--mcolor); 10 | } 11 | 12 | .tooltip-custom--narrow { 13 | max-width: 14rem; 14 | } 15 | 16 | .tooltip-custom--wide { 17 | max-width: 18rem; 18 | } 19 | 20 | .tooltip-custom__trigger { 21 | cursor: pointer; 22 | } 23 | 24 | .tooltip-custom__content-title { 25 | margin-bottom: 0.1875rem; 26 | } 27 | 28 | .tooltip-custom__content-item { 29 | margin-bottom: 0.125rem; 30 | } 31 | 32 | .tooltip-custom__content-item:last-child { 33 | margin-bottom: 0; 34 | } 35 | 36 | .tooltip-custom__content-link { 37 | color: var(--green-86); 38 | } 39 | -------------------------------------------------------------------------------- /client/src/components/ui/Topline.css: -------------------------------------------------------------------------------- 1 | .topline { 2 | position: relative; 3 | z-index: 102; 4 | margin-bottom: 0; 5 | padding: 0.75rem 0; 6 | } 7 | -------------------------------------------------------------------------------- /client/src/components/ui/Topline.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './Topline.css'; 4 | 5 | interface ToplineProps { 6 | children: React.ReactNode; 7 | type: string; 8 | } 9 | 10 | const Topline = (props: ToplineProps) => ( 11 |
12 |
{props.children}
13 |
14 | ); 15 | 16 | export default Topline; 17 | -------------------------------------------------------------------------------- /client/src/components/ui/UpdateOverlay.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Trans } from 'react-i18next'; 3 | import classnames from 'classnames'; 4 | import { useSelector } from 'react-redux'; 5 | import './Overlay.css'; 6 | import { RootState } from '../../initialState'; 7 | 8 | const UpdateOverlay = () => { 9 | const processingUpdate = useSelector((state: RootState) => state.dashboard.processingUpdate); 10 | const overlayClass = classnames('overlay', { 11 | 'overlay--visible': processingUpdate, 12 | }); 13 | 14 | return ( 15 |
16 |
17 | 18 | processing_update 19 |
20 | ); 21 | }; 22 | 23 | export default UpdateOverlay; 24 | -------------------------------------------------------------------------------- /client/src/components/ui/Version.css: -------------------------------------------------------------------------------- 1 | .version { 2 | font-size: 0.8rem; 3 | } 4 | 5 | @media screen and (min-width: 1280px) { 6 | .version { 7 | font-size: 0.85rem; 8 | } 9 | } 10 | 11 | .version__value { 12 | font-weight: 600; 13 | } 14 | 15 | @media screen and (min-width: 992px) { 16 | .version__value { 17 | max-width: 100%; 18 | overflow: visible; 19 | } 20 | } 21 | 22 | .version__link { 23 | position: relative; 24 | display: inline-block; 25 | border-bottom: 1px dashed #495057; 26 | cursor: pointer; 27 | } 28 | 29 | .version__text { 30 | display: flex; 31 | align-items: center; 32 | justify-content: center; 33 | } 34 | 35 | @media screen and (min-width: 992px) { 36 | .version__text { 37 | justify-content: flex-end; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /client/src/components/ui/svg/chevron-down.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /client/src/components/ui/svg/globe.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /client/src/components/ui/svg/help-circle-gray.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /client/src/components/ui/svg/help-circle.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /client/src/components/ui/svg/trash-2.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /client/src/components/ui/svg/x.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /client/src/components/ui/texareaCommentsHighlight.css: -------------------------------------------------------------------------------- 1 | .text-edit-container { 2 | position: relative; 3 | min-height: 240px; 4 | overflow: hidden; 5 | } 6 | 7 | .text-input, 8 | .text-output { 9 | position: absolute; 10 | top: 0; 11 | left: 0; 12 | width: 100%; 13 | height: 100%; 14 | padding: 16px; 15 | background: transparent; 16 | white-space: pre-wrap; 17 | line-height: 24px; 18 | word-wrap: break-word; 19 | font-size: var(--font-size-disable-autozoom); 20 | margin: 0; 21 | overscroll-behavior: none; 22 | } 23 | 24 | .text-input { 25 | position: relative; 26 | opacity: 1; 27 | min-height: 240px; 28 | } 29 | 30 | .text-output { 31 | pointer-events: none; 32 | z-index: 3; 33 | overflow-y: auto; 34 | background: transparent; 35 | border: 1px solid transparent; 36 | } 37 | 38 | .text-transparent { 39 | color: transparent; 40 | } 41 | -------------------------------------------------------------------------------- /client/src/configureStore.ts: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose, Reducer } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | 4 | const middlewares = [thunk]; 5 | 6 | export default function configureStore( 7 | reducer: Reducer, 8 | initialState: any 9 | ) { 10 | const store = createStore( 11 | reducer, 12 | initialState, 13 | compose(applyMiddleware(...middlewares)) 14 | ); 15 | return store; 16 | } 17 | -------------------------------------------------------------------------------- /client/src/containers/Clients.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import { getClients } from '../actions'; 4 | import { getStats } from '../actions/stats'; 5 | import { addClient, updateClient, deleteClient, toggleClientModal } from '../actions/clients'; 6 | 7 | import Clients from '../components/Settings/Clients'; 8 | 9 | const mapStateToProps = (state: any) => { 10 | const { dashboard, clients, stats } = state; 11 | const props = { 12 | dashboard, 13 | clients, 14 | stats, 15 | }; 16 | return props; 17 | }; 18 | 19 | type DispatchProps = { 20 | getClients: (dispatch: any) => void; 21 | getStats: (...args: unknown[]) => unknown; 22 | addClient: (dispatch: any) => void; 23 | updateClient: (config: any, name: any) => (dispatch: any) => void; 24 | deleteClient: (config: any, name: any) => (dispatch: any) => void; 25 | toggleClientModal: (...args: unknown[]) => unknown; 26 | } 27 | 28 | const mapDispatchToProps: DispatchProps = { 29 | getClients, 30 | getStats, 31 | addClient, 32 | updateClient, 33 | deleteClient, 34 | toggleClientModal, 35 | }; 36 | 37 | export default connect(mapStateToProps, mapDispatchToProps)(Clients); 38 | -------------------------------------------------------------------------------- /client/src/containers/CustomRules.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { setRules, getFilteringStatus, handleRulesChange, checkHost } from '../actions/filtering'; 3 | 4 | import CustomRules from '../components/Filters/CustomRules'; 5 | import { RootState } from '../initialState'; 6 | 7 | const mapStateToProps = (state: RootState) => { 8 | const { filtering } = state; 9 | const props = { filtering }; 10 | return props; 11 | }; 12 | 13 | type DispatchProps = { 14 | setRules: (...args: unknown[]) => unknown; 15 | getFilteringStatus: (...args: unknown[]) => unknown; 16 | handleRulesChange: (...args: unknown[]) => unknown; 17 | checkHost: (dispatch: any) => void; 18 | } 19 | 20 | const mapDispatchToProps: DispatchProps = { 21 | setRules, 22 | getFilteringStatus, 23 | handleRulesChange, 24 | checkHost, 25 | }; 26 | 27 | export default connect(mapStateToProps, mapDispatchToProps)(CustomRules); 28 | -------------------------------------------------------------------------------- /client/src/containers/Dashboard.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import { toggleProtection, getClients } from '../actions'; 4 | import { getStats, getStatsConfig } from '../actions/stats'; 5 | import { getAccessList } from '../actions/access'; 6 | 7 | import Dashboard from '../components/Dashboard'; 8 | import { RootState } from '../initialState'; 9 | 10 | const mapStateToProps = (state: RootState) => { 11 | const { dashboard, stats, access } = state; 12 | const props = { dashboard, stats, access }; 13 | return props; 14 | }; 15 | 16 | type DispatchProps = { 17 | toggleProtection: (...args: unknown[]) => unknown; 18 | getClients: (...args: unknown[]) => unknown; 19 | getStats: (...args: unknown[]) => unknown; 20 | getStatsConfig: (...args: unknown[]) => unknown; 21 | getAccessList: () => (dispatch: any) => void; 22 | }; 23 | 24 | const mapDispatchToProps: DispatchProps = { 25 | toggleProtection, 26 | getClients, 27 | getStats, 28 | getStatsConfig, 29 | getAccessList, 30 | }; 31 | 32 | export default connect(mapStateToProps, mapDispatchToProps)(Dashboard); 33 | -------------------------------------------------------------------------------- /client/src/containers/Dhcp.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { 3 | toggleDhcp, 4 | getDhcpStatus, 5 | getDhcpInterfaces, 6 | setDhcpConfig, 7 | findActiveDhcp, 8 | toggleLeaseModal, 9 | addStaticLease, 10 | removeStaticLease, 11 | resetDhcp, 12 | } from '../actions'; 13 | 14 | import Dhcp from '../components/Settings/Dhcp'; 15 | 16 | const mapStateToProps = (state: any) => { 17 | const { dhcp } = state; 18 | const props = { 19 | dhcp, 20 | }; 21 | return props; 22 | }; 23 | 24 | const mapDispatchToProps = { 25 | toggleDhcp, 26 | getDhcpStatus, 27 | getDhcpInterfaces, 28 | setDhcpConfig, 29 | findActiveDhcp, 30 | toggleLeaseModal, 31 | addStaticLease, 32 | removeStaticLease, 33 | resetDhcp, 34 | }; 35 | 36 | export default connect(mapStateToProps, mapDispatchToProps)(Dhcp); 37 | -------------------------------------------------------------------------------- /client/src/containers/Dns.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { getAccessList, setAccessList } from '../actions/access'; 3 | import { getRewritesList, addRewrite, deleteRewrite, toggleRewritesModal } from '../actions/rewrites'; 4 | import { getDnsConfig, setDnsConfig } from '../actions/dnsConfig'; 5 | 6 | import Dns from '../components/Settings/Dns'; 7 | 8 | const mapStateToProps = (state: any) => { 9 | const { dashboard, settings, access, rewrites, dnsConfig } = state; 10 | const props = { 11 | dashboard, 12 | settings, 13 | access, 14 | rewrites, 15 | dnsConfig, 16 | }; 17 | return props; 18 | }; 19 | 20 | const mapDispatchToProps = { 21 | getAccessList, 22 | setAccessList, 23 | getRewritesList, 24 | addRewrite, 25 | deleteRewrite, 26 | toggleRewritesModal, 27 | getDnsConfig, 28 | setDnsConfig, 29 | }; 30 | 31 | export default connect(mapStateToProps, mapDispatchToProps)(Dns); 32 | -------------------------------------------------------------------------------- /client/src/containers/DnsAllowlist.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { 3 | setRules, 4 | getFilteringStatus, 5 | addFilter, 6 | removeFilter, 7 | toggleFilterStatus, 8 | toggleFilteringModal, 9 | refreshFilters, 10 | handleRulesChange, 11 | editFilter, 12 | } from '../actions/filtering'; 13 | 14 | import DnsAllowlist from '../components/Filters/DnsAllowlist'; 15 | 16 | const mapStateToProps = (state: any) => { 17 | const { filtering } = state; 18 | const props = { filtering }; 19 | return props; 20 | }; 21 | 22 | const mapDispatchToProps = { 23 | setRules, 24 | getFilteringStatus, 25 | addFilter, 26 | removeFilter, 27 | toggleFilterStatus, 28 | toggleFilteringModal, 29 | refreshFilters, 30 | handleRulesChange, 31 | editFilter, 32 | }; 33 | 34 | export default connect(mapStateToProps, mapDispatchToProps)(DnsAllowlist); 35 | -------------------------------------------------------------------------------- /client/src/containers/DnsBlocklist.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { 3 | setRules, 4 | getFilteringStatus, 5 | addFilter, 6 | removeFilter, 7 | toggleFilterStatus, 8 | toggleFilteringModal, 9 | refreshFilters, 10 | handleRulesChange, 11 | editFilter, 12 | } from '../actions/filtering'; 13 | 14 | import DnsBlocklist from '../components/Filters/DnsBlocklist'; 15 | 16 | const mapStateToProps = (state: any) => { 17 | const { filtering } = state; 18 | const props = { filtering }; 19 | return props; 20 | }; 21 | 22 | const mapDispatchToProps = { 23 | setRules, 24 | getFilteringStatus, 25 | addFilter, 26 | removeFilter, 27 | toggleFilterStatus, 28 | toggleFilteringModal, 29 | refreshFilters, 30 | handleRulesChange, 31 | editFilter, 32 | }; 33 | 34 | export default connect(mapStateToProps, mapDispatchToProps)(DnsBlocklist); 35 | -------------------------------------------------------------------------------- /client/src/containers/DnsRewrites.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { getRewritesList, addRewrite, deleteRewrite, updateRewrite, toggleRewritesModal } from '../actions/rewrites'; 3 | 4 | import Rewrites from '../components/Filters/Rewrites'; 5 | import { RootState } from '../initialState'; 6 | 7 | const mapStateToProps = (state: RootState) => { 8 | const { rewrites } = state; 9 | const props = { rewrites }; 10 | return props; 11 | }; 12 | 13 | type DispatchProps = { 14 | getRewritesList: () => (dispatch: any) => void; 15 | toggleRewritesModal: (...args: unknown[]) => unknown; 16 | addRewrite: (...args: unknown[]) => unknown; 17 | deleteRewrite: (...args: unknown[]) => unknown; 18 | updateRewrite: (...args: unknown[]) => unknown; 19 | } 20 | 21 | const mapDispatchToProps: DispatchProps = { 22 | getRewritesList, 23 | addRewrite, 24 | deleteRewrite, 25 | updateRewrite, 26 | toggleRewritesModal, 27 | }; 28 | 29 | export default connect(mapStateToProps, mapDispatchToProps)(Rewrites); 30 | -------------------------------------------------------------------------------- /client/src/containers/Encryption.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { getTlsStatus, setTlsConfig, validateTlsConfig } from '../actions/encryption'; 3 | 4 | import { Encryption } from '../components/Settings/Encryption'; 5 | 6 | const mapStateToProps = (state: any) => { 7 | const { encryption } = state; 8 | const props = { 9 | encryption, 10 | }; 11 | return props; 12 | }; 13 | 14 | const mapDispatchToProps = { 15 | getTlsStatus, 16 | setTlsConfig, 17 | validateTlsConfig, 18 | }; 19 | 20 | export default connect(mapStateToProps, mapDispatchToProps)(Encryption); 21 | -------------------------------------------------------------------------------- /client/src/containers/Settings.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import { initSettings, toggleSetting } from '../actions'; 4 | import { getBlockedServices, updateBlockedServices } from '../actions/services'; 5 | import { getStatsConfig, setStatsConfig, resetStats } from '../actions/stats'; 6 | import { clearLogs, getLogsConfig, setLogsConfig } from '../actions/queryLogs'; 7 | import { getFilteringStatus, setFiltersConfig } from '../actions/filtering'; 8 | 9 | import Settings from '../components/Settings'; 10 | 11 | const mapStateToProps = (state: any) => { 12 | const { settings, services, stats, queryLogs, filtering } = state; 13 | const props = { 14 | settings, 15 | services, 16 | stats, 17 | queryLogs, 18 | filtering, 19 | }; 20 | return props; 21 | }; 22 | 23 | const mapDispatchToProps = { 24 | initSettings, 25 | toggleSetting, 26 | getBlockedServices, 27 | updateBlockedServices, 28 | getStatsConfig, 29 | setStatsConfig, 30 | resetStats, 31 | clearLogs, 32 | getLogsConfig, 33 | setLogsConfig, 34 | getFilteringStatus, 35 | setFiltersConfig, 36 | }; 37 | 38 | export default connect(mapStateToProps, mapDispatchToProps)(Settings); 39 | -------------------------------------------------------------------------------- /client/src/containers/SetupGuide.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import * as actionCreators from '../actions'; 4 | 5 | import SetupGuide from '../components/SetupGuide'; 6 | 7 | const mapStateToProps = (state: any) => { 8 | const { dashboard } = state; 9 | const props = { dashboard }; 10 | return props; 11 | }; 12 | 13 | export default connect(mapStateToProps, actionCreators)(SetupGuide); 14 | -------------------------------------------------------------------------------- /client/src/helpers/form.tsx: -------------------------------------------------------------------------------- 1 | import { R_MAC_WITHOUT_COLON, R_UNIX_ABSOLUTE_PATH, R_WIN_ABSOLUTE_PATH } from './constants'; 2 | 3 | /** 4 | * 5 | * @param {string} ip 6 | * @returns {*} 7 | */ 8 | export const ip4ToInt = (ip: any) => { 9 | const intIp = ip.split('.').reduce((int: any, oct: any) => int * 256 + parseInt(oct, 10), 0); 10 | return Number.isNaN(intIp) ? 0 : intIp; 11 | }; 12 | 13 | /** 14 | * @param value {string} 15 | * @returns {*|number} 16 | */ 17 | export const toNumber = (value: any) => value && parseInt(value, 10); 18 | 19 | /** 20 | * @param value {string} 21 | * @returns {*|number} 22 | */ 23 | 24 | export const toFloatNumber = (value: any) => value && parseFloat(value); 25 | 26 | /** 27 | * @param value {string} 28 | * @returns {boolean} 29 | */ 30 | export const isValidAbsolutePath = (value: any) => R_WIN_ABSOLUTE_PATH.test(value) || R_UNIX_ABSOLUTE_PATH.test(value); 31 | 32 | /** 33 | * @param value {string} 34 | * @returns {*|string} 35 | */ 36 | export const normalizeMac = (value: any) => { 37 | if (value && R_MAC_WITHOUT_COLON.test(value)) { 38 | return value.match(/.{2}/g).join(':'); 39 | } 40 | 41 | return value; 42 | }; 43 | -------------------------------------------------------------------------------- /client/src/helpers/highlightTextareaComments.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classnames from 'classnames'; 3 | import { COMMENT_LINE_DEFAULT_TOKEN } from './constants'; 4 | 5 | const renderHighlightedLine = (line: any, idx: any, commentLineTokens = [COMMENT_LINE_DEFAULT_TOKEN]) => { 6 | const isComment = commentLineTokens.some((token) => line.trim().startsWith(token)); 7 | 8 | const lineClassName = classnames({ 9 | 'text-gray': isComment, 10 | 'text-transparent': !isComment, 11 | }); 12 | 13 | return ( 14 |
15 | {line || '\n'} 16 |
17 | ); 18 | }; 19 | export const getTextareaCommentsHighlight = (ref: any, lines: any, commentLineTokens?: any, className = '') => { 20 | const renderLine = (line: any, idx: any) => renderHighlightedLine(line, idx, commentLineTokens); 21 | 22 | return ( 23 | 24 | {lines?.split('\n').map(renderLine)} 25 | 26 | ); 27 | }; 28 | 29 | export const syncScroll = (e: any, ref: any) => { 30 | // eslint-disable-next-line no-param-reassign 31 | ref.current.scrollTop = e.target.scrollTop; 32 | }; 33 | -------------------------------------------------------------------------------- /client/src/helpers/trackers/whotracksme_web.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeUpdated": "2021-12-19T13:50:00.512Z", 3 | "websites": { 4 | "netflix": "netflix.com" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /client/src/helpers/twosky.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-relative-packages 2 | import twosky from '../../../.twosky.json'; 3 | 4 | export const LANGUAGES = twosky[0].languages; 5 | export const BASE_LOCALE = twosky[0].base_locale; 6 | -------------------------------------------------------------------------------- /client/src/helpers/useDebounce.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | const useDebounce = (value: any, delay: any) => { 4 | const [debouncedValue, setDebouncedValue] = useState(value); 5 | 6 | useEffect(() => { 7 | const handler = setTimeout(() => { 8 | setDebouncedValue(value); 9 | }, delay); 10 | 11 | return () => { 12 | clearTimeout(handler); 13 | }; 14 | }, [value, delay]); 15 | 16 | return [debouncedValue, setDebouncedValue]; 17 | }; 18 | 19 | export default useDebounce; 20 | -------------------------------------------------------------------------------- /client/src/helpers/version.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Checks if versions are equal. 3 | * Please note, that this method strips the "v" prefix. 4 | * 5 | * @param left {string} - left version 6 | * @param right {string} - right version 7 | * @return {boolean} true if versions are equal 8 | */ 9 | export const areEqualVersions = (left: any, right: any) => { 10 | if (!left || !right) { 11 | return false; 12 | } 13 | 14 | const leftVersion = left.replace(/^v/, ''); 15 | const rightVersion = right.replace(/^v/, ''); 16 | return leftVersion === rightVersion; 17 | }; 18 | -------------------------------------------------------------------------------- /client/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import configureStore from './configureStore'; 5 | import reducers from './reducers'; 6 | 7 | import App from './components/App'; 8 | import './components/App/index.css'; 9 | import './i18n'; 10 | import { RootState, initialState } from './initialState'; 11 | 12 | const store = configureStore(reducers, initialState); 13 | 14 | ReactDOM.render( 15 | 16 | 17 | , 18 | document.getElementById('root'), 19 | ); 20 | -------------------------------------------------------------------------------- /client/src/install/Setup/Devices.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Trans } from 'react-i18next'; 4 | 5 | import { Guide } from '../../components/ui/Guide'; 6 | 7 | import Controls from './Controls'; 8 | 9 | import AddressList from './AddressList'; 10 | import { InstallInterface } from '../../initialState'; 11 | import { DnsConfig } from './Settings'; 12 | 13 | type Props = { 14 | interfaces: InstallInterface[]; 15 | dnsConfig: DnsConfig; 16 | }; 17 | 18 | export const Devices = ({ interfaces, dnsConfig }: Props) => ( 19 |
20 |
21 |
22 | install_devices_title 23 |
24 | 25 |
26 | install_devices_desc 27 | 28 |
29 | install_devices_address: 30 |
31 | 32 |
33 | 34 |
35 |
36 | 37 | 38 |
39 | 40 | 41 |
42 | ); 43 | -------------------------------------------------------------------------------- /client/src/install/Setup/Greeting.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Trans, withTranslation } from 'react-i18next'; 3 | 4 | import Controls from './Controls'; 5 | 6 | const Greeting = () => ( 7 |
8 |
9 |

10 | install_welcome_title 11 |

12 | 13 |

14 | install_welcome_desc 15 |

16 |
17 | 18 | 19 |
20 | ); 21 | 22 | export default withTranslation()(Greeting); 23 | -------------------------------------------------------------------------------- /client/src/install/Setup/Progress.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Trans } from 'react-i18next'; 3 | 4 | import { INSTALL_TOTAL_STEPS } from '../../helpers/constants'; 5 | 6 | const getProgressPercent = (step: number) => (step / INSTALL_TOTAL_STEPS) * 100; 7 | 8 | type Props = { 9 | step: number; 10 | }; 11 | 12 | export const Progress = ({ step }: Props) => ( 13 |
14 | install_step {step}/{INSTALL_TOTAL_STEPS} 15 |
16 |
17 |
18 |
19 | ); 20 | -------------------------------------------------------------------------------- /client/src/install/Setup/Submit.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Trans } from 'react-i18next'; 4 | 5 | import Controls from './Controls'; 6 | import { WebConfig } from './Settings'; 7 | 8 | type Props = { 9 | webConfig: WebConfig; 10 | openDashboard: (ip: string, port: number) => void; 11 | }; 12 | 13 | export const Submit = ({ openDashboard, webConfig }: Props) => ( 14 |
15 |
16 |

17 | install_submit_title 18 |

19 | 20 |

21 | install_submit_desc 22 |

23 |
24 | 25 | 26 |
27 | ); 28 | -------------------------------------------------------------------------------- /client/src/install/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | 5 | import '../components/App/index.css'; 6 | import '../components/ui/ReactTable.css'; 7 | import configureStore from '../configureStore'; 8 | import reducers from '../reducers/install'; 9 | import '../i18n'; 10 | 11 | import { Setup } from './Setup'; 12 | import { InstallState } from '../initialState'; 13 | 14 | const store = configureStore(reducers, {}); 15 | 16 | ReactDOM.render( 17 | 18 | 19 | , 20 | document.getElementById('root'), 21 | ); 22 | -------------------------------------------------------------------------------- /client/src/login/Login/Login.css: -------------------------------------------------------------------------------- 1 | /* Disable Auto Zoom in Input - Safari on iPhone https://stackoverflow.com/a/6394497 */ 2 | @media screen and (max-width: 767px) { 3 | input, 4 | select, 5 | textarea { 6 | font-size: 1rem; 7 | } 8 | } 9 | 10 | .login { 11 | display: flex; 12 | flex-direction: column; 13 | justify-content: space-between; 14 | align-items: stretch; 15 | min-height: 100vh; 16 | } 17 | 18 | [data-theme='dark'] .login__logo { 19 | filter: invert(1); 20 | } 21 | 22 | .login__form { 23 | margin: auto; 24 | padding: 40px 15px 100px; 25 | width: 100%; 26 | max-width: 24rem; 27 | } 28 | 29 | .login__info { 30 | position: relative; 31 | text-align: center; 32 | } 33 | 34 | .login__message, 35 | .login__link { 36 | font-size: 14px; 37 | font-weight: 400; 38 | letter-spacing: 0; 39 | } 40 | 41 | @media screen and (min-width: 992px) { 42 | .login__message { 43 | position: absolute; 44 | top: 40px; 45 | padding: 0 15px; 46 | } 47 | } 48 | 49 | .form__group { 50 | position: relative; 51 | margin-bottom: 15px; 52 | } 53 | 54 | .form__message { 55 | font-size: 11px; 56 | } 57 | 58 | .form__message--error { 59 | color: var(--red); 60 | } 61 | -------------------------------------------------------------------------------- /client/src/login/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | 5 | import '../components/App/index.css'; 6 | import '../components/ui/ReactTable.css'; 7 | import configureStore from '../configureStore'; 8 | import reducers from '../reducers/login'; 9 | import '../i18n'; 10 | 11 | import { Login } from './Login'; 12 | import { LoginState } from '../initialState'; 13 | 14 | const store = configureStore(reducers, {}); 15 | 16 | ReactDOM.render( 17 | 18 | 19 | , 20 | document.getElementById('root'), 21 | ); 22 | -------------------------------------------------------------------------------- /client/src/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { loadingBarReducer } from 'react-redux-loading-bar'; 3 | 4 | import toasts from './toasts'; 5 | import encryption from './encryption'; 6 | import clients from './clients'; 7 | import access from './access'; 8 | import rewrites from './rewrites'; 9 | import services from './services'; 10 | import stats from './stats'; 11 | import queryLogs from './queryLogs'; 12 | import dnsConfig from './dnsConfig'; 13 | import filtering from './filtering'; 14 | import settings from './settings'; 15 | import dashboard from './dashboard'; 16 | import dhcp from './dhcp'; 17 | 18 | export default combineReducers({ 19 | settings, 20 | dashboard, 21 | queryLogs, 22 | filtering, 23 | toasts, 24 | dhcp, 25 | encryption, 26 | clients, 27 | access, 28 | rewrites, 29 | services, 30 | stats, 31 | dnsConfig, 32 | loadingBar: loadingBarReducer, 33 | }); 34 | -------------------------------------------------------------------------------- /client/src/reducers/login.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import { handleActions } from 'redux-actions'; 4 | 5 | import * as actions from '../actions/login'; 6 | import toasts from './toasts'; 7 | 8 | const login = handleActions( 9 | { 10 | [actions.processLoginRequest.toString()]: (state: any) => ({ 11 | ...state, 12 | processingLogin: true, 13 | }), 14 | [actions.processLoginFailure.toString()]: (state: any) => ({ 15 | ...state, 16 | processingLogin: false, 17 | }), 18 | [actions.processLoginSuccess.toString()]: (state, { payload }: any) => ({ 19 | ...state, 20 | ...payload, 21 | processingLogin: false, 22 | }), 23 | }, 24 | { 25 | processingLogin: false, 26 | email: '', 27 | password: '', 28 | }, 29 | ); 30 | 31 | export default combineReducers({ 32 | login, 33 | toasts, 34 | }); 35 | -------------------------------------------------------------------------------- /client/src/types.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Window { 3 | __REDUX_DEVTOOLS_EXTENSION__?: () => any; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /client/tests/constants.ts: -------------------------------------------------------------------------------- 1 | export const ADMIN_USERNAME = 'admin'; 2 | export const ADMIN_PASSWORD = 'superpassword'; 3 | export const PORT = 3000; 4 | export const CONFIG_FILE_PATH = '/tmp/AdGuard.e2e.yaml'; 5 | -------------------------------------------------------------------------------- /client/tests/e2e/control-panel.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | import { ADMIN_USERNAME, ADMIN_PASSWORD } from '../constants'; 3 | 4 | test.describe('Control Panel', () => { 5 | test.beforeEach(async ({ page }) => { 6 | await page.goto('/login.html'); 7 | await page.getByTestId('username').click(); 8 | await page.getByTestId('username').fill(ADMIN_USERNAME); 9 | await page.getByTestId('password').click(); 10 | await page.getByTestId('password').fill(ADMIN_PASSWORD); 11 | await page.keyboard.press('Tab'); 12 | await page.getByTestId('sign_in').click(); 13 | await page.waitForURL((url) => !url.href.endsWith('/login.html')); 14 | }); 15 | 16 | test('should sign out successfully', async ({ page }) => { 17 | await page.getByTestId('sign_out').click(); 18 | 19 | await page.waitForURL((url) => url.href.endsWith('/login.html')); 20 | 21 | await expect(page.getByTestId('sign_in')).toBeVisible(); 22 | }); 23 | 24 | test('should change theme to dark and then light', async ({ page }) => { 25 | await page.getByTestId('theme_dark').click(); 26 | 27 | await expect(page.locator('body[data-theme="dark"]')).toBeVisible(); 28 | 29 | 30 | await page.getByTestId('theme_light').click(); 31 | 32 | await expect(page.locator('body:not([data-theme="dark"])')).toBeVisible(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /client/tests/e2e/globalSetup.ts: -------------------------------------------------------------------------------- 1 | import { chromium, type FullConfig } from '@playwright/test'; 2 | 3 | import { ADMIN_USERNAME, ADMIN_PASSWORD, PORT } from '../constants'; 4 | 5 | async function globalSetup(config: FullConfig) { 6 | const browser = await chromium.launch({ 7 | slowMo: 100, 8 | }); 9 | const page = await browser.newPage({ baseURL: config.webServer?.url }); 10 | 11 | try { 12 | await page.goto('/'); 13 | await page.getByTestId('install_get_started').click(); 14 | await page.getByTestId('install_web_port').fill(PORT.toString()); 15 | await page.getByTestId('install_next').click(); 16 | await page.getByTestId('install_username').fill(ADMIN_USERNAME); 17 | await page.getByTestId('install_password').fill(ADMIN_PASSWORD); 18 | await page.getByTestId('install_confirm_password').click(); 19 | await page.getByTestId('install_confirm_password').fill(ADMIN_PASSWORD); 20 | await page.getByTestId('install_next').click(); 21 | await page.getByTestId('install_next').click(); 22 | await page.getByTestId('install_open_dashboard').click(); 23 | await page.waitForURL((url) => !url.href.endsWith('/install.html')); 24 | } catch (error) { 25 | console.error('Error during global setup:', error); 26 | } finally { 27 | await browser.close(); 28 | } 29 | } 30 | 31 | export default globalSetup; 32 | -------------------------------------------------------------------------------- /client/tests/e2e/globalTeardown.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, unlinkSync } from 'fs'; 2 | import { CONFIG_FILE_PATH } from '../constants'; 3 | 4 | async function globalTeardown() { 5 | // Remove the test config file 6 | if (existsSync(CONFIG_FILE_PATH)) { 7 | unlinkSync(CONFIG_FILE_PATH); 8 | } 9 | } 10 | 11 | export default globalTeardown; 12 | -------------------------------------------------------------------------------- /client/tests/e2e/login.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@playwright/test'; 2 | import { ADMIN_PASSWORD, ADMIN_USERNAME } from '../constants'; 3 | 4 | 5 | test.describe('Login', () => { 6 | test('should successfully log in with valid credentials', async ({ page }) => { 7 | await page.goto('/login.html'); 8 | await page.getByTestId('username').click(); 9 | await page.getByTestId('username').fill(ADMIN_USERNAME); 10 | await page.getByTestId('password').click(); 11 | await page.getByTestId('password').fill(ADMIN_PASSWORD); 12 | await page.keyboard.press('Tab'); 13 | await page.getByTestId('sign_in').click(); 14 | await page.waitForURL((url) => !url.href.endsWith('/login.html')); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /client/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | environment: 'jsdom', 6 | include: ['src/__tests__/**'], 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /client/webpack.prod.js: -------------------------------------------------------------------------------- 1 | import { merge } from 'webpack-merge'; 2 | import common from './webpack.common.js'; 3 | 4 | export default merge(common, { 5 | stats: 'minimal', 6 | performance: { 7 | hints: false, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /doc/agh-arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/7cf1600f1b2fe487a0034b03a5ff31bd7f0efb9f/doc/agh-arch.png -------------------------------------------------------------------------------- /doc/agh-filtering.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/7cf1600f1b2fe487a0034b03a5ff31bd7f0efb9f/doc/agh-filtering.png -------------------------------------------------------------------------------- /internal/aghhttp/header.go: -------------------------------------------------------------------------------- 1 | package aghhttp 2 | 3 | // HTTP headers 4 | 5 | // HTTP header value constants. 6 | const ( 7 | HdrValApplicationJSON = "application/json" 8 | HdrValStrictTransportSecurity = "max-age=31536000; includeSubDomains" 9 | HdrValTextPlain = "text/plain" 10 | ) 11 | -------------------------------------------------------------------------------- /internal/aghnet/addr.go: -------------------------------------------------------------------------------- 1 | package aghnet 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // NormalizeDomain returns a lowercased version of host without the final dot, 8 | // unless host is ".", in which case it returns it unchanged. That is a special 9 | // case that to allow matching queries like: 10 | // 11 | // dig IN NS '.' 12 | func NormalizeDomain(host string) (norm string) { 13 | if host == "." { 14 | return host 15 | } 16 | 17 | return strings.ToLower(strings.TrimSuffix(host, ".")) 18 | } 19 | -------------------------------------------------------------------------------- /internal/aghnet/dhcp.go: -------------------------------------------------------------------------------- 1 | package aghnet 2 | 3 | // CheckOtherDHCP tries to discover another DHCP server in the network. 4 | func CheckOtherDHCP(ifaceName string) (ok4, ok6 bool, err4, err6 error) { 5 | return checkOtherDHCP(ifaceName) 6 | } 7 | -------------------------------------------------------------------------------- /internal/aghnet/dhcp_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package aghnet 4 | 5 | import "github.com/AdguardTeam/AdGuardHome/internal/aghos" 6 | 7 | func checkOtherDHCP(ifaceName string) (ok4, ok6 bool, err4, err6 error) { 8 | return false, 9 | false, 10 | aghos.Unsupported("CheckIfOtherDHCPServersPresentV4"), 11 | aghos.Unsupported("CheckIfOtherDHCPServersPresentV6") 12 | } 13 | -------------------------------------------------------------------------------- /internal/aghnet/hostgen.go: -------------------------------------------------------------------------------- 1 | package aghnet 2 | 3 | import ( 4 | "net/netip" 5 | "strings" 6 | ) 7 | 8 | // GenerateHostname generates the hostname from ip. In case of using IPv4 the 9 | // result should be like: 10 | // 11 | // 192-168-10-1 12 | // 13 | // In case of using IPv6, the result is like: 14 | // 15 | // ff80-f076-0000-0000-0000-0000-0000-0010 16 | // 17 | // ip must be either an IPv4 or an IPv6. 18 | func GenerateHostname(ip netip.Addr) (hostname string) { 19 | if !ip.IsValid() { 20 | // TODO(s.chzhen): Get rid of it. 21 | panic("aghnet generate hostname: invalid ip") 22 | } 23 | 24 | ip = ip.Unmap() 25 | hostname = ip.StringExpanded() 26 | 27 | if ip.Is4() { 28 | return strings.ReplaceAll(hostname, ".", "-") 29 | } 30 | 31 | return strings.ReplaceAll(hostname, ":", "-") 32 | } 33 | -------------------------------------------------------------------------------- /internal/aghnet/hostgen_test.go: -------------------------------------------------------------------------------- 1 | package aghnet_test 2 | 3 | import ( 4 | "net/netip" 5 | "testing" 6 | 7 | "github.com/AdguardTeam/AdGuardHome/internal/aghnet" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestGenerateHostName(t *testing.T) { 12 | t.Run("valid", func(t *testing.T) { 13 | testCases := []struct { 14 | name string 15 | want string 16 | ip netip.Addr 17 | }{{ 18 | name: "good_ipv4", 19 | want: "127-0-0-1", 20 | ip: netip.MustParseAddr("127.0.0.1"), 21 | }, { 22 | name: "good_ipv6", 23 | want: "fe00-0000-0000-0000-0000-0000-0000-0001", 24 | ip: netip.MustParseAddr("fe00::1"), 25 | }, { 26 | name: "4to6", 27 | want: "1-2-3-4", 28 | ip: netip.MustParseAddr("::ffff:1.2.3.4"), 29 | }} 30 | 31 | for _, tc := range testCases { 32 | t.Run(tc.name, func(t *testing.T) { 33 | hostname := aghnet.GenerateHostname(tc.ip) 34 | assert.Equal(t, tc.want, hostname) 35 | }) 36 | } 37 | }) 38 | 39 | t.Run("invalid", func(t *testing.T) { 40 | assert.Panics(t, func() { aghnet.GenerateHostname(netip.Addr{}) }) 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /internal/aghnet/ignore_test.go: -------------------------------------------------------------------------------- 1 | package aghnet_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/AdguardTeam/AdGuardHome/internal/aghnet" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestIgnoreEngine_Has(t *testing.T) { 11 | hostnames := []string{ 12 | "*.example.com", 13 | "example.com", 14 | "|.^", 15 | } 16 | 17 | engine, err := aghnet.NewIgnoreEngine(hostnames) 18 | require.NotNil(t, engine) 19 | require.NoError(t, err) 20 | 21 | testCases := []struct { 22 | name string 23 | host string 24 | ignore bool 25 | }{{ 26 | name: "basic", 27 | host: "example.com", 28 | ignore: true, 29 | }, { 30 | name: "root", 31 | host: ".", 32 | ignore: true, 33 | }, { 34 | name: "wildcard", 35 | host: "www.example.com", 36 | ignore: true, 37 | }, { 38 | name: "not_ignored", 39 | host: "something.com", 40 | ignore: false, 41 | }} 42 | 43 | for _, tc := range testCases { 44 | require.Equal(t, tc.ignore, engine.Has(tc.host)) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /internal/aghnet/interfaces_bsd.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || freebsd || openbsd 2 | 3 | package aghnet 4 | 5 | import ( 6 | "context" 7 | "net" 8 | "os" 9 | "syscall" 10 | 11 | "github.com/AdguardTeam/golibs/errors" 12 | "golang.org/x/sys/unix" 13 | ) 14 | 15 | // reuseAddrCtrl is the function to be set to net.ListenConfig.Control. It 16 | // configures the socket to have a reusable port binding. 17 | func reuseAddrCtrl(_, _ string, c syscall.RawConn) (err error) { 18 | cerr := c.Control(func(fd uintptr) { 19 | // TODO(e.burkov): Consider using SO_REUSEPORT. 20 | err = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1) 21 | if err != nil { 22 | err = os.NewSyscallError("setsockopt", err) 23 | } 24 | }) 25 | 26 | err = errors.Join(err, cerr) 27 | 28 | return errors.Annotate(err, "setting control options: %w") 29 | } 30 | 31 | // listenPacketReusable announces on the local network address additionally 32 | // configuring the socket to have a reusable binding. 33 | func listenPacketReusable(_, network, address string) (c net.PacketConn, err error) { 34 | var lc net.ListenConfig 35 | lc.Control = reuseAddrCtrl 36 | 37 | return lc.ListenPacket(context.Background(), network, address) 38 | } 39 | -------------------------------------------------------------------------------- /internal/aghnet/interfaces_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package aghnet 4 | 5 | import ( 6 | "net" 7 | 8 | "github.com/AdguardTeam/golibs/netutil" 9 | "github.com/insomniacslk/dhcp/dhcpv4/nclient4" 10 | ) 11 | 12 | // listenPacketReusable announces on the local network address additionally 13 | // configuring the socket to have a reusable binding. 14 | func listenPacketReusable(ifaceName, network, address string) (c net.PacketConn, err error) { 15 | var port uint16 16 | _, port, err = netutil.SplitHostPort(address) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | // TODO(e.burkov): Inspect nclient4.NewRawUDPConn and implement here. 22 | return nclient4.NewRawUDPConn(ifaceName, int(port)) 23 | } 24 | -------------------------------------------------------------------------------- /internal/aghnet/ipmut.go: -------------------------------------------------------------------------------- 1 | package aghnet 2 | 3 | import ( 4 | "net" 5 | "sync/atomic" 6 | ) 7 | 8 | // IPMutFunc is the signature of a function which modifies the IP address 9 | // instance. It should be safe for concurrent use. 10 | type IPMutFunc func(ip net.IP) 11 | 12 | // nopIPMutFunc is the IPMutFunc that does nothing. 13 | func nopIPMutFunc(net.IP) {} 14 | 15 | // IPMut is a type-safe wrapper of atomic.Value to store the IPMutFunc. 16 | type IPMut struct { 17 | f atomic.Value 18 | } 19 | 20 | // NewIPMut returns the new properly initialized *IPMut. The m is guaranteed to 21 | // always store non-nil IPMutFunc which is safe to call. 22 | func NewIPMut(f IPMutFunc) (m *IPMut) { 23 | m = &IPMut{ 24 | f: atomic.Value{}, 25 | } 26 | m.Store(f) 27 | 28 | return m 29 | } 30 | 31 | // Store sets the IPMutFunc to return from Func. It's safe for concurrent use. 32 | // If f is nil, the stored function is the no-op one. 33 | func (m *IPMut) Store(f IPMutFunc) { 34 | if f == nil { 35 | f = nopIPMutFunc 36 | } 37 | m.f.Store(f) 38 | } 39 | 40 | // Load returns the previously stored IPMutFunc. 41 | func (m *IPMut) Load() (f IPMutFunc) { 42 | return m.f.Load().(IPMutFunc) 43 | } 44 | -------------------------------------------------------------------------------- /internal/aghnet/ipmut_test.go: -------------------------------------------------------------------------------- 1 | package aghnet_test 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | 7 | "github.com/AdguardTeam/AdGuardHome/internal/aghnet" 8 | "github.com/AdguardTeam/golibs/netutil" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestIPMut(t *testing.T) { 13 | testIPs := []net.IP{{ 14 | 127, 0, 0, 1, 15 | }, { 16 | 192, 168, 0, 1, 17 | }, { 18 | 8, 8, 8, 8, 19 | }} 20 | 21 | t.Run("nil_no_mut", func(t *testing.T) { 22 | ipmut := aghnet.NewIPMut(nil) 23 | 24 | ips := netutil.CloneIPs(testIPs) 25 | for i := range ips { 26 | ipmut.Load()(ips[i]) 27 | assert.True(t, ips[i].Equal(testIPs[i])) 28 | } 29 | }) 30 | 31 | t.Run("not_nil_mut", func(t *testing.T) { 32 | ipmut := aghnet.NewIPMut(func(ip net.IP) { 33 | for i := range ip { 34 | ip[i] = 0 35 | } 36 | }) 37 | want := netutil.IPv4Zero() 38 | 39 | ips := netutil.CloneIPs(testIPs) 40 | for i := range ips { 41 | ipmut.Load()(ips[i]) 42 | assert.True(t, ips[i].Equal(want)) 43 | } 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /internal/aghnet/net_bsd.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || freebsd || openbsd 2 | 3 | package aghnet 4 | 5 | import "github.com/AdguardTeam/AdGuardHome/internal/aghos" 6 | 7 | func canBindPrivilegedPorts() (can bool, err error) { 8 | return aghos.HaveAdminRights() 9 | } 10 | -------------------------------------------------------------------------------- /internal/aghnet/net_openbsd.go: -------------------------------------------------------------------------------- 1 | //go:build openbsd 2 | 3 | package aghnet 4 | 5 | import ( 6 | "bufio" 7 | "fmt" 8 | "io" 9 | "strings" 10 | 11 | "github.com/AdguardTeam/AdGuardHome/internal/aghos" 12 | "github.com/AdguardTeam/golibs/netutil" 13 | ) 14 | 15 | func ifaceHasStaticIP(ifaceName string) (ok bool, err error) { 16 | filename := fmt.Sprintf("etc/hostname.%s", ifaceName) 17 | 18 | return aghos.FileWalker(hostnameIfStaticConfig).Walk(rootDirFS, filename) 19 | } 20 | 21 | // hostnameIfStaticConfig checks if the interface is configured by 22 | // /etc/hostname.* to have a static IP. 23 | func hostnameIfStaticConfig(r io.Reader) (_ []string, ok bool, err error) { 24 | s := bufio.NewScanner(r) 25 | for s.Scan() { 26 | line := strings.TrimSpace(s.Text()) 27 | fields := strings.Fields(line) 28 | switch { 29 | case 30 | len(fields) < 2, 31 | fields[0] != "inet", 32 | !netutil.IsValidIPString(fields[1]): 33 | continue 34 | default: 35 | return nil, false, s.Err() 36 | } 37 | } 38 | 39 | return nil, true, s.Err() 40 | } 41 | 42 | func ifaceSetStaticIP(string) (err error) { 43 | return aghos.Unsupported("setting static ip") 44 | } 45 | -------------------------------------------------------------------------------- /internal/aghnet/net_unix.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || freebsd || linux || openbsd 2 | 3 | package aghnet 4 | 5 | import ( 6 | "io" 7 | "syscall" 8 | 9 | "github.com/AdguardTeam/golibs/errors" 10 | ) 11 | 12 | // closePortChecker closes c. c must be non-nil. 13 | func closePortChecker(c io.Closer) (err error) { 14 | return c.Close() 15 | } 16 | 17 | func isAddrInUse(err syscall.Errno) (ok bool) { 18 | return errors.Is(err, syscall.EADDRINUSE) 19 | } 20 | -------------------------------------------------------------------------------- /internal/aghnet/net_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package aghnet 4 | 5 | import ( 6 | "io" 7 | "syscall" 8 | "time" 9 | 10 | "github.com/AdguardTeam/AdGuardHome/internal/aghos" 11 | "github.com/AdguardTeam/golibs/errors" 12 | "golang.org/x/sys/windows" 13 | ) 14 | 15 | func canBindPrivilegedPorts() (can bool, err error) { 16 | return true, nil 17 | } 18 | 19 | func ifaceHasStaticIP(string) (ok bool, err error) { 20 | return false, aghos.Unsupported("checking static ip") 21 | } 22 | 23 | func ifaceSetStaticIP(string) (err error) { 24 | return aghos.Unsupported("setting static ip") 25 | } 26 | 27 | // closePortChecker closes c. c must be non-nil. 28 | func closePortChecker(c io.Closer) (err error) { 29 | if err = c.Close(); err != nil { 30 | return err 31 | } 32 | 33 | // It seems that net.Listener.Close() doesn't close file descriptors right 34 | // away. We wait for some time and hope that this fd will be closed. 35 | // 36 | // TODO(e.burkov): Investigate the purpose of the line and perhaps use more 37 | // reliable approach. 38 | time.Sleep(100 * time.Millisecond) 39 | 40 | return nil 41 | } 42 | 43 | func isAddrInUse(err syscall.Errno) (ok bool) { 44 | return errors.Is(err, windows.WSAEADDRINUSE) 45 | } 46 | -------------------------------------------------------------------------------- /internal/aghnet/upstream.go: -------------------------------------------------------------------------------- 1 | package aghnet 2 | 3 | import "github.com/AdguardTeam/dnsproxy/upstream" 4 | 5 | // UpstreamHTTPVersions returns the HTTP versions for upstream configuration 6 | // depending on configuration. 7 | func UpstreamHTTPVersions(http3 bool) (v []upstream.HTTPVersion) { 8 | if !http3 { 9 | return upstream.DefaultHTTPVersions 10 | } 11 | 12 | return []upstream.HTTPVersion{ 13 | upstream.HTTPVersion3, 14 | upstream.HTTPVersion2, 15 | upstream.HTTPVersion11, 16 | } 17 | } 18 | 19 | // IsCommentOrEmpty returns true if s starts with a "#" character or is empty. 20 | // This function is useful for filtering out non-upstream lines from upstream 21 | // configs. 22 | func IsCommentOrEmpty(s string) (ok bool) { 23 | return len(s) == 0 || s[0] == '#' 24 | } 25 | -------------------------------------------------------------------------------- /internal/aghnet/upstream_test.go: -------------------------------------------------------------------------------- 1 | package aghnet_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/AdguardTeam/AdGuardHome/internal/aghnet" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestIsCommentOrEmpty(t *testing.T) { 11 | for _, tc := range []struct { 12 | want assert.BoolAssertionFunc 13 | str string 14 | }{{ 15 | want: assert.True, 16 | str: "", 17 | }, { 18 | want: assert.True, 19 | str: "# comment", 20 | }, { 21 | want: assert.False, 22 | str: "1.2.3.4", 23 | }} { 24 | tc.want(t, aghnet.IsCommentOrEmpty(tc.str)) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /internal/aghos/aghos_test.go: -------------------------------------------------------------------------------- 1 | package aghos_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/AdguardTeam/golibs/testutil" 7 | ) 8 | 9 | func TestMain(m *testing.M) { 10 | testutil.DiscardLogOutput(m) 11 | } 12 | -------------------------------------------------------------------------------- /internal/aghos/os_bsd.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || openbsd 2 | 3 | package aghos 4 | 5 | import ( 6 | "os" 7 | "syscall" 8 | ) 9 | 10 | func setRlimit(val uint64) (err error) { 11 | var rlim syscall.Rlimit 12 | rlim.Max = val 13 | rlim.Cur = val 14 | 15 | return syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rlim) 16 | } 17 | 18 | func haveAdminRights() (bool, error) { 19 | return os.Getuid() == 0, nil 20 | } 21 | 22 | func isOpenWrt() (ok bool) { 23 | return false 24 | } 25 | -------------------------------------------------------------------------------- /internal/aghos/os_freebsd.go: -------------------------------------------------------------------------------- 1 | //go:build freebsd 2 | 3 | package aghos 4 | 5 | import ( 6 | "os" 7 | "syscall" 8 | ) 9 | 10 | func setRlimit(val uint64) (err error) { 11 | var rlim syscall.Rlimit 12 | rlim.Max = int64(val) 13 | rlim.Cur = int64(val) 14 | 15 | return syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rlim) 16 | } 17 | 18 | func haveAdminRights() (bool, error) { 19 | return os.Getuid() == 0, nil 20 | } 21 | 22 | func isOpenWrt() (ok bool) { 23 | return false 24 | } 25 | -------------------------------------------------------------------------------- /internal/aghos/os_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package aghos 4 | 5 | import ( 6 | "io" 7 | "os" 8 | "syscall" 9 | 10 | "github.com/AdguardTeam/golibs/osutil" 11 | "github.com/AdguardTeam/golibs/stringutil" 12 | ) 13 | 14 | func setRlimit(val uint64) (err error) { 15 | var rlim syscall.Rlimit 16 | rlim.Max = val 17 | rlim.Cur = val 18 | 19 | return syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rlim) 20 | } 21 | 22 | func haveAdminRights() (bool, error) { 23 | // The error is nil because the platform-independent function signature 24 | // requires returning an error. 25 | return os.Getuid() == 0, nil 26 | } 27 | 28 | func isOpenWrt() (ok bool) { 29 | const etcReleasePattern = "etc/*release*" 30 | 31 | var err error 32 | ok, err = FileWalker(func(r io.Reader) (_ []string, cont bool, err error) { 33 | const osNameData = "openwrt" 34 | 35 | // This use of ReadAll is now safe, because FileWalker's Walk() 36 | // have limited r. 37 | var data []byte 38 | data, err = io.ReadAll(r) 39 | if err != nil { 40 | return nil, false, err 41 | } 42 | 43 | return nil, !stringutil.ContainsFold(string(data), osNameData), nil 44 | }).Walk(osutil.RootDirFS(), etcReleasePattern) 45 | 46 | return err == nil && ok 47 | } 48 | -------------------------------------------------------------------------------- /internal/aghos/os_unix.go: -------------------------------------------------------------------------------- 1 | //go:build unix 2 | 3 | package aghos 4 | 5 | import ( 6 | "os" 7 | ) 8 | 9 | func sendShutdownSignal(_ chan<- os.Signal) { 10 | // On Unix we are already notified by the system. 11 | } 12 | -------------------------------------------------------------------------------- /internal/aghos/os_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package aghos 4 | 5 | import ( 6 | "os" 7 | 8 | "golang.org/x/sys/windows" 9 | ) 10 | 11 | func setRlimit(_ uint64) (err error) { 12 | return Unsupported("setrlimit") 13 | } 14 | 15 | func haveAdminRights() (bool, error) { 16 | var token windows.Token 17 | h := windows.CurrentProcess() 18 | err := windows.OpenProcessToken(h, windows.TOKEN_QUERY, &token) 19 | if err != nil { 20 | return false, err 21 | } 22 | 23 | info := make([]byte, 4) 24 | var returnedLen uint32 25 | err = windows.GetTokenInformation(token, windows.TokenElevation, &info[0], uint32(len(info)), &returnedLen) 26 | token.Close() 27 | if err != nil { 28 | return false, err 29 | } 30 | if info[0] == 0 { 31 | return false, nil 32 | } 33 | return true, nil 34 | } 35 | 36 | func isOpenWrt() (ok bool) { 37 | return false 38 | } 39 | 40 | func sendShutdownSignal(c chan<- os.Signal) { 41 | c <- os.Interrupt 42 | } 43 | -------------------------------------------------------------------------------- /internal/aghos/service.go: -------------------------------------------------------------------------------- 1 | package aghos 2 | 3 | // PreCheckActionStart performs the service start action pre-check. 4 | func PreCheckActionStart() (err error) { 5 | return preCheckActionStart() 6 | } 7 | -------------------------------------------------------------------------------- /internal/aghos/service_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | 3 | package aghos 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/AdguardTeam/golibs/log" 12 | ) 13 | 14 | // preCheckActionStart performs the service start action pre-check. It warns 15 | // user that the service should be installed into Applications directory. 16 | func preCheckActionStart() (err error) { 17 | exe, err := os.Executable() 18 | if err != nil { 19 | return fmt.Errorf("getting executable path: %v", err) 20 | } 21 | 22 | exe, err = filepath.EvalSymlinks(exe) 23 | if err != nil { 24 | return fmt.Errorf("evaluating executable symlinks: %v", err) 25 | } 26 | 27 | if !strings.HasPrefix(exe, "/Applications/") { 28 | log.Info("warning: service must be started from within the /Applications directory") 29 | } 30 | 31 | return err 32 | } 33 | -------------------------------------------------------------------------------- /internal/aghos/service_others.go: -------------------------------------------------------------------------------- 1 | //go:build !darwin 2 | 3 | package aghos 4 | 5 | // preCheckActionStart performs the service start action pre-check. 6 | func preCheckActionStart() (err error) { 7 | return nil 8 | } 9 | -------------------------------------------------------------------------------- /internal/aghos/syslog.go: -------------------------------------------------------------------------------- 1 | package aghos 2 | 3 | // ConfigureSyslog reroutes standard logger output to syslog. 4 | func ConfigureSyslog(serviceName string) (err error) { 5 | return configureSyslog(serviceName) 6 | } 7 | -------------------------------------------------------------------------------- /internal/aghos/syslog_others.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package aghos 4 | 5 | import ( 6 | "log/syslog" 7 | 8 | "github.com/AdguardTeam/golibs/log" 9 | ) 10 | 11 | // configureSyslog sets standard log output to syslog. 12 | func configureSyslog(serviceName string) (err error) { 13 | w, err := syslog.New(syslog.LOG_NOTICE|syslog.LOG_USER, serviceName) 14 | if err != nil { 15 | // Don't wrap the error, because it's informative enough as is. 16 | return err 17 | } 18 | 19 | log.SetOutput(w) 20 | 21 | return nil 22 | } 23 | -------------------------------------------------------------------------------- /internal/aghos/user.go: -------------------------------------------------------------------------------- 1 | package aghos 2 | 3 | // SetGroup sets the effective group ID of the calling process. 4 | func SetGroup(groupName string) (err error) { 5 | return setGroup(groupName) 6 | } 7 | 8 | // SetUser sets the effective user ID of the calling process. 9 | func SetUser(userName string) (err error) { 10 | return setUser(userName) 11 | } 12 | -------------------------------------------------------------------------------- /internal/aghos/user_unix.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || freebsd || linux || openbsd 2 | 3 | package aghos 4 | 5 | import ( 6 | "fmt" 7 | "os/user" 8 | "strconv" 9 | "syscall" 10 | ) 11 | 12 | func setGroup(groupName string) (err error) { 13 | g, err := user.LookupGroup(groupName) 14 | if err != nil { 15 | return fmt.Errorf("looking up group: %w", err) 16 | } 17 | 18 | gid, err := strconv.Atoi(g.Gid) 19 | if err != nil { 20 | return fmt.Errorf("parsing gid: %w", err) 21 | } 22 | 23 | err = syscall.Setgid(gid) 24 | if err != nil { 25 | return fmt.Errorf("setting gid: %w", err) 26 | } 27 | 28 | return nil 29 | } 30 | 31 | func setUser(userName string) (err error) { 32 | u, err := user.Lookup(userName) 33 | if err != nil { 34 | return fmt.Errorf("looking up user: %w", err) 35 | } 36 | 37 | uid, err := strconv.Atoi(u.Uid) 38 | if err != nil { 39 | return fmt.Errorf("parsing uid: %w", err) 40 | } 41 | 42 | err = syscall.Setuid(uid) 43 | if err != nil { 44 | return fmt.Errorf("setting uid: %w", err) 45 | } 46 | 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /internal/aghos/user_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package aghos 4 | 5 | // TODO(a.garipov): Think of a way to implement these. Perhaps by using 6 | // syscall.CreateProcessAsUser or something from the golang.org/x/sys module. 7 | 8 | func setGroup(_ string) (err error) { 9 | return Unsupported("setgid") 10 | } 11 | 12 | func setUser(_ string) (err error) { 13 | return Unsupported("setuid") 14 | } 15 | -------------------------------------------------------------------------------- /internal/aghrenameio/renameio_unix.go: -------------------------------------------------------------------------------- 1 | //go:build unix 2 | 3 | package aghrenameio 4 | 5 | import ( 6 | "io/fs" 7 | 8 | "github.com/google/renameio/v2" 9 | ) 10 | 11 | // pendingFile is a wrapper around [*renameio.PendingFile] making it an 12 | // [io.WriteCloser]. 13 | type pendingFile struct { 14 | file *renameio.PendingFile 15 | } 16 | 17 | // type check 18 | var _ PendingFile = pendingFile{} 19 | 20 | // Cleanup implements the [PendingFile] interface for pendingFile. 21 | func (f pendingFile) Cleanup() (err error) { 22 | return f.file.Cleanup() 23 | } 24 | 25 | // CloseReplace implements the [PendingFile] interface for pendingFile. 26 | func (f pendingFile) CloseReplace() (err error) { 27 | return f.file.CloseAtomicallyReplace() 28 | } 29 | 30 | // Write implements the [PendingFile] interface for pendingFile. 31 | func (f pendingFile) Write(b []byte) (n int, err error) { 32 | return f.file.Write(b) 33 | } 34 | 35 | // NewPendingFile is a wrapper around [renameio.NewPendingFile]. 36 | // 37 | // f.Close must be called to finish the renaming. 38 | func newPendingFile(filePath string, mode fs.FileMode) (f PendingFile, err error) { 39 | file, err := renameio.NewPendingFile(filePath, renameio.WithPermissions(mode)) 40 | if err != nil { 41 | // Don't wrap the error since it's informative enough as is. 42 | return nil, err 43 | } 44 | 45 | return pendingFile{ 46 | file: file, 47 | }, nil 48 | } 49 | -------------------------------------------------------------------------------- /internal/aghtest/interface_test.go: -------------------------------------------------------------------------------- 1 | package aghtest_test 2 | 3 | import ( 4 | "github.com/AdguardTeam/AdGuardHome/internal/aghtest" 5 | "github.com/AdguardTeam/AdGuardHome/internal/client" 6 | "github.com/AdguardTeam/AdGuardHome/internal/filtering" 7 | ) 8 | 9 | // Put interface checks that cause import cycles here. 10 | 11 | // type check 12 | var _ filtering.Resolver = (*aghtest.Resolver)(nil) 13 | 14 | // type check 15 | // 16 | // TODO(s.chzhen): It's here to avoid the import cycle. Remove it. 17 | var _ client.AddressProcessor = (*aghtest.AddressProcessor)(nil) 18 | 19 | // type check 20 | // 21 | // TODO(s.chzhen): It's here to avoid the import cycle. Remove it. 22 | var _ client.AddressUpdater = (*aghtest.AddressUpdater)(nil) 23 | -------------------------------------------------------------------------------- /internal/aghtls/root.go: -------------------------------------------------------------------------------- 1 | package aghtls 2 | 3 | import ( 4 | "crypto/x509" 5 | ) 6 | 7 | // SystemRootCAs tries to load root certificates from the operating system. It 8 | // returns nil in case nothing is found so that Go' crypto/x509 can use its 9 | // default algorithm to find system root CA list. 10 | // 11 | // See https://github.com/AdguardTeam/AdGuardHome/issues/1311. 12 | func SystemRootCAs() (roots *x509.CertPool) { 13 | return rootCAs() 14 | } 15 | -------------------------------------------------------------------------------- /internal/aghtls/root_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package aghtls 4 | 5 | import ( 6 | "crypto/x509" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/AdguardTeam/golibs/errors" 11 | "github.com/AdguardTeam/golibs/log" 12 | ) 13 | 14 | func rootCAs() (roots *x509.CertPool) { 15 | // Directories with the system root certificates, which aren't supported by 16 | // Go's crypto/x509. 17 | dirs := []string{ 18 | // Entware. 19 | "/opt/etc/ssl/certs", 20 | } 21 | 22 | roots = x509.NewCertPool() 23 | for _, dir := range dirs { 24 | dirEnts, err := os.ReadDir(dir) 25 | if err != nil { 26 | if errors.Is(err, os.ErrNotExist) { 27 | continue 28 | } 29 | 30 | // TODO(a.garipov): Improve error handling here and in other places. 31 | log.Error("aghtls: opening directory %q: %s", dir, err) 32 | } 33 | 34 | var rootsAdded bool 35 | for _, de := range dirEnts { 36 | var certData []byte 37 | rootFile := filepath.Join(dir, de.Name()) 38 | certData, err = os.ReadFile(rootFile) 39 | if err != nil { 40 | log.Error("aghtls: reading root cert: %s", err) 41 | } else { 42 | if roots.AppendCertsFromPEM(certData) { 43 | rootsAdded = true 44 | } else { 45 | log.Error("aghtls: could not add root from %q", rootFile) 46 | } 47 | } 48 | } 49 | 50 | if rootsAdded { 51 | return roots 52 | } 53 | } 54 | 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /internal/aghtls/root_others.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | 3 | package aghtls 4 | 5 | import "crypto/x509" 6 | 7 | func rootCAs() (roots *x509.CertPool) { 8 | return nil 9 | } 10 | -------------------------------------------------------------------------------- /internal/aghuser/aghuser_test.go: -------------------------------------------------------------------------------- 1 | package aghuser_test 2 | 3 | import "time" 4 | 5 | // testTimeout is the common timeout for tests. 6 | const testTimeout = 1 * time.Second 7 | -------------------------------------------------------------------------------- /internal/aghuser/session.go: -------------------------------------------------------------------------------- 1 | package aghuser 2 | 3 | import ( 4 | "crypto/rand" 5 | "time" 6 | ) 7 | 8 | // SessionTokenLength is the length of the web user session token. 9 | const SessionTokenLength = 16 10 | 11 | // SessionToken is the type for the web user session token. 12 | type SessionToken [SessionTokenLength]byte 13 | 14 | // NewSessionToken returns a cryptographically secure randomly generated web 15 | // user session token. If an error occurs during random generation, it will 16 | // cause the program to crash. 17 | func NewSessionToken() (t SessionToken) { 18 | _, _ = rand.Read(t[:]) 19 | 20 | return t 21 | } 22 | 23 | // Session represents a web user session. 24 | type Session struct { 25 | // Expire indicates when the session will expire. 26 | Expire time.Time 27 | 28 | // UserLogin is the login of the web user associated with the session. 29 | // 30 | // TODO(s.chzhen): Remove this field and associate the user by UserID. 31 | UserLogin Login 32 | 33 | // Token is the session token. 34 | Token SessionToken 35 | 36 | // UserID is the identifier of the web user associated with the session. 37 | UserID UserID 38 | } 39 | -------------------------------------------------------------------------------- /internal/aghuser/user.go: -------------------------------------------------------------------------------- 1 | // Package aghuser contains types and logic for dealing with AdGuard Home's web 2 | // users. 3 | package aghuser 4 | 5 | import ( 6 | "fmt" 7 | 8 | "github.com/google/uuid" 9 | ) 10 | 11 | // UserID is the type for the unique IDs of web users. 12 | type UserID uuid.UUID 13 | 14 | // NewUserID returns a new web user unique identifier. Any error returned is an 15 | // error from the cryptographic randomness reader. 16 | func NewUserID() (uid UserID, err error) { 17 | uuidv7, err := uuid.NewV7() 18 | 19 | return UserID(uuidv7), err 20 | } 21 | 22 | // MustNewUserID is a wrapper around [NewUserID] that panics if there is an 23 | // error. It is currently only used in tests. 24 | func MustNewUserID() (uid UserID) { 25 | uid, err := NewUserID() 26 | if err != nil { 27 | panic(fmt.Errorf("unexpected uuidv7 error: %w", err)) 28 | } 29 | 30 | return uid 31 | } 32 | 33 | // User represents a web user. 34 | type User struct { 35 | // Password stores the password information for the web user. It must not 36 | // be nil. 37 | Password Password 38 | 39 | // Login is the login name of the web user. It must not be empty. 40 | Login Login 41 | 42 | // ID is the unique identifier for the web user. It must not be empty. 43 | ID UserID 44 | } 45 | -------------------------------------------------------------------------------- /internal/arpdb/arpdb_bsd_internal_test.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || freebsd 2 | 3 | package arpdb 4 | 5 | import ( 6 | "net" 7 | "net/netip" 8 | ) 9 | 10 | const arpAOutput = ` 11 | invalid.mac (1.2.3.4) at 12:34:56:78:910 on el0 ifscope [ethernet] 12 | invalid.ip (1.2.3.4.5) at ab:cd:ef:ab:cd:12 on ek0 ifscope [ethernet] 13 | invalid.fmt 1 at 12:cd:ef:ab:cd:ef on er0 ifscope [ethernet] 14 | hostname.one (192.168.1.2) at ab:cd:ef:ab:cd:ef on en0 ifscope [ethernet] 15 | hostname.two (::ffff:ffff) at ef:cd:ab:ef:cd:ab on em0 expires in 1198 seconds [ethernet] 16 | ? (::1234) at aa:bb:cc:dd:ee:ff on ej0 expires in 1918 seconds [ethernet] 17 | ` 18 | 19 | var wantNeighs = []Neighbor{{ 20 | Name: "hostname.one", 21 | IP: netip.MustParseAddr("192.168.1.2"), 22 | MAC: net.HardwareAddr{0xAB, 0xCD, 0xEF, 0xAB, 0xCD, 0xEF}, 23 | }, { 24 | Name: "hostname.two", 25 | IP: netip.MustParseAddr("::ffff:ffff"), 26 | MAC: net.HardwareAddr{0xEF, 0xCD, 0xAB, 0xEF, 0xCD, 0xAB}, 27 | }, { 28 | Name: "", 29 | IP: netip.MustParseAddr("::1234"), 30 | MAC: net.HardwareAddr{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}, 31 | }} 32 | -------------------------------------------------------------------------------- /internal/arpdb/arpdb_openbsd_internal_test.go: -------------------------------------------------------------------------------- 1 | //go:build openbsd 2 | 3 | package arpdb 4 | 5 | import ( 6 | "net" 7 | "net/netip" 8 | ) 9 | 10 | const arpAOutput = ` 11 | Host Ethernet Address Netif Expire Flags 12 | 1.2.3.4.5 aa:bb:cc:dd:ee:ff em0 permanent 13 | 1.2.3.4 12:34:56:78:910 em0 permanent 14 | 192.168.1.2 ab:cd:ef:ab:cd:ef em0 19m56s 15 | ::ffff:ffff ef:cd:ab:ef:cd:ab em0 permanent l 16 | ` 17 | 18 | var wantNeighs = []Neighbor{{ 19 | IP: netip.MustParseAddr("192.168.1.2"), 20 | MAC: net.HardwareAddr{0xAB, 0xCD, 0xEF, 0xAB, 0xCD, 0xEF}, 21 | }, { 22 | IP: netip.MustParseAddr("::ffff:ffff"), 23 | MAC: net.HardwareAddr{0xEF, 0xCD, 0xAB, 0xEF, 0xCD, 0xAB}, 24 | }} 25 | -------------------------------------------------------------------------------- /internal/arpdb/arpdb_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package arpdb 4 | 5 | import ( 6 | "bufio" 7 | "log/slog" 8 | "strings" 9 | "sync" 10 | 11 | "github.com/AdguardTeam/golibs/logutil/slogutil" 12 | ) 13 | 14 | func newARPDB(logger *slog.Logger) (arp *cmdARPDB) { 15 | return &cmdARPDB{ 16 | logger: logger, 17 | parse: parseArpA, 18 | ns: &neighs{ 19 | mu: &sync.RWMutex{}, 20 | ns: make([]Neighbor, 0), 21 | }, 22 | cmd: "arp", 23 | args: []string{"/a"}, 24 | } 25 | } 26 | 27 | // parseArpA parses the output of the "arp /a" command on Windows. The expected 28 | // input format (the first line is empty): 29 | // 30 | // Interface: 192.168.56.16 --- 0x7 31 | // Internet Address Physical Address Type 32 | // 192.168.56.1 0a-00-27-00-00-00 dynamic 33 | // 192.168.56.255 ff-ff-ff-ff-ff-ff static 34 | func parseArpA(logger *slog.Logger, sc *bufio.Scanner, lenHint int) (ns []Neighbor) { 35 | ns = make([]Neighbor, 0, lenHint) 36 | for sc.Scan() { 37 | ln := sc.Text() 38 | if ln == "" { 39 | continue 40 | } 41 | 42 | fields := strings.Fields(ln) 43 | if len(fields) != 3 { 44 | continue 45 | } 46 | 47 | n, err := newNeighbor("", fields[0], fields[1]) 48 | if err != nil { 49 | logger.Debug("parsing arp output", "line", ln, slogutil.KeyError, err) 50 | 51 | continue 52 | } 53 | 54 | ns = append(ns, *n) 55 | } 56 | 57 | return ns 58 | } 59 | -------------------------------------------------------------------------------- /internal/arpdb/arpdb_windows_internal_test.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package arpdb 4 | 5 | import ( 6 | "net" 7 | "net/netip" 8 | ) 9 | 10 | const arpAOutput = ` 11 | 12 | Interface: 192.168.1.1 --- 0x7 13 | Internet Address Physical Address Type 14 | 192.168.1.2 ab-cd-ef-ab-cd-ef dynamic 15 | ::ffff:ffff ef-cd-ab-ef-cd-ab static` 16 | 17 | var wantNeighs = []Neighbor{{ 18 | IP: netip.MustParseAddr("192.168.1.2"), 19 | MAC: net.HardwareAddr{0xAB, 0xCD, 0xEF, 0xAB, 0xCD, 0xEF}, 20 | }, { 21 | IP: netip.MustParseAddr("::ffff:ffff"), 22 | MAC: net.HardwareAddr{0xEF, 0xCD, 0xAB, 0xEF, 0xCD, 0xAB}, 23 | }} 24 | -------------------------------------------------------------------------------- /internal/arpdb/testdata/proc_net_arp: -------------------------------------------------------------------------------- 1 | IP address HW type Flags HW address Mask Device 2 | 192.168.1.2 0x1 0x2 ab:cd:ef:ab:cd:ef * wan 3 | ::ffff:ffff 0x1 0x0 ef:cd:ab:ef:cd:ab * br-lan 4 | 0.0.0.0 0x0 0x0 00:00:00:00:00:00 * unspec 5 | 1.2.3.4.5 0x1 0x2 aa:bb:cc:dd:ee:ff * wan 6 | 1.2.3.4 0x1 0x2 12:34:56:78:910 * wan 7 | -------------------------------------------------------------------------------- /internal/client/client_test.go: -------------------------------------------------------------------------------- 1 | package client_test 2 | 3 | import ( 4 | "net/netip" 5 | "testing" 6 | "time" 7 | 8 | "github.com/AdguardTeam/golibs/testutil" 9 | ) 10 | 11 | func TestMain(m *testing.M) { 12 | testutil.DiscardLogOutput(m) 13 | } 14 | 15 | // testHost is the common hostname for tests. 16 | const testHost = "client.example" 17 | 18 | // testTimeout is the common timeout for tests. 19 | const testTimeout = 1 * time.Second 20 | 21 | // testWHOISCity is the common city for tests. 22 | const testWHOISCity = "Brussels" 23 | 24 | // testIP is the common IP address for tests. 25 | var testIP = netip.MustParseAddr("1.2.3.4") 26 | -------------------------------------------------------------------------------- /internal/configmigrate/configmigrate.go: -------------------------------------------------------------------------------- 1 | // Package configmigrate provides a way to upgrade the YAML configuration file. 2 | package configmigrate 3 | 4 | // LastSchemaVersion is the most recent schema version. 5 | const LastSchemaVersion uint = 29 6 | -------------------------------------------------------------------------------- /internal/configmigrate/testdata/TestMigrateConfig_Migrate/v1/input.yml: -------------------------------------------------------------------------------- 1 | bind_host: 127.0.0.1 2 | bind_port: 3000 3 | auth_name: testuser 4 | auth_pass: testpassword 5 | coredns: 6 | port: 53 7 | protection_enabled: true 8 | filtering_enabled: true 9 | safebrowsing_enabled: false 10 | safesearch_enabled: false 11 | parental_enabled: false 12 | parental_sensitivity: 0 13 | blocked_response_ttl: 10 14 | querylog_enabled: true 15 | upstream_dns: 16 | - tls://1.1.1.1 17 | - tls://1.0.0.1 18 | filters: 19 | - url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt 20 | name: "" 21 | enabled: true 22 | - url: https://adaway.org/hosts.txt 23 | name: AdAway 24 | enabled: false 25 | - url: https://hosts-file.net/ad_servers.txt 26 | name: hpHosts - Ad and Tracking servers only 27 | enabled: false 28 | - url: http://www.malwaredomainlist.com/hostslist/hosts.txt 29 | name: MalwareDomainList.com Hosts List 30 | enabled: false 31 | user_rules: [] 32 | -------------------------------------------------------------------------------- /internal/configmigrate/testdata/TestMigrateConfig_Migrate/v1/output.yml: -------------------------------------------------------------------------------- 1 | bind_host: 127.0.0.1 2 | bind_port: 3000 3 | auth_name: testuser 4 | auth_pass: testpassword 5 | coredns: 6 | port: 53 7 | protection_enabled: true 8 | filtering_enabled: true 9 | safebrowsing_enabled: false 10 | safesearch_enabled: false 11 | parental_enabled: false 12 | parental_sensitivity: 0 13 | blocked_response_ttl: 10 14 | querylog_enabled: true 15 | upstream_dns: 16 | - tls://1.1.1.1 17 | - tls://1.0.0.1 18 | filters: 19 | - url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt 20 | name: "" 21 | enabled: true 22 | - url: https://adaway.org/hosts.txt 23 | name: AdAway 24 | enabled: false 25 | - url: https://hosts-file.net/ad_servers.txt 26 | name: hpHosts - Ad and Tracking servers only 27 | enabled: false 28 | - url: http://www.malwaredomainlist.com/hostslist/hosts.txt 29 | name: MalwareDomainList.com Hosts List 30 | enabled: false 31 | schema_version: 1 32 | user_rules: [] 33 | -------------------------------------------------------------------------------- /internal/configmigrate/testdata/TestMigrateConfig_Migrate/v2/input.yml: -------------------------------------------------------------------------------- 1 | bind_host: 127.0.0.1 2 | bind_port: 3000 3 | auth_name: testuser 4 | auth_pass: testpassword 5 | coredns: 6 | bind_host: 127.0.0.1 7 | port: 53 8 | protection_enabled: true 9 | filtering_enabled: true 10 | safebrowsing_enabled: false 11 | safesearch_enabled: false 12 | parental_enabled: false 13 | parental_sensitivity: 0 14 | blocked_response_ttl: 10 15 | querylog_enabled: true 16 | upstream_dns: 17 | - tls://1.1.1.1 18 | - tls://1.0.0.1 19 | bootstrap_dns: 8.8.8.8:53 20 | filters: 21 | - url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt 22 | name: "" 23 | enabled: true 24 | - url: https://adaway.org/hosts.txt 25 | name: AdAway 26 | enabled: false 27 | - url: https://hosts-file.net/ad_servers.txt 28 | name: hpHosts - Ad and Tracking servers only 29 | enabled: false 30 | - url: http://www.malwaredomainlist.com/hostslist/hosts.txt 31 | name: MalwareDomainList.com Hosts List 32 | enabled: false 33 | schema_version: 1 34 | user_rules: [] 35 | -------------------------------------------------------------------------------- /internal/configmigrate/testdata/TestMigrateConfig_Migrate/v2/output.yml: -------------------------------------------------------------------------------- 1 | bind_host: 127.0.0.1 2 | bind_port: 3000 3 | auth_name: testuser 4 | auth_pass: testpassword 5 | dns: 6 | bind_host: 127.0.0.1 7 | port: 53 8 | protection_enabled: true 9 | filtering_enabled: true 10 | safebrowsing_enabled: false 11 | safesearch_enabled: false 12 | parental_enabled: false 13 | parental_sensitivity: 0 14 | blocked_response_ttl: 10 15 | querylog_enabled: true 16 | upstream_dns: 17 | - tls://1.1.1.1 18 | - tls://1.0.0.1 19 | bootstrap_dns: 8.8.8.8:53 20 | filters: 21 | - url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt 22 | name: "" 23 | enabled: true 24 | - url: https://adaway.org/hosts.txt 25 | name: AdAway 26 | enabled: false 27 | - url: https://hosts-file.net/ad_servers.txt 28 | name: hpHosts - Ad and Tracking servers only 29 | enabled: false 30 | - url: http://www.malwaredomainlist.com/hostslist/hosts.txt 31 | name: MalwareDomainList.com Hosts List 32 | enabled: false 33 | schema_version: 2 34 | user_rules: [] 35 | -------------------------------------------------------------------------------- /internal/configmigrate/testdata/TestMigrateConfig_Migrate/v3/input.yml: -------------------------------------------------------------------------------- 1 | bind_host: 127.0.0.1 2 | bind_port: 3000 3 | auth_name: testuser 4 | auth_pass: testpassword 5 | dns: 6 | port: 53 7 | protection_enabled: true 8 | filtering_enabled: true 9 | safebrowsing_enabled: false 10 | safesearch_enabled: false 11 | parental_enabled: false 12 | parental_sensitivity: 0 13 | blocked_response_ttl: 10 14 | querylog_enabled: true 15 | upstream_dns: 16 | - tls://1.1.1.1 17 | - tls://1.0.0.1 18 | bootstrap_dns: 8.8.8.8:53 19 | filters: 20 | - url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt 21 | name: "" 22 | enabled: true 23 | - url: https://adaway.org/hosts.txt 24 | name: AdAway 25 | enabled: false 26 | - url: https://hosts-file.net/ad_servers.txt 27 | name: hpHosts - Ad and Tracking servers only 28 | enabled: false 29 | - url: http://www.malwaredomainlist.com/hostslist/hosts.txt 30 | name: MalwareDomainList.com Hosts List 31 | enabled: false 32 | schema_version: 2 33 | user_rules: [] 34 | -------------------------------------------------------------------------------- /internal/configmigrate/testdata/TestMigrateConfig_Migrate/v3/output.yml: -------------------------------------------------------------------------------- 1 | bind_host: 127.0.0.1 2 | bind_port: 3000 3 | auth_name: testuser 4 | auth_pass: testpassword 5 | dns: 6 | port: 53 7 | protection_enabled: true 8 | filtering_enabled: true 9 | safebrowsing_enabled: false 10 | safesearch_enabled: false 11 | parental_enabled: false 12 | parental_sensitivity: 0 13 | blocked_response_ttl: 10 14 | querylog_enabled: true 15 | upstream_dns: 16 | - tls://1.1.1.1 17 | - tls://1.0.0.1 18 | bootstrap_dns: 19 | - 8.8.8.8:53 20 | filters: 21 | - url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt 22 | name: "" 23 | enabled: true 24 | - url: https://adaway.org/hosts.txt 25 | name: AdAway 26 | enabled: false 27 | - url: https://hosts-file.net/ad_servers.txt 28 | name: hpHosts - Ad and Tracking servers only 29 | enabled: false 30 | - url: http://www.malwaredomainlist.com/hostslist/hosts.txt 31 | name: MalwareDomainList.com Hosts List 32 | enabled: false 33 | schema_version: 3 34 | user_rules: [] 35 | -------------------------------------------------------------------------------- /internal/configmigrate/testdata/TestMigrateConfig_Migrate/v4/input.yml: -------------------------------------------------------------------------------- 1 | bind_host: 127.0.0.1 2 | bind_port: 3000 3 | auth_name: testuser 4 | auth_pass: testpassword 5 | dns: 6 | port: 53 7 | protection_enabled: true 8 | filtering_enabled: true 9 | safebrowsing_enabled: false 10 | safesearch_enabled: false 11 | parental_enabled: false 12 | parental_sensitivity: 0 13 | blocked_response_ttl: 10 14 | querylog_enabled: true 15 | upstream_dns: 16 | - tls://1.1.1.1 17 | - tls://1.0.0.1 18 | bootstrap_dns: 19 | - 8.8.8.8:53 20 | filters: 21 | - url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt 22 | name: "" 23 | enabled: true 24 | - url: https://adaway.org/hosts.txt 25 | name: AdAway 26 | enabled: false 27 | - url: https://hosts-file.net/ad_servers.txt 28 | name: hpHosts - Ad and Tracking servers only 29 | enabled: false 30 | - url: http://www.malwaredomainlist.com/hostslist/hosts.txt 31 | name: MalwareDomainList.com Hosts List 32 | enabled: false 33 | clients: 34 | - name: localhost 35 | ip: 127.0.0.1 36 | mac: "" 37 | use_global_settings: true 38 | filtering_enabled: false 39 | parental_enabled: false 40 | safebrowsing_enabled: false 41 | safesearch_enabled: false 42 | schema_version: 3 43 | user_rules: [] 44 | -------------------------------------------------------------------------------- /internal/configmigrate/testdata/TestMigrateConfig_Migrate/v4/output.yml: -------------------------------------------------------------------------------- 1 | bind_host: 127.0.0.1 2 | bind_port: 3000 3 | auth_name: testuser 4 | auth_pass: testpassword 5 | dns: 6 | port: 53 7 | protection_enabled: true 8 | filtering_enabled: true 9 | safebrowsing_enabled: false 10 | safesearch_enabled: false 11 | parental_enabled: false 12 | parental_sensitivity: 0 13 | blocked_response_ttl: 10 14 | querylog_enabled: true 15 | upstream_dns: 16 | - tls://1.1.1.1 17 | - tls://1.0.0.1 18 | bootstrap_dns: 19 | - 8.8.8.8:53 20 | filters: 21 | - url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt 22 | name: "" 23 | enabled: true 24 | - url: https://adaway.org/hosts.txt 25 | name: AdAway 26 | enabled: false 27 | - url: https://hosts-file.net/ad_servers.txt 28 | name: hpHosts - Ad and Tracking servers only 29 | enabled: false 30 | - url: http://www.malwaredomainlist.com/hostslist/hosts.txt 31 | name: MalwareDomainList.com Hosts List 32 | enabled: false 33 | clients: 34 | - name: localhost 35 | ip: 127.0.0.1 36 | mac: "" 37 | use_global_settings: true 38 | use_global_blocked_services: true 39 | filtering_enabled: false 40 | parental_enabled: false 41 | safebrowsing_enabled: false 42 | safesearch_enabled: false 43 | schema_version: 4 44 | user_rules: [] 45 | -------------------------------------------------------------------------------- /internal/configmigrate/testdata/TestMigrateConfig_Migrate/v5/input.yml: -------------------------------------------------------------------------------- 1 | bind_host: 127.0.0.1 2 | bind_port: 3000 3 | auth_name: testuser 4 | auth_pass: testpassword 5 | dns: 6 | port: 53 7 | protection_enabled: true 8 | filtering_enabled: true 9 | safebrowsing_enabled: false 10 | safesearch_enabled: false 11 | parental_enabled: false 12 | parental_sensitivity: 0 13 | blocked_response_ttl: 10 14 | querylog_enabled: true 15 | upstream_dns: 16 | - tls://1.1.1.1 17 | - tls://1.0.0.1 18 | bootstrap_dns: 19 | - 8.8.8.8:53 20 | filters: 21 | - url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt 22 | name: "" 23 | enabled: true 24 | - url: https://adaway.org/hosts.txt 25 | name: AdAway 26 | enabled: false 27 | - url: https://hosts-file.net/ad_servers.txt 28 | name: hpHosts - Ad and Tracking servers only 29 | enabled: false 30 | - url: http://www.malwaredomainlist.com/hostslist/hosts.txt 31 | name: MalwareDomainList.com Hosts List 32 | enabled: false 33 | clients: 34 | - name: localhost 35 | ip: 127.0.0.1 36 | mac: "" 37 | use_global_settings: true 38 | use_global_blocked_services: true 39 | filtering_enabled: false 40 | parental_enabled: false 41 | safebrowsing_enabled: false 42 | safesearch_enabled: false 43 | schema_version: 4 44 | user_rules: [] 45 | -------------------------------------------------------------------------------- /internal/configmigrate/testdata/TestMigrateConfig_Migrate/v5/output.yml: -------------------------------------------------------------------------------- 1 | bind_host: 127.0.0.1 2 | bind_port: 3000 3 | users: 4 | - name: testuser 5 | password: testpassword 6 | dns: 7 | port: 53 8 | protection_enabled: true 9 | filtering_enabled: true 10 | safebrowsing_enabled: false 11 | safesearch_enabled: false 12 | parental_enabled: false 13 | parental_sensitivity: 0 14 | blocked_response_ttl: 10 15 | querylog_enabled: true 16 | upstream_dns: 17 | - tls://1.1.1.1 18 | - tls://1.0.0.1 19 | bootstrap_dns: 20 | - 8.8.8.8:53 21 | filters: 22 | - url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt 23 | name: "" 24 | enabled: true 25 | - url: https://adaway.org/hosts.txt 26 | name: AdAway 27 | enabled: false 28 | - url: https://hosts-file.net/ad_servers.txt 29 | name: hpHosts - Ad and Tracking servers only 30 | enabled: false 31 | - url: http://www.malwaredomainlist.com/hostslist/hosts.txt 32 | name: MalwareDomainList.com Hosts List 33 | enabled: false 34 | clients: 35 | - name: localhost 36 | ip: 127.0.0.1 37 | mac: "" 38 | use_global_settings: true 39 | use_global_blocked_services: true 40 | filtering_enabled: false 41 | parental_enabled: false 42 | safebrowsing_enabled: false 43 | safesearch_enabled: false 44 | schema_version: 5 45 | user_rules: [] 46 | -------------------------------------------------------------------------------- /internal/configmigrate/testdata/TestMigrateConfig_Migrate/v6/input.yml: -------------------------------------------------------------------------------- 1 | bind_host: 127.0.0.1 2 | bind_port: 3000 3 | users: 4 | - name: testuser 5 | password: testpassword 6 | dns: 7 | port: 53 8 | protection_enabled: true 9 | filtering_enabled: true 10 | safebrowsing_enabled: false 11 | safesearch_enabled: false 12 | parental_enabled: false 13 | parental_sensitivity: 0 14 | blocked_response_ttl: 10 15 | querylog_enabled: true 16 | upstream_dns: 17 | - tls://1.1.1.1 18 | - tls://1.0.0.1 19 | bootstrap_dns: 20 | - 8.8.8.8:53 21 | filters: 22 | - url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt 23 | name: "" 24 | enabled: true 25 | - url: https://adaway.org/hosts.txt 26 | name: AdAway 27 | enabled: false 28 | - url: https://hosts-file.net/ad_servers.txt 29 | name: hpHosts - Ad and Tracking servers only 30 | enabled: false 31 | - url: http://www.malwaredomainlist.com/hostslist/hosts.txt 32 | name: MalwareDomainList.com Hosts List 33 | enabled: false 34 | clients: 35 | - name: localhost 36 | ip: 127.0.0.1 37 | mac: aa:aa:aa:aa:aa:aa 38 | use_global_settings: true 39 | use_global_blocked_services: true 40 | filtering_enabled: false 41 | parental_enabled: false 42 | safebrowsing_enabled: false 43 | safesearch_enabled: false 44 | schema_version: 5 45 | user_rules: [] 46 | -------------------------------------------------------------------------------- /internal/configmigrate/testdata/TestMigrateConfig_Migrate/v6/output.yml: -------------------------------------------------------------------------------- 1 | bind_host: 127.0.0.1 2 | bind_port: 3000 3 | users: 4 | - name: testuser 5 | password: testpassword 6 | dns: 7 | port: 53 8 | protection_enabled: true 9 | filtering_enabled: true 10 | safebrowsing_enabled: false 11 | safesearch_enabled: false 12 | parental_enabled: false 13 | parental_sensitivity: 0 14 | blocked_response_ttl: 10 15 | querylog_enabled: true 16 | upstream_dns: 17 | - tls://1.1.1.1 18 | - tls://1.0.0.1 19 | bootstrap_dns: 20 | - 8.8.8.8:53 21 | filters: 22 | - url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt 23 | name: "" 24 | enabled: true 25 | - url: https://adaway.org/hosts.txt 26 | name: AdAway 27 | enabled: false 28 | - url: https://hosts-file.net/ad_servers.txt 29 | name: hpHosts - Ad and Tracking servers only 30 | enabled: false 31 | - url: http://www.malwaredomainlist.com/hostslist/hosts.txt 32 | name: MalwareDomainList.com Hosts List 33 | enabled: false 34 | clients: 35 | - name: localhost 36 | ids: 37 | - 127.0.0.1 38 | - aa:aa:aa:aa:aa:aa 39 | ip: 127.0.0.1 40 | mac: aa:aa:aa:aa:aa:aa 41 | use_global_settings: true 42 | use_global_blocked_services: true 43 | filtering_enabled: false 44 | parental_enabled: false 45 | safebrowsing_enabled: false 46 | safesearch_enabled: false 47 | schema_version: 6 48 | user_rules: [] 49 | -------------------------------------------------------------------------------- /internal/configmigrate/v1.go: -------------------------------------------------------------------------------- 1 | package configmigrate 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/AdguardTeam/golibs/errors" 8 | "github.com/AdguardTeam/golibs/log" 9 | ) 10 | 11 | // migrateTo1 performs the following changes: 12 | // 13 | // # BEFORE: 14 | // # … 15 | // 16 | // # AFTER: 17 | // 'schema_version': 1 18 | // # … 19 | // 20 | // It also deletes the unused dnsfilter.txt file, since the following versions 21 | // store filters in data/filters/. 22 | func (m *Migrator) migrateTo1(diskConf yobj) (err error) { 23 | diskConf["schema_version"] = 1 24 | 25 | dnsFilterPath := filepath.Join(m.workingDir, "dnsfilter.txt") 26 | log.Printf("deleting %s as we don't need it anymore", dnsFilterPath) 27 | err = os.Remove(dnsFilterPath) 28 | if err != nil && !errors.Is(err, os.ErrNotExist) { 29 | log.Info("warning: %s", err) 30 | 31 | // Go on. 32 | } 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /internal/configmigrate/v11.go: -------------------------------------------------------------------------------- 1 | package configmigrate 2 | 3 | // migrateTo11 performs the following changes: 4 | // 5 | // # BEFORE: 6 | // 'schema_version': 10 7 | // 'rlimit_nofile': 42 8 | // # … 9 | // 10 | // # AFTER: 11 | // 'schema_version': 11 12 | // 'os': 13 | // 'group': '' 14 | // 'rlimit_nofile': 42 15 | // 'user': '' 16 | // # … 17 | func migrateTo11(diskConf yobj) (err error) { 18 | diskConf["schema_version"] = 11 19 | 20 | rlimit, _, err := fieldVal[int](diskConf, "rlimit_nofile") 21 | if err != nil { 22 | return err 23 | } 24 | 25 | delete(diskConf, "rlimit_nofile") 26 | diskConf["os"] = yobj{ 27 | "group": "", 28 | "rlimit_nofile": rlimit, 29 | "user": "", 30 | } 31 | 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /internal/configmigrate/v12.go: -------------------------------------------------------------------------------- 1 | package configmigrate 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/AdguardTeam/golibs/timeutil" 7 | ) 8 | 9 | // migrateTo12 performs the following changes: 10 | // 11 | // # BEFORE: 12 | // 'schema_version': 11 13 | // 'querylog_interval': 90 14 | // # … 15 | // 16 | // # AFTER: 17 | // 'schema_version': 12 18 | // 'querylog_interval': '2160h' 19 | // # … 20 | func migrateTo12(diskConf yobj) (err error) { 21 | diskConf["schema_version"] = 12 22 | 23 | dns, ok, err := fieldVal[yobj](diskConf, "dns") 24 | if !ok { 25 | return err 26 | } 27 | 28 | const field = "querylog_interval" 29 | 30 | qlogIvl, ok, err := fieldVal[int](dns, field) 31 | if !ok { 32 | if err != nil { 33 | return err 34 | } 35 | 36 | // Set the initial value from home.initConfig function. 37 | qlogIvl = 90 38 | } 39 | 40 | dns[field] = timeutil.Duration(time.Duration(qlogIvl) * timeutil.Day) 41 | 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /internal/configmigrate/v13.go: -------------------------------------------------------------------------------- 1 | package configmigrate 2 | 3 | // migrateTo13 performs the following changes: 4 | // 5 | // # BEFORE: 6 | // 'schema_version': 12 7 | // 'dns': 8 | // 'local_domain_name': 'lan' 9 | // # … 10 | // # … 11 | // 12 | // # AFTER: 13 | // 'schema_version': 13 14 | // 'dhcp': 15 | // 'local_domain_name': 'lan' 16 | // # … 17 | // # … 18 | func migrateTo13(diskConf yobj) (err error) { 19 | diskConf["schema_version"] = 13 20 | 21 | dns, ok, err := fieldVal[yobj](diskConf, "dns") 22 | if !ok { 23 | return err 24 | } 25 | 26 | dhcp, ok, err := fieldVal[yobj](diskConf, "dhcp") 27 | if !ok { 28 | return err 29 | } 30 | 31 | return moveSameVal[string](dns, dhcp, "local_domain_name") 32 | } 33 | -------------------------------------------------------------------------------- /internal/configmigrate/v14.go: -------------------------------------------------------------------------------- 1 | package configmigrate 2 | 3 | // migrateTo14 performs the following changes: 4 | // 5 | // # BEFORE: 6 | // 'schema_version': 13 7 | // 'dns': 8 | // 'resolve_clients': true 9 | // # … 10 | // 'clients': 11 | // - 'name': 'client-name' 12 | // # … 13 | // # … 14 | // 15 | // # AFTER: 16 | // 'schema_version': 14 17 | // 'dns': 18 | // # … 19 | // 'clients': 20 | // 'persistent': 21 | // - 'name': 'client-name' 22 | // # … 23 | // 'runtime_sources': 24 | // 'whois': true 25 | // 'arp': true 26 | // 'rdns': true 27 | // 'dhcp': true 28 | // 'hosts': true 29 | // # … 30 | func migrateTo14(diskConf yobj) (err error) { 31 | diskConf["schema_version"] = 14 32 | 33 | persistent, ok, err := fieldVal[yarr](diskConf, "clients") 34 | if !ok { 35 | if err != nil { 36 | return err 37 | } 38 | 39 | persistent = yarr{} 40 | } 41 | 42 | runtimeClients := yobj{ 43 | "whois": true, 44 | "arp": true, 45 | "rdns": false, 46 | "dhcp": true, 47 | "hosts": true, 48 | } 49 | diskConf["clients"] = yobj{ 50 | "persistent": persistent, 51 | "runtime_sources": runtimeClients, 52 | } 53 | 54 | dns, ok, err := fieldVal[yobj](diskConf, "dns") 55 | if err != nil { 56 | return err 57 | } else if !ok { 58 | return nil 59 | } 60 | 61 | return moveVal[bool](dns, runtimeClients, "resolve_clients", "rdns") 62 | } 63 | -------------------------------------------------------------------------------- /internal/configmigrate/v15.go: -------------------------------------------------------------------------------- 1 | package configmigrate 2 | 3 | import "github.com/AdguardTeam/golibs/errors" 4 | 5 | // migrateTo15 performs the following changes: 6 | // 7 | // # BEFORE: 8 | // 'schema_version': 14 9 | // 'dns': 10 | // # … 11 | // 'querylog_enabled': true 12 | // 'querylog_file_enabled': true 13 | // 'querylog_interval': '2160h' 14 | // 'querylog_size_memory': 1000 15 | // 'querylog': 16 | // # … 17 | // # … 18 | // 19 | // # AFTER: 20 | // 'schema_version': 15 21 | // 'dns': 22 | // # … 23 | // 'querylog': 24 | // 'enabled': true 25 | // 'file_enabled': true 26 | // 'interval': '2160h' 27 | // 'size_memory': 1000 28 | // 'ignored': [] 29 | // # … 30 | // # … 31 | func migrateTo15(diskConf yobj) (err error) { 32 | diskConf["schema_version"] = 15 33 | 34 | dns, ok, err := fieldVal[yobj](diskConf, "dns") 35 | if !ok { 36 | return err 37 | } 38 | 39 | qlog := map[string]any{ 40 | "ignored": yarr{}, 41 | "enabled": true, 42 | "file_enabled": true, 43 | "interval": "2160h", 44 | "size_memory": 1000, 45 | } 46 | diskConf["querylog"] = qlog 47 | 48 | return errors.Join( 49 | moveVal[bool](dns, qlog, "querylog_enabled", "enabled"), 50 | moveVal[bool](dns, qlog, "querylog_file_enabled", "file_enabled"), 51 | moveVal[any](dns, qlog, "querylog_interval", "interval"), 52 | moveVal[int](dns, qlog, "querylog_size_memory", "size_memory"), 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /internal/configmigrate/v17.go: -------------------------------------------------------------------------------- 1 | package configmigrate 2 | 3 | // migrateTo17 performs the following changes: 4 | // 5 | // # BEFORE: 6 | // 'schema_version': 16 7 | // 'dns': 8 | // 'edns_client_subnet': false 9 | // # … 10 | // # … 11 | // 12 | // # AFTER: 13 | // 'schema_version': 17 14 | // 'dns': 15 | // 'edns_client_subnet': 16 | // 'enabled': false 17 | // 'use_custom': false 18 | // 'custom_ip': "" 19 | // # … 20 | // # … 21 | func migrateTo17(diskConf yobj) (err error) { 22 | diskConf["schema_version"] = 17 23 | 24 | dns, ok, err := fieldVal[yobj](diskConf, "dns") 25 | if !ok { 26 | return err 27 | } 28 | 29 | const field = "edns_client_subnet" 30 | 31 | enabled, _, _ := fieldVal[bool](dns, field) 32 | dns[field] = yobj{ 33 | "enabled": enabled, 34 | "use_custom": false, 35 | "custom_ip": "", 36 | } 37 | 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /internal/configmigrate/v18.go: -------------------------------------------------------------------------------- 1 | package configmigrate 2 | 3 | // migrateTo18 performs the following changes: 4 | // 5 | // # BEFORE: 6 | // 'schema_version': 17 7 | // 'dns': 8 | // 'safesearch_enabled': true 9 | // # … 10 | // # … 11 | // 12 | // # AFTER: 13 | // 'schema_version': 18 14 | // 'dns': 15 | // 'safe_search': 16 | // 'enabled': true 17 | // 'bing': true 18 | // 'duckduckgo': true 19 | // 'google': true 20 | // 'pixabay': true 21 | // 'yandex': true 22 | // 'youtube': true 23 | // # … 24 | // # … 25 | func migrateTo18(diskConf yobj) (err error) { 26 | diskConf["schema_version"] = 18 27 | 28 | dns, ok, err := fieldVal[yobj](diskConf, "dns") 29 | if !ok { 30 | return err 31 | } 32 | 33 | safeSearch := yobj{ 34 | "enabled": true, 35 | "bing": true, 36 | "duckduckgo": true, 37 | "google": true, 38 | "pixabay": true, 39 | "yandex": true, 40 | "youtube": true, 41 | } 42 | dns["safe_search"] = safeSearch 43 | 44 | return moveVal[bool](dns, safeSearch, "safesearch_enabled", "enabled") 45 | } 46 | -------------------------------------------------------------------------------- /internal/configmigrate/v2.go: -------------------------------------------------------------------------------- 1 | package configmigrate 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/AdguardTeam/golibs/errors" 8 | "github.com/AdguardTeam/golibs/log" 9 | ) 10 | 11 | // migrateTo2 performs the following changes: 12 | // 13 | // # BEFORE: 14 | // 'schema_version': 1 15 | // 'coredns': 16 | // # … 17 | // 18 | // # AFTER: 19 | // 'schema_version': 2 20 | // 'dns': 21 | // # … 22 | // 23 | // It also deletes the Corefile file, since it isn't used anymore. 24 | func (m *Migrator) migrateTo2(diskConf yobj) (err error) { 25 | diskConf["schema_version"] = 2 26 | 27 | coreFilePath := filepath.Join(m.workingDir, "Corefile") 28 | log.Printf("deleting %s as we don't need it anymore", coreFilePath) 29 | err = os.Remove(coreFilePath) 30 | if err != nil && !errors.Is(err, os.ErrNotExist) { 31 | log.Info("warning: %s", err) 32 | 33 | // Go on. 34 | } 35 | 36 | return moveVal[any](diskConf, diskConf, "coredns", "dns") 37 | } 38 | -------------------------------------------------------------------------------- /internal/configmigrate/v20.go: -------------------------------------------------------------------------------- 1 | package configmigrate 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/AdguardTeam/golibs/timeutil" 7 | ) 8 | 9 | // migrateTo20 performs the following changes: 10 | // 11 | // # BEFORE: 12 | // 'schema_version': 19 13 | // 'statistics': 14 | // 'interval': 1 15 | // # … 16 | // # … 17 | // 18 | // # AFTER: 19 | // 'schema_version': 20 20 | // 'statistics': 21 | // 'interval': 24h 22 | // # … 23 | // # … 24 | func migrateTo20(diskConf yobj) (err error) { 25 | diskConf["schema_version"] = 20 26 | 27 | stats, ok, err := fieldVal[yobj](diskConf, "statistics") 28 | if !ok { 29 | return err 30 | } 31 | 32 | const field = "interval" 33 | 34 | ivl, ok, err := fieldVal[int](stats, field) 35 | if err != nil { 36 | return err 37 | } else if !ok || ivl == 0 { 38 | ivl = 1 39 | } 40 | 41 | stats[field] = timeutil.Duration(time.Duration(ivl) * timeutil.Day) 42 | 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /internal/configmigrate/v21.go: -------------------------------------------------------------------------------- 1 | package configmigrate 2 | 3 | // migrateTo21 performs the following changes: 4 | // 5 | // # BEFORE: 6 | // 'schema_version': 20 7 | // 'dns': 8 | // 'blocked_services': 9 | // - 'svc_name' 10 | // - # … 11 | // # … 12 | // # … 13 | // 14 | // # AFTER: 15 | // 'schema_version': 21 16 | // 'dns': 17 | // 'blocked_services': 18 | // 'ids': 19 | // - 'svc_name' 20 | // - # … 21 | // 'schedule': 22 | // 'time_zone': 'Local' 23 | // # … 24 | // # … 25 | func migrateTo21(diskConf yobj) (err error) { 26 | diskConf["schema_version"] = 21 27 | 28 | const field = "blocked_services" 29 | 30 | dns, ok, err := fieldVal[yobj](diskConf, "dns") 31 | if !ok { 32 | return err 33 | } 34 | 35 | svcs := yobj{ 36 | "schedule": yobj{ 37 | "time_zone": "Local", 38 | }, 39 | } 40 | 41 | err = moveVal[yarr](dns, svcs, field, "ids") 42 | if err != nil { 43 | return err 44 | } 45 | 46 | dns[field] = svcs 47 | 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /internal/configmigrate/v23.go: -------------------------------------------------------------------------------- 1 | package configmigrate 2 | 3 | import ( 4 | "fmt" 5 | "net/netip" 6 | "time" 7 | 8 | "github.com/AdguardTeam/golibs/timeutil" 9 | ) 10 | 11 | // migrateTo23 performs the following changes: 12 | // 13 | // # BEFORE: 14 | // 'schema_version': 22 15 | // 'bind_host': '1.2.3.4' 16 | // 'bind_port': 8080 17 | // 'web_session_ttl': 720 18 | // # … 19 | // 20 | // # AFTER: 21 | // 'schema_version': 23 22 | // 'http': 23 | // 'address': '1.2.3.4:8080' 24 | // 'session_ttl': '720h' 25 | // # … 26 | func migrateTo23(diskConf yobj) (err error) { 27 | diskConf["schema_version"] = 23 28 | 29 | bindHost, ok, err := fieldVal[string](diskConf, "bind_host") 30 | if !ok { 31 | return err 32 | } 33 | 34 | bindHostAddr, err := netip.ParseAddr(bindHost) 35 | if err != nil { 36 | return fmt.Errorf("invalid bind_host value: %s", bindHost) 37 | } 38 | 39 | bindPort, _, err := fieldVal[int](diskConf, "bind_port") 40 | if err != nil { 41 | return err 42 | } 43 | 44 | sessionTTL, _, err := fieldVal[int](diskConf, "web_session_ttl") 45 | if err != nil { 46 | return err 47 | } 48 | 49 | diskConf["http"] = yobj{ 50 | "address": netip.AddrPortFrom(bindHostAddr, uint16(bindPort)).String(), 51 | "session_ttl": timeutil.Duration(time.Duration(sessionTTL) * time.Hour).String(), 52 | } 53 | 54 | delete(diskConf, "bind_host") 55 | delete(diskConf, "bind_port") 56 | delete(diskConf, "web_session_ttl") 57 | 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /internal/configmigrate/v25.go: -------------------------------------------------------------------------------- 1 | package configmigrate 2 | 3 | // migrateTo25 performs the following changes: 4 | // 5 | // # BEFORE: 6 | // 'schema_version': 24 7 | // 'debug_pprof': true 8 | // # … 9 | // 10 | // # AFTER: 11 | // 'schema_version': 25 12 | // 'http': 13 | // 'pprof': 14 | // 'enabled': true 15 | // 'port': 6060 16 | // # … 17 | func migrateTo25(diskConf yobj) (err error) { 18 | diskConf["schema_version"] = 25 19 | 20 | httpObj, ok, err := fieldVal[yobj](diskConf, "http") 21 | if !ok { 22 | return err 23 | } 24 | 25 | pprofObj := yobj{ 26 | "enabled": false, 27 | "port": 6060, 28 | } 29 | 30 | err = moveVal[bool](diskConf, pprofObj, "debug_pprof", "enabled") 31 | if err != nil { 32 | return err 33 | } 34 | 35 | httpObj["pprof"] = pprofObj 36 | 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /internal/configmigrate/v28.go: -------------------------------------------------------------------------------- 1 | package configmigrate 2 | 3 | import ( 4 | "github.com/AdguardTeam/AdGuardHome/internal/dnsforward" 5 | ) 6 | 7 | // migrateTo28 performs the following changes: 8 | // 9 | // # BEFORE: 10 | // 'dns': 11 | // 'all_servers': true 12 | // 'fastest_addr': true 13 | // # … 14 | // # … 15 | // 16 | // # AFTER: 17 | // 'dns': 18 | // 'upstream_mode': 'parallel' 19 | // # … 20 | // # … 21 | func migrateTo28(diskConf yobj) (err error) { 22 | diskConf["schema_version"] = 28 23 | 24 | dns, ok, err := fieldVal[yobj](diskConf, "dns") 25 | if !ok { 26 | return err 27 | } 28 | 29 | allServers, _, _ := fieldVal[bool](dns, "all_servers") 30 | fastestAddr, _, _ := fieldVal[bool](dns, "fastest_addr") 31 | 32 | var upstreamModeType dnsforward.UpstreamMode 33 | if allServers { 34 | upstreamModeType = dnsforward.UpstreamModeParallel 35 | } else if fastestAddr { 36 | upstreamModeType = dnsforward.UpstreamModeFastestAddr 37 | } else { 38 | upstreamModeType = dnsforward.UpstreamModeLoadBalance 39 | } 40 | 41 | dns["upstream_mode"] = upstreamModeType 42 | 43 | delete(dns, "all_servers") 44 | delete(dns, "fastest_addr") 45 | 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /internal/configmigrate/v29.go: -------------------------------------------------------------------------------- 1 | package configmigrate 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | ) 7 | 8 | // migrateTo29 performs the following changes: 9 | // 10 | // # BEFORE: 11 | // 'filters': 12 | // - 'enabled': true 13 | // 'url': /path/to/file.txt 14 | // 'name': My FS Filter 15 | // 'id': 1234 16 | // 17 | // # AFTER: 18 | // 'filters': 19 | // - 'enabled': true 20 | // 'url': /path/to/file.txt 21 | // 'name': My FS Filter 22 | // 'id': 1234 23 | // # … 24 | // 'filtering': 25 | // 'safe_fs_patterns': 26 | // - '/opt/AdGuardHome/data/userfilters/*' 27 | // - '/path/to/file.txt' 28 | // # … 29 | func (m Migrator) migrateTo29(diskConf yobj) (err error) { 30 | diskConf["schema_version"] = 29 31 | 32 | filterVals, ok, err := fieldVal[[]any](diskConf, "filters") 33 | if !ok { 34 | return err 35 | } 36 | 37 | paths := []string{ 38 | filepath.Join(m.dataDir, "userfilters", "*"), 39 | } 40 | 41 | for i, v := range filterVals { 42 | var f yobj 43 | f, ok = v.(yobj) 44 | if !ok { 45 | return fmt.Errorf("filters: at index %d: expected object, got %T", i, v) 46 | } 47 | 48 | var u string 49 | u, ok, _ = fieldVal[string](f, "url") 50 | if ok && filepath.IsAbs(u) { 51 | paths = append(paths, u) 52 | } 53 | } 54 | 55 | fltConf, ok, err := fieldVal[yobj](diskConf, "filtering") 56 | if !ok { 57 | return err 58 | } 59 | 60 | fltConf["safe_fs_patterns"] = paths 61 | 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /internal/configmigrate/v3.go: -------------------------------------------------------------------------------- 1 | package configmigrate 2 | 3 | // migrateTo3 performs the following changes: 4 | // 5 | // # BEFORE: 6 | // 'schema_version': 2 7 | // 'dns': 8 | // 'bootstrap_dns': '1.1.1.1' 9 | // # … 10 | // 11 | // # AFTER: 12 | // 'schema_version': 3 13 | // 'dns': 14 | // 'bootstrap_dns': 15 | // - '1.1.1.1' 16 | // # … 17 | func migrateTo3(diskConf yobj) (err error) { 18 | diskConf["schema_version"] = 3 19 | 20 | dnsConfig, ok, err := fieldVal[yobj](diskConf, "dns") 21 | if !ok { 22 | return err 23 | } 24 | 25 | bootstrapDNS, ok, err := fieldVal[any](dnsConfig, "bootstrap_dns") 26 | if ok { 27 | dnsConfig["bootstrap_dns"] = yarr{bootstrapDNS} 28 | } 29 | 30 | return err 31 | } 32 | -------------------------------------------------------------------------------- /internal/configmigrate/v4.go: -------------------------------------------------------------------------------- 1 | package configmigrate 2 | 3 | // migrateTo4 performs the following changes: 4 | // 5 | // # BEFORE: 6 | // 'schema_version': 3 7 | // 'clients': 8 | // - # … 9 | // # … 10 | // 11 | // # AFTER: 12 | // 'schema_version': 4 13 | // 'clients': 14 | // - 'use_global_blocked_services': true 15 | // # … 16 | // # … 17 | func migrateTo4(diskConf yobj) (err error) { 18 | diskConf["schema_version"] = 4 19 | 20 | clients, ok, _ := fieldVal[yarr](diskConf, "clients") 21 | if ok { 22 | for i := range clients { 23 | if c, isYobj := clients[i].(yobj); isYobj { 24 | c["use_global_blocked_services"] = true 25 | } 26 | } 27 | } 28 | 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /internal/configmigrate/v5.go: -------------------------------------------------------------------------------- 1 | package configmigrate 2 | 3 | import ( 4 | "fmt" 5 | 6 | "golang.org/x/crypto/bcrypt" 7 | ) 8 | 9 | // migrateTo5 performs the following changes: 10 | // 11 | // # BEFORE: 12 | // 'schema_version': 4 13 | // 'auth_name': … 14 | // 'auth_pass': … 15 | // # … 16 | // 17 | // # AFTER: 18 | // 'schema_version': 5 19 | // 'users': 20 | // - 'name': … 21 | // 'password': 22 | // # … 23 | func migrateTo5(diskConf yobj) (err error) { 24 | diskConf["schema_version"] = 5 25 | 26 | user := yobj{} 27 | 28 | if err = moveVal[string](diskConf, user, "auth_name", "name"); err != nil { 29 | return err 30 | } 31 | 32 | pass, ok, err := fieldVal[string](diskConf, "auth_pass") 33 | if !ok { 34 | return err 35 | } 36 | delete(diskConf, "auth_pass") 37 | 38 | hash, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost) 39 | if err != nil { 40 | return fmt.Errorf("generating password hash: %w", err) 41 | } 42 | 43 | user["password"] = string(hash) 44 | diskConf["users"] = yarr{user} 45 | 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /internal/configmigrate/v6.go: -------------------------------------------------------------------------------- 1 | package configmigrate 2 | 3 | import "fmt" 4 | 5 | // migrateTo6 performs the following changes: 6 | // 7 | // # BEFORE: 8 | // 'schema_version': 5 9 | // 'clients': 10 | // - # … 11 | // 'ip': '127.0.0.1' 12 | // 'mac': 'AA:AA:AA:AA:AA:AA' 13 | // # … 14 | // # … 15 | // 16 | // # AFTER: 17 | // 'schema_version': 6 18 | // 'clients': 19 | // - # … 20 | // 'ip': '127.0.0.1' 21 | // 'mac': 'AA:AA:AA:AA:AA:AA' 22 | // 'ids': 23 | // - '127.0.0.1' 24 | // - 'AA:AA:AA:AA:AA:AA' 25 | // # … 26 | // # … 27 | func migrateTo6(diskConf yobj) (err error) { 28 | diskConf["schema_version"] = 6 29 | 30 | clients, ok, err := fieldVal[yarr](diskConf, "clients") 31 | if !ok { 32 | return err 33 | } 34 | 35 | for i, client := range clients { 36 | var c yobj 37 | c, ok = client.(yobj) 38 | if !ok { 39 | return fmt.Errorf("unexpected type of client at index %d: %T", i, client) 40 | } 41 | 42 | ids := yarr{} 43 | for _, id := range []string{"ip", "mac"} { 44 | val, _, valErr := fieldVal[string](c, id) 45 | if valErr != nil { 46 | return fmt.Errorf("client at index %d: %w", i, valErr) 47 | } else if val != "" { 48 | ids = append(ids, val) 49 | } 50 | } 51 | 52 | c["ids"] = ids 53 | } 54 | 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /internal/configmigrate/v8.go: -------------------------------------------------------------------------------- 1 | package configmigrate 2 | 3 | // migrateTo8 performs the following changes: 4 | // 5 | // # BEFORE: 6 | // 'schema_version': 7 7 | // 'dns': 8 | // 'bind_host': '127.0.0.1' 9 | // # … 10 | // # … 11 | // 12 | // # AFTER: 13 | // 'schema_version': 8 14 | // 'dns': 15 | // 'bind_hosts': 16 | // - '127.0.0.1' 17 | // # … 18 | // # … 19 | func migrateTo8(diskConf yobj) (err error) { 20 | diskConf["schema_version"] = 8 21 | 22 | dns, ok, err := fieldVal[yobj](diskConf, "dns") 23 | if !ok { 24 | return err 25 | } 26 | 27 | bindHost, ok, err := fieldVal[string](dns, "bind_host") 28 | if !ok { 29 | return err 30 | } 31 | 32 | delete(dns, "bind_host") 33 | dns["bind_hosts"] = yarr{bindHost} 34 | 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /internal/configmigrate/v9.go: -------------------------------------------------------------------------------- 1 | package configmigrate 2 | 3 | // migrateTo9 performs the following changes: 4 | // 5 | // # BEFORE: 6 | // 'schema_version': 8 7 | // 'dns': 8 | // 'autohost_tld': 'lan' 9 | // # … 10 | // # … 11 | // 12 | // # AFTER: 13 | // 'schema_version': 9 14 | // 'dns': 15 | // 'local_domain_name': 'lan' 16 | // # … 17 | // # … 18 | func migrateTo9(diskConf yobj) (err error) { 19 | diskConf["schema_version"] = 9 20 | 21 | dns, ok, err := fieldVal[yobj](diskConf, "dns") 22 | if !ok { 23 | return err 24 | } 25 | 26 | return moveVal[string](dns, dns, "autohost_tld", "local_domain_name") 27 | } 28 | -------------------------------------------------------------------------------- /internal/configmigrate/yaml.go: -------------------------------------------------------------------------------- 1 | package configmigrate 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type ( 8 | // yarr is the convenience alias for YAML array. 9 | yarr = []any 10 | 11 | // yobj is the convenience alias for YAML key-value object. 12 | yobj = map[string]any 13 | ) 14 | 15 | // fieldVal returns the value of type T for key from obj. Use [any] if the 16 | // field's type doesn't matter. 17 | func fieldVal[T any](obj yobj, key string) (v T, ok bool, err error) { 18 | val, ok := obj[key] 19 | if !ok { 20 | return v, false, nil 21 | } 22 | 23 | if val == nil { 24 | return v, true, nil 25 | } 26 | 27 | v, ok = val.(T) 28 | if !ok { 29 | return v, false, fmt.Errorf("unexpected type of %q: %T", key, val) 30 | } 31 | 32 | return v, true, nil 33 | } 34 | 35 | // moveVal copies the value for srcKey from src into dst for dstKey and deletes 36 | // it from src. 37 | func moveVal[T any](src, dst yobj, srcKey, dstKey string) (err error) { 38 | newVal, ok, err := fieldVal[T](src, srcKey) 39 | if !ok { 40 | return err 41 | } 42 | 43 | dst[dstKey] = newVal 44 | delete(src, srcKey) 45 | 46 | return nil 47 | } 48 | 49 | // moveSameVal moves the value for key from src into dst. 50 | func moveSameVal[T any](src, dst yobj, key string) (err error) { 51 | return moveVal[T](src, dst, key, key) 52 | } 53 | -------------------------------------------------------------------------------- /internal/dhcpd/bitset.go: -------------------------------------------------------------------------------- 1 | package dhcpd 2 | 3 | const bitsPerWord = 64 4 | 5 | // bitSet is a sparse bitSet. A nil *bitSet is an empty bitSet. 6 | type bitSet struct { 7 | words map[uint64]uint64 8 | } 9 | 10 | // newBitSet returns a new bitset. 11 | func newBitSet() (s *bitSet) { 12 | return &bitSet{ 13 | words: map[uint64]uint64{}, 14 | } 15 | } 16 | 17 | // isSet returns true if the bit n is set. 18 | func (s *bitSet) isSet(n uint64) (ok bool) { 19 | if s == nil { 20 | return false 21 | } 22 | 23 | wordIdx := n / bitsPerWord 24 | bitIdx := n % bitsPerWord 25 | 26 | var word uint64 27 | word, ok = s.words[wordIdx] 28 | 29 | return ok && word&(1< 2 | 3 | 4 | AdGuard Home API 5 | 6 | 7 | 8 | 9 | 10 | 13 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /scripts/companiesdb/download.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e -f -u -x 4 | 5 | # This script syncs companies DB that we bundle with AdGuard Home. The source 6 | # for this database is https://github.com/AdguardTeam/companiesdb. 7 | # 8 | trackers_url='https://raw.githubusercontent.com/AdguardTeam/companiesdb/main/dist/trackers.json' 9 | output='./client/src/helpers/trackers/trackers.json' 10 | readonly trackers_url output 11 | 12 | curl -o "$output" -v "$trackers_url" 13 | -------------------------------------------------------------------------------- /scripts/make/go-bench.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | verbose="${VERBOSE:-0}" 4 | readonly verbose 5 | 6 | # Verbosity levels: 7 | # 0 = Don't print anything except for errors. 8 | # 1 = Print commands, but not nested commands. 9 | # 2 = Print everything. 10 | if [ "$verbose" -gt '1' ]; then 11 | set -x 12 | v_flags='-v=1' 13 | x_flags='-x=1' 14 | elif [ "$verbose" -gt '0' ]; then 15 | set -x 16 | v_flags='-v=1' 17 | x_flags='-x=0' 18 | else 19 | set +x 20 | v_flags='-v=0' 21 | x_flags='-x=0' 22 | fi 23 | readonly v_flags x_flags 24 | 25 | set -e -f -u 26 | 27 | if [ "${RACE:-1}" -eq '0' ]; then 28 | race_flags='--race=0' 29 | else 30 | race_flags='--race=1' 31 | fi 32 | readonly race_flags 33 | 34 | go="${GO:-go}" 35 | 36 | count_flags='--count=2' 37 | shuffle_flags='--shuffle=on' 38 | timeout_flags="${TIMEOUT_FLAGS:---timeout=30s}" 39 | readonly go count_flags shuffle_flags timeout_flags 40 | 41 | "$go" test \ 42 | "$count_flags" \ 43 | "$shuffle_flags" \ 44 | "$race_flags" \ 45 | "$timeout_flags" \ 46 | "$x_flags" \ 47 | "$v_flags" \ 48 | --bench='.' \ 49 | --benchmem \ 50 | --benchtime='1s' \ 51 | --run='^$' \ 52 | ./... 53 | -------------------------------------------------------------------------------- /scripts/make/go-deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This comment is used to simplify checking local copies of the script. Bump 4 | # this number every time a significant change is made to this script. 5 | # 6 | # AdGuard-Project-Version: 2 7 | 8 | verbose="${VERBOSE:-0}" 9 | readonly verbose 10 | 11 | if [ "$verbose" -gt '1' ]; then 12 | env 13 | set -x 14 | x_flags='-x=1' 15 | elif [ "$verbose" -gt '0' ]; then 16 | set -x 17 | x_flags='-x=0' 18 | else 19 | set +x 20 | x_flags='-x=0' 21 | fi 22 | readonly x_flags 23 | 24 | set -e -f -u 25 | 26 | go="${GO:-go}" 27 | readonly go 28 | 29 | "$go" mod download "$x_flags" 30 | -------------------------------------------------------------------------------- /scripts/make/go-fuzz.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | verbose="${VERBOSE:-0}" 4 | readonly verbose 5 | 6 | # Verbosity levels: 7 | # 0 = Don't print anything except for errors. 8 | # 1 = Print commands, but not nested commands. 9 | # 2 = Print everything. 10 | if [ "$verbose" -gt '1' ]; then 11 | set -x 12 | v_flags='-v=1' 13 | x_flags='-x=1' 14 | elif [ "$verbose" -gt '0' ]; then 15 | set -x 16 | v_flags='-v=1' 17 | x_flags='-x=0' 18 | else 19 | set +x 20 | v_flags='-v=0' 21 | x_flags='-x=0' 22 | fi 23 | readonly v_flags x_flags 24 | 25 | set -e -f -u 26 | 27 | if [ "${RACE:-1}" -eq '0' ]; then 28 | race_flags='--race=0' 29 | else 30 | race_flags='--race=1' 31 | fi 32 | readonly race_flags 33 | 34 | go="${GO:-go}" 35 | 36 | count_flags='--count=2' 37 | shuffle_flags='--shuffle=on' 38 | timeout_flags="${TIMEOUT_FLAGS:---timeout=30s}" 39 | fuzztime_flags="${FUZZTIME_FLAGS:---fuzztime=20s}" 40 | 41 | readonly go count_flags shuffle_flags timeout_flags fuzztime_flags 42 | 43 | # TODO(a.garipov): File an issue about using --fuzz with multiple packages. 44 | "$go" test \ 45 | "$count_flags" \ 46 | "$shuffle_flags" \ 47 | "$race_flags" \ 48 | "$timeout_flags" \ 49 | "$x_flags" \ 50 | "$v_flags" \ 51 | "$fuzztime_flags" \ 52 | --fuzz='.' \ 53 | --run='^$' \ 54 | ./internal/filtering/rulelist/ \ 55 | ; 56 | -------------------------------------------------------------------------------- /scripts/make/go-tools.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This comment is used to simplify checking local copies of the script. Bump 4 | # this number every time a significant change is made to this script. 5 | # 6 | # AdGuard-Project-Version: 7 7 | 8 | verbose="${VERBOSE:-0}" 9 | readonly verbose 10 | 11 | if [ "$verbose" -gt '1' ]; then 12 | set -x 13 | v_flags='-v=1' 14 | x_flags='-x=1' 15 | elif [ "$verbose" -gt '0' ]; then 16 | set -x 17 | v_flags='-v=1' 18 | x_flags='-x=0' 19 | else 20 | set +x 21 | v_flags='-v=0' 22 | x_flags='-x=0' 23 | fi 24 | readonly v_flags x_flags 25 | 26 | set -e -f -u 27 | 28 | # Reset GOARCH and GOOS to make sure we install the tools for the native 29 | # architecture even when we're cross-compiling the main binary, and also to 30 | # prevent the "cannot install cross-compiled binaries when GOBIN is set" error. 31 | env \ 32 | GOARCH="" \ 33 | GOBIN="${PWD}/bin" \ 34 | GOOS="" \ 35 | GOWORK='off' \ 36 | "${GO:-go}" install "$v_flags" "$x_flags" tool \ 37 | ; 38 | -------------------------------------------------------------------------------- /scripts/make/go-upd-tools.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This comment is used to simplify checking local copies of the script. Bump 4 | # this number every time a significant change is made to this script. 5 | # 6 | # AdGuard-Project-Version: 4 7 | 8 | verbose="${VERBOSE:-0}" 9 | readonly verbose 10 | 11 | if [ "$verbose" -gt '1' ]; then 12 | env 13 | set -x 14 | x_flags='-x=1' 15 | elif [ "$verbose" -gt '0' ]; then 16 | set -x 17 | x_flags='-x=0' 18 | else 19 | set +x 20 | x_flags='-x=0' 21 | fi 22 | readonly x_flags 23 | 24 | set -e -f -u 25 | 26 | go="${GO:-go}" 27 | readonly go 28 | 29 | "$go" get -u "$x_flags" tool 30 | "$go" mod tidy "$x_flags" 31 | -------------------------------------------------------------------------------- /scripts/make/md-lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This comment is used to simplify checking local copies of the script. Bump 4 | # this number every time a remarkable change is made to this script. 5 | # 6 | # AdGuard-Project-Version: 3 7 | 8 | verbose="${VERBOSE:-0}" 9 | readonly verbose 10 | 11 | # Don't use -f, because we use globs in this script. 12 | set -e -u 13 | 14 | if [ "$verbose" -gt '0' ]; then 15 | set -x 16 | fi 17 | 18 | # TODO(e.burkov): Add README.md and possibly AGHTechDoc.md. 19 | markdownlint \ 20 | ./CHANGELOG.md \ 21 | ./CONTRIBUTING.md \ 22 | ./HACKING.md \ 23 | ./SECURITY.md \ 24 | ./internal/next/changelog.md \ 25 | ./internal/dhcpd/*.md \ 26 | ./openapi/*.md \ 27 | ./scripts/*.md \ 28 | ; 29 | -------------------------------------------------------------------------------- /scripts/make/sh-lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This comment is used to simplify checking local copies of the script. Bump 4 | # this number every time a remarkable change is made to this script. 5 | # 6 | # AdGuard-Project-Version: 3 7 | 8 | verbose="${VERBOSE:-0}" 9 | readonly verbose 10 | 11 | # Don't use -f, because we use globs in this script. 12 | set -e -u 13 | 14 | if [ "$verbose" -gt '0' ]; then 15 | set -x 16 | fi 17 | 18 | # Source the common helpers, including not_found and run_linter. 19 | . ./scripts/make/helper.sh 20 | 21 | run_linter -e shfmt --binary-next-line -d -p -s \ 22 | ./scripts/hooks/* \ 23 | ./scripts/install.sh \ 24 | ./scripts/make/*.sh \ 25 | ./scripts/snap/*.sh \ 26 | ./snap/local/*.sh \ 27 | ; 28 | 29 | shellcheck -e 'SC2250' -e 'SC2310' -f 'gcc' -o 'all' -x -- \ 30 | ./scripts/hooks/* \ 31 | ./scripts/install.sh \ 32 | ./scripts/make/*.sh \ 33 | ./scripts/snap/*.sh \ 34 | ./snap/local/*.sh \ 35 | ; 36 | -------------------------------------------------------------------------------- /scripts/querylog/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "querylog", 3 | "version": "0.1.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "dns-packet": { 8 | "version": "5.2.1", 9 | "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.2.1.tgz", 10 | "integrity": "sha512-JHj2yJeKOqlxzeuYpN1d56GfhzivAxavNwHj9co3qptECel27B1rLY5PifJAvubsInX5pGLDjAHuCfCUc2Zv/w==", 11 | "requires": { 12 | "ip": "^1.1.5" 13 | } 14 | }, 15 | "ip": { 16 | "version": "1.1.5", 17 | "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", 18 | "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /scripts/querylog/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "querylog", 3 | "version": "0.1.0", 4 | "scripts": { 5 | "anonymize": "node anonymize.js" 6 | }, 7 | "dependencies": { 8 | "dns-packet": "^5.2.1" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /scripts/snap/download.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | verbose="${VERBOSE:-0}" 4 | 5 | if [ "$verbose" -gt '0' ]; then 6 | set -x 7 | fi 8 | 9 | set -e -f -u 10 | 11 | channel="${CHANNEL:?please set CHANNEL}" 12 | readonly channel 13 | 14 | printf '%s %s\n' \ 15 | '386' 'i386' \ 16 | 'amd64' 'amd64' \ 17 | 'armv7' 'armhf' \ 18 | 'arm64' 'arm64' \ 19 | | while read -r arch snap_arch; do 20 | release_url="https://static.adtidy.org/adguardhome/${channel}/AdGuardHome_linux_${arch}.tar.gz" 21 | output="./AdGuardHome_linux_${arch}.tar.gz" 22 | 23 | curl -o "$output" -v "$release_url" 24 | tar -f "$output" -v -x -z 25 | cp ./AdGuardHome/AdGuardHome "./AdGuardHome_${snap_arch}" 26 | rm -f -r "$output" ./AdGuardHome 27 | done 28 | -------------------------------------------------------------------------------- /snap/gui/adguard-home-web.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Encoding=UTF-8 4 | Name=AdGuard Home 5 | Comment=Network-wide ads & trackers blocking DNS server 6 | Exec=adguard-home.adguard-home-web 7 | Icon=${SNAP}/meta/gui/adguard-home-web.png 8 | Terminal=false 9 | -------------------------------------------------------------------------------- /snap/gui/adguard-home-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/7cf1600f1b2fe487a0034b03a5ff31bd7f0efb9f/snap/gui/adguard-home-web.png -------------------------------------------------------------------------------- /snap/local/adguard-home-web.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # shellcheck disable=SC2154 4 | conf_file="${SNAP_DATA}/AdGuardHome.yaml" 5 | readonly conf_file 6 | 7 | if ! [ -f "$conf_file" ]; then 8 | xdg-open 'http://localhost:3000' 9 | 10 | exit 11 | fi 12 | 13 | # Get the admin interface port from the configuration. 14 | # 15 | # shellcheck disable=SC2016 16 | awk_prog='/^[^[:space:]]/ { is_http = /^http:/ };/^[[:space:]]+address:/ { if (is_http) print $2 }' 17 | readonly awk_prog 18 | 19 | bind_port="$(awk "$awk_prog" "$conf_file" | awk -F ':' '{print $NF}')" 20 | readonly bind_port 21 | 22 | if [ "$bind_port" = '' ]; then 23 | xdg-open 'http://localhost:3000' 24 | else 25 | xdg-open "http://localhost:${bind_port}" 26 | fi 27 | -------------------------------------------------------------------------------- /staticcheck.conf: -------------------------------------------------------------------------------- 1 | checks = ["all"] 2 | initialisms = [ 3 | # See https://github.com/dominikh/go-tools/blob/master/config/config.go. 4 | # 5 | # Do not add "PTR" since we use "Ptr" as a suffix. 6 | "inherit" 7 | , "ASN" 8 | , "DHCP" 9 | , "DNSSEC" 10 | # E.g. SentryDSN. 11 | , "DSN" 12 | , "ECS" 13 | , "EDNS" 14 | , "MX" 15 | , "QUIC" 16 | , "RA" 17 | , "RRSIG" 18 | , "SDNS" 19 | , "SLAAC" 20 | , "SOA" 21 | , "SVCB" 22 | , "TLD" 23 | , "WHOIS" 24 | ] 25 | dot_import_whitelist = [] 26 | http_status_code_whitelist = [] 27 | --------------------------------------------------------------------------------