diff --git a/.gitignore b/.gitignore index 3873fd3d..bdc4c29f 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,6 @@ /snapcraft_login AdGuardHome* coverage.txt -leases.db node_modules/ !/build/gitkeep diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bb96409..12bfdb07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,11 +14,11 @@ and this project adheres to @@ -29,6 +29,38 @@ NOTE: Add new changes ABOVE THIS COMMENT. +## [v0.107.29] - 2023-04-18 + +See also the [v0.107.29 GitHub milestone][ms-v0.107.29]. + +### Added + +- The ability to exclude client activity from the query log or statistics by + editing client's settings on the Clients settings page in the UI ([#1717], + [#4299]). + +### Changed + +- Stored DHCP leases moved from `leases.db` to `data/leases.json`. The file + format has also been optimized. + +### Fixed + +- The `github.com/mdlayher/raw` dependency has been temporarily returned to + support raw connections on Darwin ([#5712]). +- Incorrect recording of blocked results as “Blocked by CNAME or IP” in the + query log ([#5725]). +- All Safe Search services being unchecked by default. +- Panic when a DNSCrypt stamp is invalid ([#5721]). + +[#5712]: https://github.com/AdguardTeam/AdGuardHome/issues/5712 +[#5721]: https://github.com/AdguardTeam/AdGuardHome/issues/5721 +[#5725]: https://github.com/AdguardTeam/AdGuardHome/issues/5725 + +[ms-v0.107.29]: https://github.com/AdguardTeam/AdGuardHome/milestone/65?closed=1 + + + ## [v0.107.28] - 2023-04-12 See also the [v0.107.28 GitHub milestone][ms-v0.107.28]. @@ -149,12 +181,10 @@ In this release, the schema version has changed from 17 to 20. [#1163]: https://github.com/AdguardTeam/AdGuardHome/issues/1163 [#1333]: https://github.com/AdguardTeam/AdGuardHome/issues/1333 -[#1163]: https://github.com/AdguardTeam/AdGuardHome/issues/1717 [#1472]: https://github.com/AdguardTeam/AdGuardHome/issues/1472 [#3290]: https://github.com/AdguardTeam/AdGuardHome/issues/3290 [#3459]: https://github.com/AdguardTeam/AdGuardHome/issues/3459 [#4262]: https://github.com/AdguardTeam/AdGuardHome/issues/4262 -[#3290]: https://github.com/AdguardTeam/AdGuardHome/issues/4299 [#5567]: https://github.com/AdguardTeam/AdGuardHome/issues/5567 [#5701]: https://github.com/AdguardTeam/AdGuardHome/issues/5701 @@ -1920,11 +1950,12 @@ See also the [v0.104.2 GitHub milestone][ms-v0.104.2]. -[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.28...HEAD +[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.29...HEAD +[v0.107.29]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.28...v0.107.29 [v0.107.28]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.27...v0.107.28 [v0.107.27]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.26...v0.107.27 [v0.107.26]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.25...v0.107.26 diff --git a/client/public/index.html b/client/public/index.html index d736e508..730a3c95 100644 --- a/client/public/index.html +++ b/client/public/index.html @@ -12,11 +12,40 @@ AdGuard Home + -
+
+
+
+ diff --git a/client/public/login.html b/client/public/login.html index 586522c7..611edaa2 100644 --- a/client/public/login.html +++ b/client/public/login.html @@ -17,5 +17,12 @@ You need to enable JavaScript to run this app.
+ diff --git a/client/src/__locales/ar.json b/client/src/__locales/ar.json index b091c4fd..55b176ad 100644 --- a/client/src/__locales/ar.json +++ b/client/src/__locales/ar.json @@ -635,5 +635,6 @@ "parental_control": "الرقابة الابويه", "safe_browsing": "تصفح آمن", "served_from_cache": "{{value}} (يتم تقديمه من ذاكرة التخزين المؤقت)", - "form_error_password_length": "يجب أن تتكون كلمة المرور من {{value}} من الأحرف على الأقل" + "form_error_password_length": "يجب أن تتكون كلمة المرور من {{value}} من الأحرف على الأقل", + "protection_section_label": "الحماية" } diff --git a/client/src/__locales/be.json b/client/src/__locales/be.json index a90f2fb4..0ad7119a 100644 --- a/client/src/__locales/be.json +++ b/client/src/__locales/be.json @@ -642,5 +642,6 @@ "anonymizer_notification": "<0>Заўвага: Ананімізацыя IP уключана. Вы можаце адключыць яе ў <1>Агульных наладах.", "confirm_dns_cache_clear": "Вы ўпэўнены, што хочаце ачысціць кэш DNS?", "cache_cleared": "Кэш DNS паспяхова ачышчаны", - "clear_cache": "Ачысціць кэш" + "clear_cache": "Ачысціць кэш", + "protection_section_label": "Ахова" } diff --git a/client/src/__locales/cs.json b/client/src/__locales/cs.json index 444415e2..3c7533e9 100644 --- a/client/src/__locales/cs.json +++ b/client/src/__locales/cs.json @@ -257,12 +257,12 @@ "query_log_cleared": "Protokol dotazů byl úspěšně vymazán", "query_log_updated": "Protokol dotazů byl úspěšně aktualizován", "query_log_clear": "Vymazat protokoly dotazů", - "query_log_retention": "Uchování protokolů dotazů", + "query_log_retention": "Rotace protokolů dotazů", "query_log_enable": "Povolit protokol", "query_log_configuration": "Konfigurace protokolů", "query_log_disabled": "Protokol dotazu je zakázán a lze jej nakonfigurovat v <0>nastavení", "query_log_strict_search": "Pro striktní vyhledávání použijte dvojité uvozovky", - "query_log_retention_confirm": "Opravdu chcete změnit uchovávání protokolu dotazů? Pokud snížíte hodnotu intervalu, některá data budou ztracena", + "query_log_retention_confirm": "Opravdu chcete změnit rotaci protokolu dotazů? Pokud snížíte hodnotu intervalu, některá data budou ztracena", "anonymize_client_ip": "Anonymizovat IP klienta", "anonymize_client_ip_desc": "Neukládat úplnou IP adresu klienta do protokolů a statistik", "dns_config": "Konfigurace DNS serveru", @@ -668,5 +668,11 @@ "disable_notify_for_hours": "Vypnout ochranu na {{count}} hod.", "disable_notify_for_hours_plural": "Vypnout ochranu na {{count}} hod.", "disable_notify_until_tomorrow": "Vypnout ochranu do zítřka", - "enable_protection_timer": "Ochrana bude zapnuta za {{time}}" + "enable_protection_timer": "Ochrana bude zapnuta za {{time}}", + "custom_retention_input": "Zadejte retenci v hodinách", + "custom_rotation_input": "Zadejte rotaci v hodinách", + "protection_section_label": "Ochrana", + "log_and_stats_section_label": "Protokol dotazů a statistiky", + "ignore_query_log": "Ignorovat tohoto klienta v protokolu dotazů", + "ignore_statistics": "Ignorovat tohoto klienta ve statistikách" } diff --git a/client/src/__locales/da.json b/client/src/__locales/da.json index 465143b5..076f2536 100644 --- a/client/src/__locales/da.json +++ b/client/src/__locales/da.json @@ -257,12 +257,12 @@ "query_log_cleared": "Forespørgselsloggen er blevet ryddet", "query_log_updated": "Forespørgselsloggen er blevet opdateret", "query_log_clear": "Ryd forespørgselslogfiler", - "query_log_retention": "Opbevar forespørgselslogger i", + "query_log_retention": "Rotation af forespørgselslog", "query_log_enable": "Aktivér log", "query_log_configuration": "Opsætning af logger", "query_log_disabled": "Forespørgselsloggen er deaktiveret og kan opsættes i <0>indstillingerne", "query_log_strict_search": "Brug dobbelt anførselstegn til stringent søgning", - "query_log_retention_confirm": "Sikker på, at du vil ændre forespørgselsloggens opbevaringperiode? Mindskes intervalværdien, mistes data", + "query_log_retention_confirm": "Sikker på, at forespørgselsloggens rotationstid skal ændres? Mindskes intervalværdien, mistes nogle data", "anonymize_client_ip": "Anonymisér klient-IP", "anonymize_client_ip_desc": "Gem ikke fuld klient IP-adresse i logfiler eller statistikker", "dns_config": "DNS-serveropsætning", @@ -668,5 +668,11 @@ "disable_notify_for_hours": "Deaktivere beskyttelse i {{count}} time", "disable_notify_for_hours_plural": "Deaktivere beskyttelse i {{count}} timer", "disable_notify_until_tomorrow": "Deaktiver beskyttelse indtil i morgen", - "enable_protection_timer": "Beskyttelse deaktiveres om {{time}}" + "enable_protection_timer": "Beskyttelse deaktiveres om {{time}}", + "custom_retention_input": "Angiv opbevaringstid i timer", + "custom_rotation_input": "Angiv rotationstid i timer", + "protection_section_label": "Beskyttelse", + "log_and_stats_section_label": "Forespørgselslog og statistik", + "ignore_query_log": "Ignorér denne klient i forespørgselslog", + "ignore_statistics": "Ignorér denne klient i statistik" } diff --git a/client/src/__locales/de.json b/client/src/__locales/de.json index b7d59a9c..230caa3c 100644 --- a/client/src/__locales/de.json +++ b/client/src/__locales/de.json @@ -257,12 +257,12 @@ "query_log_cleared": "Das Abfrageprotokoll wurde erfolgreich gelöscht", "query_log_updated": "Das Abfrageprotokoll wurde erfolgreich aktualisiert", "query_log_clear": "Abfrageprotokolle leeren", - "query_log_retention": "Abfrageprotokolle aufbewahren", + "query_log_retention": "Rotation der Abfrageprotokolle", "query_log_enable": "Protokoll aktivieren", "query_log_configuration": "Konfiguration der Protokolle", "query_log_disabled": "Das Abfrageprotokoll ist deaktiviert und kann in den <0>Einstellungen konfiguriert werden.", "query_log_strict_search": "Doppelte Anführungszeichen für die strikte Suche verwenden", - "query_log_retention_confirm": "Möchten Sie die Aufbewahrung des Abfrageprotokolls wirklich ändern? Wenn Sie den Zeitabstand verringern, gehen einige Daten verloren.", + "query_log_retention_confirm": "Möchten Sie die Abfrageprotokollrotation wirklich ändern? Wenn Sie den Intervallwert verringern, gehen einige Daten verloren", "anonymize_client_ip": "Client-IP anonymisieren", "anonymize_client_ip_desc": "Vollständige IP-Adresse des Clients nicht in Protokollen und Statistiken speichern", "dns_config": "DNS-Serverkonfiguration", @@ -668,5 +668,11 @@ "disable_notify_for_hours": "Schutz für {{count}} Stunde deaktivieren", "disable_notify_for_hours_plural": "Schutz für {{count}} Stunden deaktivieren", "disable_notify_until_tomorrow": "Schutz bis morgen deaktivieren", - "enable_protection_timer": "Der Schutz wird in {{time}} wieder aktiviert" + "enable_protection_timer": "Der Schutz wird in {{time}} wieder aktiviert", + "custom_retention_input": "Rückhaltezeit in Stunden eingeben", + "custom_rotation_input": "Rotation in Stunden eingeben", + "protection_section_label": "Schutz", + "log_and_stats_section_label": "Abfrageprotokoll und Statistik", + "ignore_query_log": "Diesen Client im Abfrageprotokoll ignorieren", + "ignore_statistics": "Diesen Client in der Statistik ignorieren" } diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index 14bd92e7..a2633ad8 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -257,12 +257,12 @@ "query_log_cleared": "The query log has been successfully cleared", "query_log_updated": "The query log has been successfully updated", "query_log_clear": "Clear query logs", - "query_log_retention": "Query logs retention", + "query_log_retention": "Query logs rotation", "query_log_enable": "Enable log", "query_log_configuration": "Logs configuration", "query_log_disabled": "The query log is disabled and can be configured in the <0>settings", "query_log_strict_search": "Use double quotes for strict search", - "query_log_retention_confirm": "Are you sure you want to change query log retention? If you decrease the interval value, some data will be lost", + "query_log_retention_confirm": "Are you sure you want to change query log rotation? If you decrease the interval value, some data will be lost", "anonymize_client_ip": "Anonymize client IP", "anonymize_client_ip_desc": "Don't save the client's full IP address to logs or statistics", "dns_config": "DNS server configuration", @@ -668,5 +668,11 @@ "disable_notify_for_hours": "Disable protection for {{count}} hour", "disable_notify_for_hours_plural": "Disable protection for {{count}} hours", "disable_notify_until_tomorrow": "Disable protection until tomorrow", - "enable_protection_timer": "Protection will be enabled in {{time}}" + "enable_protection_timer": "Protection will be enabled in {{time}}", + "custom_retention_input": "Enter retention in hours", + "custom_rotation_input": "Enter rotation in hours", + "protection_section_label": "Protection", + "log_and_stats_section_label": "Query log and statistics", + "ignore_query_log": "Ignore this client in query log", + "ignore_statistics": "Ignore this client in statistics" } diff --git a/client/src/__locales/es.json b/client/src/__locales/es.json index 2e112cd3..a4b3292b 100644 --- a/client/src/__locales/es.json +++ b/client/src/__locales/es.json @@ -257,12 +257,12 @@ "query_log_cleared": "El registro de consultas se ha borrado correctamente", "query_log_updated": "El registro de consultas se ha actualizado correctamente", "query_log_clear": "Borrar registros de consultas", - "query_log_retention": "Retención de registros de consultas", + "query_log_retention": "Rotanción de registros de consultas", "query_log_enable": "Habilitar registro", "query_log_configuration": "Configuración de registros", "query_log_disabled": "El registro de consultas está deshabilitado y se puede configurar en la <0>configuración", "query_log_strict_search": "Usar comillas dobles para una búsqueda estricta", - "query_log_retention_confirm": "¿Estás seguro de que deseas cambiar la retención del registro de consultas? Si disminuye el valor del intervalo, se perderán algunos datos", + "query_log_retention_confirm": "¿Está seguro de que deseas cambiar la rotación del registro de consultas? Si reduces el valor del intervalo, se perderán algunos datos", "anonymize_client_ip": "Anonimizar IP del cliente", "anonymize_client_ip_desc": "No guarda la dirección IP completa del cliente en registros o estadísticas", "dns_config": "Configuración del servidor DNS", @@ -668,5 +668,11 @@ "disable_notify_for_hours": "Desactivar la protección por {{count}} hora", "disable_notify_for_hours_plural": "Desactivar la protección por {{count}} horas", "disable_notify_until_tomorrow": "Desactivar la protección hasta mañana", - "enable_protection_timer": "La protección se activará en {{time}}" + "enable_protection_timer": "La protección se activará en {{time}}", + "custom_retention_input": "Ingresa la retención en horas", + "custom_rotation_input": "Ingresa la rotación en horas", + "protection_section_label": "Protección", + "log_and_stats_section_label": "Registro de consultas y estadísticas", + "ignore_query_log": "Ignorar este cliente en el registro de consultas", + "ignore_statistics": "Ignorar este cliente en las estadísticas" } diff --git a/client/src/__locales/fi.json b/client/src/__locales/fi.json index e74f05d5..89cc2444 100644 --- a/client/src/__locales/fi.json +++ b/client/src/__locales/fi.json @@ -257,12 +257,12 @@ "query_log_cleared": "Pyyntöhistorian tyhjennys onnistui", "query_log_updated": "Pyyntöhistorian päivitys onnistui", "query_log_clear": "Tyhjennä pyyntöhistoria", - "query_log_retention": "Pyyntöhistorian säilytys", + "query_log_retention": "Kyselylokien kierto", "query_log_enable": "Käytä historiaa", "query_log_configuration": "Historian määritys", "query_log_disabled": "Pyyntöhistoria ei ole käytössä. Voit ottaa sen käyttöön <0>asetuksissa", "query_log_strict_search": "Käytä tarkalle haulle lainausmerkkejä", - "query_log_retention_confirm": "Haluatko varmasti muuttaa pyyntöhistoriasi säilytysaikaa? Jos lyhennät aikaa, joitakin tietoja menetetään", + "query_log_retention_confirm": "Haluatko varmasti muuttaa kyselylokin kiertoa? Jos pienennät intervalliarvoa, osa tiedoista menetetään", "anonymize_client_ip": "Piilota päätelaitteen IP-osoite", "anonymize_client_ip_desc": "Älä tallenna päätelaitteen täydellistä IP-osoitetta historiaan ja tilastoihin.", "dns_config": "DNS-palvelimen määritys", @@ -668,5 +668,11 @@ "disable_notify_for_hours": "Poista suojaus käytöstä {{count}} tunniksi", "disable_notify_for_hours_plural": "Poista suojaus käytöstä {{count}} tunniksi", "disable_notify_until_tomorrow": "Poista suojaus käytöstä huomiseen asti", - "enable_protection_timer": "Suojaus otetaan käyttöön {{time}} kuluttua" + "enable_protection_timer": "Suojaus otetaan käyttöön {{time}} kuluttua", + "custom_retention_input": "Syötä säilytysaika tunteina", + "custom_rotation_input": "Syötä uudistusaika tunteina", + "protection_section_label": "Suojaus", + "log_and_stats_section_label": "Kyselyhistoria ja tilastot", + "ignore_query_log": "Älä huomioi tätä päätettä kyselyhistoriassa", + "ignore_statistics": "Älä huomioi tätä päätettä tilastoissa" } diff --git a/client/src/__locales/fr.json b/client/src/__locales/fr.json index b514ca87..facc29ee 100644 --- a/client/src/__locales/fr.json +++ b/client/src/__locales/fr.json @@ -257,12 +257,12 @@ "query_log_cleared": "Le journal des requêtes a été effacé", "query_log_updated": "Le journal des requêtes a été mis à jour", "query_log_clear": "Effacer journal des requêtes", - "query_log_retention": "Rétention du journal des requêtes", + "query_log_retention": "Rotation des journaux de requêtes", "query_log_enable": "Activer le journal", "query_log_configuration": "Configuration du journal", "query_log_disabled": "Le journal des requêtes est désactivé et peut être configuré dans les <0>paramètres", "query_log_strict_search": "Utilisez les doubles guillemets pour une recherche stricte", - "query_log_retention_confirm": "Êtes-vous sûr de vouloir modifier la rétention des journaux de requêtes ? Si vous diminuez la valeur de l'intervalle, certaines données seront perdues", + "query_log_retention_confirm": "Êtes-vous sûr de souhaiter modifier la rotation des journaux de requêtes ? Si vous diminuez la valeur de l'intervalle, certaines données seront perdues", "anonymize_client_ip": "Anonymiser l’IP du client", "anonymize_client_ip_desc": "Ne pas enregistrer l’adresse IP complète du client dans les journaux et statistiques", "dns_config": "Configuration du serveur DNS", @@ -668,5 +668,11 @@ "disable_notify_for_hours": "Désactiver la protection pendant {{count}} heure", "disable_notify_for_hours_plural": "Désactiver la protection pendant {{count}} heures", "disable_notify_until_tomorrow": "Désactiver la protection jusqu'à demain", - "enable_protection_timer": "La protection sera activée dans {{time}}" + "enable_protection_timer": "La protection sera activée dans {{time}}", + "custom_retention_input": "Saisir la rétention en heures", + "custom_rotation_input": "Saisir la rotation en heures", + "protection_section_label": "Protection", + "log_and_stats_section_label": "Journal des requêtes et statistiques", + "ignore_query_log": "Ignorer ce client dans le journal des requêtes", + "ignore_statistics": "Ignorer ce client dans les statistiques" } diff --git a/client/src/__locales/hr.json b/client/src/__locales/hr.json index d7784998..6114d133 100644 --- a/client/src/__locales/hr.json +++ b/client/src/__locales/hr.json @@ -257,12 +257,12 @@ "query_log_cleared": "Zapisnik upita je uspješno uklonjen", "query_log_updated": "Zapisnik upita je uspješno ažuriran", "query_log_clear": "Očisti zapisnik upita", - "query_log_retention": "Spremanje zapisnika upita", + "query_log_retention": "Rotacija dnevnika upita", "query_log_enable": "Omogući zapise", "query_log_configuration": "Postavke zapisa", "query_log_disabled": "Zapisnik upita je onemogućen i može se postaviti u <0>postavkama", "query_log_strict_search": "Koristite dvostruke navodnike za strogo pretraživanje", - "query_log_retention_confirm": "Jeste li sigurni da želite promijeniti zadržavanje zapisnika upita? Ako smanjite vrijednost intervala, neki će podaci biti izgubljeni", + "query_log_retention_confirm": "Jeste li sigurni da želite promijeniti rotaciju dnevnika upita? Ako smanjite vrijednost intervala, neki će se podaci izgubiti", "anonymize_client_ip": "Anonimiraj IP klijenta", "anonymize_client_ip_desc": "Ne spremajte cijelu IP adresu klijenta u zapisnike i statistike", "dns_config": "DNS postavke poslužitelja", @@ -668,5 +668,11 @@ "disable_notify_for_hours": "Isključi zaštitu na {{count}} sati", "disable_notify_for_hours_plural": "Isključi zaštitu na {{count}} sati", "disable_notify_until_tomorrow": "Isključi zaštitu do sutra", - "enable_protection_timer": "Zaštita će biti omogućena u {{time}}" + "enable_protection_timer": "Zaštita će biti omogućena u {{time}}", + "custom_retention_input": "Unesite zadržavanje u satima", + "custom_rotation_input": "Unesite rotaciju u satima", + "protection_section_label": "Zaštita", + "log_and_stats_section_label": "Zapisnik upita i statistika", + "ignore_query_log": "Zanemari ovog klijenta u zapisniku upita", + "ignore_statistics": "Ignorirajte ovog klijenta u statistici" } diff --git a/client/src/__locales/hu.json b/client/src/__locales/hu.json index aef5766d..7b885ff3 100644 --- a/client/src/__locales/hu.json +++ b/client/src/__locales/hu.json @@ -642,5 +642,6 @@ "anonymizer_notification": "<0>Megjegyzés: Az IP anonimizálás engedélyezve van. Az <1>Általános beállításoknál letilthatja .", "confirm_dns_cache_clear": "Biztos benne, hogy törölni szeretné a DNS-gyorsítótárat?", "cache_cleared": "A DNS gyorsítótár sikeresen törlődött", - "clear_cache": "Gyorsítótár törlése" + "clear_cache": "Gyorsítótár törlése", + "protection_section_label": "Védelem" } diff --git a/client/src/__locales/id.json b/client/src/__locales/id.json index 85793d9a..390d3ba3 100644 --- a/client/src/__locales/id.json +++ b/client/src/__locales/id.json @@ -641,5 +641,6 @@ "anonymizer_notification": "<0>Catatan: Anonimisasi IP diaktifkan. Anda dapat menonaktifkannya di <1>Pengaturan umum .", "confirm_dns_cache_clear": "Apakah Anda yakin ingin menghapus cache DNS?", "cache_cleared": "Cache DNS berhasil dibersihkan", - "clear_cache": "Hapus cache" + "clear_cache": "Hapus cache", + "protection_section_label": "Perlindungan" } diff --git a/client/src/__locales/it.json b/client/src/__locales/it.json index b2b2c143..a42dcda6 100644 --- a/client/src/__locales/it.json +++ b/client/src/__locales/it.json @@ -257,12 +257,12 @@ "query_log_cleared": "Il registro richieste è stato correttamente cancellato", "query_log_updated": "Il registro richieste è stato correttamente aggiornato", "query_log_clear": "Cancella registri richieste", - "query_log_retention": "Conservazione dei registri richieste", + "query_log_retention": "Rotazione dei registri richieste", "query_log_enable": "Attiva registro", "query_log_configuration": "Configurazione registri", "query_log_disabled": "Il registro richieste è stato disattivato e può essere configurata dalle <0>impostazioni", "query_log_strict_search": "Utilizzare le doppie virgolette per una ricerca precisa", - "query_log_retention_confirm": "Sei sicuro di voler modificare il registro delle richieste? Se il valore di intervallo dovesse diminuire, alcuni dati andranno persi", + "query_log_retention_confirm": "Sei sicuro di voler modificare il registro delle richieste? Se si riduce il valore dell'intervallo, alcuni dati andranno persi", "anonymize_client_ip": "Anonimizza client IP", "anonymize_client_ip_desc": "Non salvare l'indirizzo IP completo del client nel registro o nelle statistiche", "dns_config": "Configurazione server DNS", @@ -668,5 +668,11 @@ "disable_notify_for_hours": "Disattiva la protezione per {{count}} ora", "disable_notify_for_hours_plural": "Disattiva la protezione per {{count}} ore", "disable_notify_until_tomorrow": "Disattiva la protezione fino a domani", - "enable_protection_timer": "La protezione verrà attivata in {{time}}" + "enable_protection_timer": "La protezione verrà attivata in {{time}}", + "custom_retention_input": "Inserisci la conservazione in ore", + "custom_rotation_input": "Inserisci la rotazione in ore", + "protection_section_label": "Protezione", + "log_and_stats_section_label": "Registro richieste e statistiche", + "ignore_query_log": "Ignora questo client nel registro delle richieste", + "ignore_statistics": "Ignora questo cliente nelle statistiche" } diff --git a/client/src/__locales/ja.json b/client/src/__locales/ja.json index 08a6f4e3..25b8942f 100644 --- a/client/src/__locales/ja.json +++ b/client/src/__locales/ja.json @@ -257,12 +257,12 @@ "query_log_cleared": "クエリ・ログの消去に成功しました", "query_log_updated": "クエリ・ログの更新が成功しました", "query_log_clear": "クエリ・ログを消去する", - "query_log_retention": "クエリ・ログの保持", + "query_log_retention": "クエリ・ログのローテーション", "query_log_enable": "ログを有効にする", "query_log_configuration": "ログ設定", "query_log_disabled": "クエリ・ログは無効になっており、<0>設定で構成できます", "query_log_strict_search": "完全一致検索には二重引用符を使用します", - "query_log_retention_confirm": "クエリ・ログの保持を変更してもよろしいですか? 期間を短くすると、一部のデータが失われます", + "query_log_retention_confirm": "クエリ・ログのローテーションを変更してもよろしいですか? 間隔の値を減らすと、一部のデータが失われます", "anonymize_client_ip": "クライアントIPを匿名化する", "anonymize_client_ip_desc": "ログと統計にクライアントのフルIPアドレスを保存しないようにします。", "dns_config": "DNSサーバ設定", @@ -668,5 +668,11 @@ "disable_notify_for_hours": "保護を {{count}} 時間無効にする", "disable_notify_for_hours_plural": "保護を {{count}} 時間無効にする", "disable_notify_until_tomorrow": "明日まで保護を無効にする", - "enable_protection_timer": "保護は後 {{time}} で有効になります" + "enable_protection_timer": "保護は後 {{time}} で有効になります", + "custom_retention_input": "保持期間を入力してください(時間単位)", + "custom_rotation_input": "ローテーションを入力してください(時間単位)", + "protection_section_label": "AdGuardによる保護", + "log_and_stats_section_label": "クエリ・ログと統計情報", + "ignore_query_log": "クエリ・ログでこのクライアントを無視する", + "ignore_statistics": "統計でこのクライアントを無視する" } diff --git a/client/src/__locales/ko.json b/client/src/__locales/ko.json index cf0028e4..057c1c4e 100644 --- a/client/src/__locales/ko.json +++ b/client/src/__locales/ko.json @@ -257,12 +257,12 @@ "query_log_cleared": "쿼리 로그를 성공적으로 초기화했습니다", "query_log_updated": "질의 로그가 성공적으로 업데이트되었습니다", "query_log_clear": "쿼리 로그 비우기", - "query_log_retention": "쿼리 로그 저장 기간", + "query_log_retention": "쿼리 로그 로테이션", "query_log_enable": "로그 활성화", "query_log_configuration": "로그 구성", "query_log_disabled": "쿼리 로그가 비활성화되어 있으며 <0>설정에서 설정할 수 있습니다", "query_log_strict_search": "검색을 제한하려면 쌍따옴표를 사용해주세요", - "query_log_retention_confirm": "정말로 쿼리 로그 저장 기간을 변경하시겠습니까? 저장 주기를 낮출 경우, 일부 데이터가 손실됩니다", + "query_log_retention_confirm": "쿼리 로그 로테이션을 변경하시겠습니까? 간격 값을 줄이면 일부 데이터가 손실됩니다.", "anonymize_client_ip": "클라이언트 IP 익명화", "anonymize_client_ip_desc": "클라이언트의 전체 IP 주소를 로그와 통계에 저장하저장하지 마세요", "dns_config": "DNS 서버 설정", @@ -668,5 +668,11 @@ "disable_notify_for_hours": "{{count}}시간 동안 보호 기능 비활성화", "disable_notify_for_hours_plural": "{{count}}시간 동안 보호 기능 비활성화", "disable_notify_until_tomorrow": "내일까지 보호 기능 비활성화", - "enable_protection_timer": "{{time}}에 보호 기능이 활성화됩니다." + "enable_protection_timer": "{{time}}에 보호 기능이 활성화됩니다.", + "custom_retention_input": "시간 단위로 보존 기간 입력", + "custom_rotation_input": "시간 단위로 로테이션 입력", + "protection_section_label": "보호", + "log_and_stats_section_label": "쿼리 로그 및 통계", + "ignore_query_log": "쿼리 로그에서 이 클라이언트 무시", + "ignore_statistics": "통계에서 이 클라이언트 무시" } diff --git a/client/src/__locales/nl.json b/client/src/__locales/nl.json index 474d9ad0..610cd9d3 100644 --- a/client/src/__locales/nl.json +++ b/client/src/__locales/nl.json @@ -257,12 +257,12 @@ "query_log_cleared": "Het query logboek is succesvol geleegd", "query_log_updated": "Het query logboek is succesvol bijgewerkt", "query_log_clear": "Leeg query logs", - "query_log_retention": "Query logs bewaartermijn", + "query_log_retention": "Query logs rotatie", "query_log_enable": "Log bestanden inschakelen", "query_log_configuration": "Logbestanden instellingen", "query_log_disabled": "Het query logboek is uitgeschakeld en kan worden geconfigureerd in de <0>instellingen", "query_log_strict_search": "Gebruik dubbele aanhalingstekens voor strikt zoeken", - "query_log_retention_confirm": "Weet u zeker dat u de bewaartermijn van het query logboek wilt wijzigen? Als u de intervalwaarde verlaagt, gaan sommige gegevens verloren", + "query_log_retention_confirm": "Weet u zeker dat u de rotatie van het querylogboek wilt wijzigen? Als u de intervalwaarde verlaagt, gaan sommige gegevens verloren", "anonymize_client_ip": "Cliënt IP anonimiseren", "anonymize_client_ip_desc": "Het volledige IP-adres van de cliënt niet opnemen in logboeken en statistiekbestanden", "dns_config": "DNS-server configuratie", @@ -668,5 +668,11 @@ "disable_notify_for_hours": "Beveiliging uitschakelen voor {{count}} uur", "disable_notify_for_hours_plural": "Beveiliging uitschakelen voor {{count}} uren", "disable_notify_until_tomorrow": "Beveiliging uitschakelen tot morgen", - "enable_protection_timer": "Bescherming wordt ingeschakeld over {{time}}" + "enable_protection_timer": "Bescherming wordt ingeschakeld over {{time}}", + "custom_retention_input": "Voer retentie in uren in", + "custom_rotation_input": "Voer rotatie in uren in", + "protection_section_label": "Bescherming", + "log_and_stats_section_label": "Aanvragenlogboek en statistieken", + "ignore_query_log": "Deze client negeren in het aanvragenlogboek", + "ignore_statistics": "Deze client negeren in de statistieken" } diff --git a/client/src/__locales/no.json b/client/src/__locales/no.json index ca218d07..3d3c91d6 100644 --- a/client/src/__locales/no.json +++ b/client/src/__locales/no.json @@ -614,5 +614,6 @@ "use_saved_key": "Bruk den tidligere lagrede nøkkelen", "parental_control": "Foreldrekontroll", "safe_browsing": "Sikker surfing", - "served_from_cache": "{{value}} (formidlet fra mellomlageret)" + "served_from_cache": "{{value}} (formidlet fra mellomlageret)", + "protection_section_label": "Beskyttelse" } diff --git a/client/src/__locales/pl.json b/client/src/__locales/pl.json index 513dfcf3..096fb012 100644 --- a/client/src/__locales/pl.json +++ b/client/src/__locales/pl.json @@ -167,6 +167,7 @@ "enabled_parental_toast": "Włączona Kontrola Rodzicielska", "disabled_safe_search_toast": "Wyłączone bezpieczne wyszukiwanie", "enabled_save_search_toast": "Włączone bezpieczne wyszukiwanie", + "updated_save_search_toast": "Zaktualizowano ustawienia bezpiecznego wyszukiwania", "enabled_table_header": "Włączone", "name_table_header": "Nazwa", "list_url_table_header": "Adres URL listy", @@ -256,12 +257,12 @@ "query_log_cleared": "Dziennik zapytań został pomyślnie wyczyszczony", "query_log_updated": "Dziennik zapytań został zaktualizowany", "query_log_clear": "Wyczyść dzienniki zapytań", - "query_log_retention": "Przechowywanie dzienników zapytań", + "query_log_retention": "Rotacja dzienników zapytań", "query_log_enable": "Włącz dziennik", "query_log_configuration": "Konfiguracja dzienników", "query_log_disabled": "Dziennik zapytań jest wyłączony i można go skonfigurować w <0>ustawieniach", "query_log_strict_search": "Używaj podwójnych cudzysłowów do ścisłego wyszukiwania", - "query_log_retention_confirm": "Czy na pewno chcesz zmienić sposób przechowywania dziennika zapytań? Jeśli zmniejszysz wartość interwału, niektóre dane zostaną utracone", + "query_log_retention_confirm": "Czy na pewno chcesz zmienić rotację dziennika zapytań? Jeśli zmniejszysz wartość interwału, niektóre dane zostaną utracone", "anonymize_client_ip": "Anonimizuj adres IP klienta", "anonymize_client_ip_desc": "Nie zapisuj pełnego adresu IP w dziennikach i statystykach", "dns_config": "Konfiguracja serwera DNS", @@ -290,6 +291,8 @@ "rate_limit": "Limit ilościowy", "edns_enable": "Włącz podsieć klienta EDNS", "edns_cs_desc": "Dodaj opcję podsieci klienta EDNS (ECS) do żądań nadrzędnych i rejestruj wartości wysyłane przez klientów w dzienniku zapytań.", + "edns_use_custom_ip": "Użyj niestandardowego adresu IP dla EDNS", + "edns_use_custom_ip_desc": "Zezwól na użycie niestandardowego adresu IP dla EDNS", "rate_limit_desc": "Liczba żądań na sekundę dozwolona na klienta. Ustawienie wartości 0 oznacza brak ograniczeń.", "blocking_ipv4_desc": "Adres IP, który ma zostać zwrócony w przypadku zablokowanego żądania A", "blocking_ipv6_desc": "Adres IP, który ma zostać zwrócony w przypadku zablokowanego żądania AAAA", @@ -523,6 +526,10 @@ "statistics_retention_confirm": "Czy chcesz zmienić sposób przechowania statystyk? Jeżeli obniżysz wartość interwału, niektóre dane będą utracone", "statistics_cleared": "Statystyki zostały pomyślnie wyczyszczone", "statistics_enable": "Włącz statystyki", + "ignore_domains": "Ignorowane domeny (każda w nowym wierszu)", + "ignore_domains_title": "Ignorowane domeny", + "ignore_domains_desc_stats": "Zapytania dla tych domen nie są zapisywane do statystyk", + "ignore_domains_desc_query": "Zapytania dla tych domen nie są zapisywane do dziennika", "interval_hours": "{{count}} godzina", "interval_hours_plural": "{{count}} godziny", "filters_configuration": "Konfiguracja filtrów", @@ -642,5 +649,30 @@ "anonymizer_notification": "<0>Uwaga: Anonimizacja IP jest włączona. Możesz ją wyłączyć w <1>Ustawieniach ogólnych.", "confirm_dns_cache_clear": "Czy na pewno chcesz wyczyścić pamięć podręczną DNS?", "cache_cleared": "Pamięć podręczna DNS została pomyślnie wyczyszczona", - "clear_cache": "Wyczyść pamięć podręczną" + "clear_cache": "Wyczyść pamięć podręczną", + "make_static": "Ustaw adres statyczny", + "theme_auto_desc": "Automatycznie (na podstawie schematu kolorów Twojego urządzenia)", + "theme_dark_desc": "Ciemny motyw", + "theme_light_desc": "Jasny motyw", + "disable_for_seconds": "Na {{count}} sekundę", + "disable_for_seconds_plural": "Na {{count}} sekund", + "disable_for_minutes": "Na {{count}} minutę", + "disable_for_minutes_plural": "Na {{count}} minut", + "disable_for_hours": "Na {{count}} godzinę", + "disable_for_hours_plural": "Na {{count}} godziny", + "disable_until_tomorrow": "Do jutra", + "disable_notify_for_seconds": "Wyłącz ochronę na {{count}} sekundę", + "disable_notify_for_seconds_plural": "Wyłącz ochronę na {{count}} sekund", + "disable_notify_for_minutes": "Wyłącz ochronę na {{count}} minutę", + "disable_notify_for_minutes_plural": "Wyłącz ochronę na {{count}} minut", + "disable_notify_for_hours": "Wyłącz ochronę na {{count}} godzinę", + "disable_notify_for_hours_plural": "Wyłącz ochronę na {{count}} godziny", + "disable_notify_until_tomorrow": "Wyłącz ochronę do jutra", + "enable_protection_timer": "Ochrona zostanie włączona za {{time}}", + "custom_retention_input": "Wprowadź retencję w godzinach", + "custom_rotation_input": "Wprowadź rotację w godzinach", + "protection_section_label": "Ochrona", + "log_and_stats_section_label": "Dziennik zapytań i statystyki", + "ignore_query_log": "Zignoruj tego klienta w dzienniku zapytań", + "ignore_statistics": "Ignoruj tego klienta w statystykach" } diff --git a/client/src/__locales/pt-br.json b/client/src/__locales/pt-br.json index 7e595612..c85234f4 100644 --- a/client/src/__locales/pt-br.json +++ b/client/src/__locales/pt-br.json @@ -257,12 +257,12 @@ "query_log_cleared": "O registro de consulta foi limpo com sucesso", "query_log_updated": "O registro da consulta foi atualizado com sucesso", "query_log_clear": "Limpar registros de consulta", - "query_log_retention": "Arquivamento de registros de consultas", + "query_log_retention": "Rotação de registros de consulta", "query_log_enable": "Ativar registro", "query_log_configuration": "Configuração de registros", "query_log_disabled": "O registro de consulta está desativado e pode ser configurado em <0>configurações", "query_log_strict_search": "Use aspas duplas para uma pesquisa mais criteriosa", - "query_log_retention_confirm": "Você tem certeza de que deseja alterar o arquivamento do registro de consulta? Se diminuir o valor de intervalo, alguns dados serão perdidos", + "query_log_retention_confirm": "Tem a certeza de que quer alterar a rotação do registo de consulta? Se diminuir o valor do intervalo, alguns dados serão perdidos", "anonymize_client_ip": "Tornar anônimo o IP do cliente", "anonymize_client_ip_desc": "Não salva o endereço de IP completo do cliente em registros ou estatísticas", "dns_config": "Configuração do servidor DNS", @@ -668,5 +668,11 @@ "disable_notify_for_hours": "Desativar proteção por {{count}} hora", "disable_notify_for_hours_plural": "Desativar proteção por {{count}} horas", "disable_notify_until_tomorrow": "Desativar a proteção até amanhã", - "enable_protection_timer": "A proteção será ativada em {{time}}" + "enable_protection_timer": "A proteção será ativada em {{time}}", + "custom_retention_input": "Insira a retenção em horas", + "custom_rotation_input": "Insira a rotação em horas", + "protection_section_label": "Proteção", + "log_and_stats_section_label": "Registro de consultas e estatísticas", + "ignore_query_log": "Ignorar este cliente no registo de consultas", + "ignore_statistics": "Ignorar este cliente nas estatísticas" } diff --git a/client/src/__locales/pt-pt.json b/client/src/__locales/pt-pt.json index 34574c3e..44eac323 100644 --- a/client/src/__locales/pt-pt.json +++ b/client/src/__locales/pt-pt.json @@ -257,12 +257,12 @@ "query_log_cleared": "O registo de consulta foi limpo com sucesso", "query_log_updated": "O registo da consulta foi atualizado com sucesso", "query_log_clear": "Limpar registos de consulta", - "query_log_retention": "Retenção de registos de consulta", + "query_log_retention": "Rotação de registros de consulta", "query_log_enable": "Ativar registo", "query_log_configuration": "Definições do registo", "query_log_disabled": "O registo de consulta está desativado e pode ser configurado em <0>definições", "query_log_strict_search": "Usar aspas duplas para uma pesquisa rigorosa", - "query_log_retention_confirm": "Tem a certeza de que deseja alterar a retenção do registo de consulta? Se diminuir o valor do intervalo, alguns dados serão perdidos", + "query_log_retention_confirm": "Tem a certeza de que quer alterar a rotação do registo de consulta? Se diminuir o valor do intervalo, alguns dados serão perdidos", "anonymize_client_ip": "Tornar anónimo o IP do cliente", "anonymize_client_ip_desc": "Não gurda o endereço de IP completo do cliente em registo ou estatísticas", "dns_config": "Definição do servidor DNS", @@ -668,5 +668,11 @@ "disable_notify_for_hours": "Desativar proteção por {{count}} hora", "disable_notify_for_hours_plural": "Desativar proteção por {{count}} horas", "disable_notify_until_tomorrow": "Desativar a proteção até amanhã", - "enable_protection_timer": "A proteção será habilitada em {{time}}" + "enable_protection_timer": "A proteção será habilitada em {{time}}", + "custom_retention_input": "Insira a retenção em horas", + "custom_rotation_input": "Insira a rotação em horas", + "protection_section_label": "Proteção", + "log_and_stats_section_label": "Log de consulta e estatísticas", + "ignore_query_log": "Ignorar este cliente no log de consulta", + "ignore_statistics": "Ignorar este cliente nas estatísticas" } diff --git a/client/src/__locales/ro.json b/client/src/__locales/ro.json index 69f7281c..7c31db97 100644 --- a/client/src/__locales/ro.json +++ b/client/src/__locales/ro.json @@ -642,5 +642,6 @@ "anonymizer_notification": "<0>Nota: Anonimizarea IP este activată. Puteți să o dezactivați în <1>Setări generale.", "confirm_dns_cache_clear": "Sunteți sigur că doriți să ștergeți memoria cache DNS?", "cache_cleared": "Cache-ul DNS a fost golit cu succes", - "clear_cache": "Goliți memoria cache" + "clear_cache": "Goliți memoria cache", + "protection_section_label": "Protecție" } diff --git a/client/src/__locales/ru.json b/client/src/__locales/ru.json index aa78f3ae..00537670 100644 --- a/client/src/__locales/ru.json +++ b/client/src/__locales/ru.json @@ -257,12 +257,12 @@ "query_log_cleared": "Журнал запросов успешно очищен", "query_log_updated": "Журнал запросов успешно обновлён", "query_log_clear": "Очистить журнал запросов", - "query_log_retention": "Сохранение журнала запросов", + "query_log_retention": "Частота ротации журнала запросов", "query_log_enable": "Включить журнал", "query_log_configuration": "Настройка журнала", "query_log_disabled": "Журнал запросов выключен, его можно включить в <0>настройках", "query_log_strict_search": "Используйте двойные кавычки для строгого поиска", - "query_log_retention_confirm": "Вы уверены, что хотите изменить срок хранения запросов? При сокращении интервала данные могут быть утеряны", + "query_log_retention_confirm": "Вы уверены, что хотите изменить частоту ротации журнала запросов? При сокращении срока данные могут быть утеряны", "anonymize_client_ip": "Анонимизировать IP-адрес клиента", "anonymize_client_ip_desc": "Не сохранять полный IP-адрес клиента в журналах и статистике", "dns_config": "Настройки DNS-сервера", @@ -668,5 +668,11 @@ "disable_notify_for_hours": "Отключить защиту на {{count}} час", "disable_notify_for_hours_plural": "Отключить защиту на {{count}} часов", "disable_notify_until_tomorrow": "Отключить защиту до завтра", - "enable_protection_timer": "Защита будет включена в {{time}}" + "enable_protection_timer": "Защита будет включена в {{time}}", + "custom_retention_input": "Введите срок хранения в часах", + "custom_rotation_input": "Введите частоту ротации в часах", + "protection_section_label": "Защита", + "log_and_stats_section_label": "Журнал запросов и статистика", + "ignore_query_log": "Игнорировать этого клиента в журнале запросов", + "ignore_statistics": "Игнорировать этого клиента в статистике" } diff --git a/client/src/__locales/sk.json b/client/src/__locales/sk.json index d98a9f53..f2ee71ab 100644 --- a/client/src/__locales/sk.json +++ b/client/src/__locales/sk.json @@ -257,12 +257,12 @@ "query_log_cleared": "Denník dopytov bol úspešne vymazaný", "query_log_updated": "Denník dopytov bol úspešne aktualizovaný", "query_log_clear": "Vymazať denníky dopytov", - "query_log_retention": "Obdobie záznamu denníka dopytov", + "query_log_retention": "Rotácia denníkov dopytov", "query_log_enable": "Zapnúť denník", "query_log_configuration": "Konfigurácia denníka", "query_log_disabled": "Protokol dopytov je vypnutý a možno ho nakonfigurovať v <0>nastaveniach", "query_log_strict_search": "Na prísne vyhľadávanie použite dvojité úvodzovky", - "query_log_retention_confirm": "Naozaj chcete zmeniť uchovávanie denníku dopytov? Ak znížite hodnotu intervalu, niektoré údaje sa stratia", + "query_log_retention_confirm": "Naozaj chcete zmeniť rotáciu denníka dopytov? Ak znížite hodnotu intervalu, niektoré údaje sa stratia", "anonymize_client_ip": "Anonymizujte IP klienta", "anonymize_client_ip_desc": "Neukladať úplnú IP adresu klienta do protokolov a štatistík", "dns_config": "Konfigurácia DNS servera", @@ -668,5 +668,11 @@ "disable_notify_for_hours": "Vypnite ochranu na {{count}} hodinu", "disable_notify_for_hours_plural": "Vypnite ochranu na {{count}} hodín", "disable_notify_until_tomorrow": "Vypnúť ochranu do zajtra", - "enable_protection_timer": "Ochrana bude zapnutá o {{time}}" + "enable_protection_timer": "Ochrana bude zapnutá o {{time}}", + "custom_retention_input": "Zadajte retenciu v hodinách", + "custom_rotation_input": "Zadajte rotáciu v hodinách", + "protection_section_label": "Ochrana", + "log_and_stats_section_label": "Protokol dopytov a štatistiky", + "ignore_query_log": "Ignorovať tohto klienta v denníku dopytov", + "ignore_statistics": "Ignorovanie tohto klienta v štatistikách" } diff --git a/client/src/__locales/sl.json b/client/src/__locales/sl.json index a46256c2..a6759019 100644 --- a/client/src/__locales/sl.json +++ b/client/src/__locales/sl.json @@ -257,12 +257,12 @@ "query_log_cleared": "Dnevnik poizvedb je uspešno izbrisan", "query_log_updated": "Dnevnik poizvedb je bil uspešno posodobljen", "query_log_clear": "Počisti dnevnike poizvedb", - "query_log_retention": "Zadrževanje dnevnikov poizvedb", + "query_log_retention": "Rotacija dnevnikov poizvedb", "query_log_enable": "Omogoči dnevni", "query_log_configuration": "Konfiguracija dnevnikov", "query_log_disabled": "Dnevnik poizvedb je onemogočen in ga je mogoče konfigurirati v <0>nastavitvah", "query_log_strict_search": "Za strogo iskanje uporabite dvojne narekovaje", - "query_log_retention_confirm": "Ali ste prepričani, da želite spremeniti zadrževanje dnevnika poizvedb? Če zmanjšate vrednost intervala, bodo nekateri podatki izgubljeni", + "query_log_retention_confirm": "Ali ste prepričani, da želite spremeniti rotacijo dnevnika poizvedb? Če zmanjšate vrednost intervala, bodo nekateri podatki izgubljeni", "anonymize_client_ip": "Anonimiziraj odjemalca IP", "anonymize_client_ip_desc": "Ne shrani celotnega naslova IP odjemalca v dnevnikih ali statistiki", "dns_config": "Konfiguracija strežnika DNS", @@ -668,5 +668,11 @@ "disable_notify_for_hours": "Onemogoči zaščito za {{count}} uro", "disable_notify_for_hours_plural": "Onemogoči zaščito za {{count}} ur", "disable_notify_until_tomorrow": "Onemogoči zaščito do jutri", - "enable_protection_timer": "Zaščita bo omogočena ob {{time}}" + "enable_protection_timer": "Zaščita bo omogočena ob {{time}}", + "custom_retention_input": "Vnesite zadrževanje v urah", + "custom_rotation_input": "Vnesite rotacijo v urah", + "protection_section_label": "Zaščita", + "log_and_stats_section_label": "Dnevnik poizvedb in statistika", + "ignore_query_log": "Ignorirajte tega odjemalca v dnevniku poizvedb", + "ignore_statistics": "Ignoriranje tega odjemalca v statistiki" } diff --git a/client/src/__locales/sr-cs.json b/client/src/__locales/sr-cs.json index 637f64cd..a5162c4e 100644 --- a/client/src/__locales/sr-cs.json +++ b/client/src/__locales/sr-cs.json @@ -642,5 +642,6 @@ "anonymizer_notification": "<0>Nota: IP prepoznavanje je omogućeno. Možete ga onemogućiti u opštim <1>postavkama.", "confirm_dns_cache_clear": "Želite li zaista da obrišite DNS keš?", "cache_cleared": "DNS keš je uspešno očišćen", - "clear_cache": "Obriši keš memoriju" + "clear_cache": "Obriši keš memoriju", + "protection_section_label": "Zaštita" } diff --git a/client/src/__locales/tr.json b/client/src/__locales/tr.json index bb83a711..12611b2d 100644 --- a/client/src/__locales/tr.json +++ b/client/src/__locales/tr.json @@ -257,12 +257,12 @@ "query_log_cleared": "Sorgu günlüğü başarıyla temizlendi", "query_log_updated": "Sorgu günlüğü başarıyla güncellendi", "query_log_clear": "Sorgu günlüklerini temizle", - "query_log_retention": "Sorgu günlüklerini sakla", + "query_log_retention": "Sorgu günlükleri rotasyonu", "query_log_enable": "Günlüğü etkinleştir", "query_log_configuration": "Günlük yapılandırması", "query_log_disabled": "Sorgu günlüğü devre dışı bırakıldı, bunu <0>ayarlar kısmından yapılandırılabilirsiniz", "query_log_strict_search": "Tam arama için çift tırnak işareti kullanın", - "query_log_retention_confirm": "Sorgu günlüğü saklama süresini değiştirmek istediğinize emin misiniz? Aralık değerini azaltırsanız, bazı veriler kaybolacaktır", + "query_log_retention_confirm": "Sorgu günlüğü rotasyonunu değiştirmek istediğinizden emin misiniz? Aralık değerini düşürürseniz, bazı veriler kaybolacaktır.", "anonymize_client_ip": "İstemcinin IP adresini gizle", "anonymize_client_ip_desc": "İstemcinin tam IP adresini günlüklere veya istatistiklere kaydetmeyin", "dns_config": "DNS sunucu yapılandırması", @@ -668,5 +668,11 @@ "disable_notify_for_hours": "Korumayı {{count}} saatliğine devre dışı bırak", "disable_notify_for_hours_plural": "Korumayı {{count}} saatliğine devre dışı bırak", "disable_notify_until_tomorrow": "Korumayı yarına kadar devre dışı bırak", - "enable_protection_timer": "Koruma {{time}} içinde etkinleştirilecektir" + "enable_protection_timer": "Koruma {{time}} içinde etkinleştirilecektir", + "custom_retention_input": "Saklama süresini saat olarak girin", + "custom_rotation_input": "Rotasyonu saat cinsinden girin", + "protection_section_label": "Koruma", + "log_and_stats_section_label": "Sorgu günlüğü ve istatistikler", + "ignore_query_log": "Sorgu günlüğünde bu istemciyi yoksay", + "ignore_statistics": "İstatistiklerde bu istemciyi yoksay" } diff --git a/client/src/__locales/uk.json b/client/src/__locales/uk.json index 1df2b457..9b8fae71 100644 --- a/client/src/__locales/uk.json +++ b/client/src/__locales/uk.json @@ -642,5 +642,6 @@ "anonymizer_notification": "<0>Примітка: IP-анонімізацію ввімкнено. Ви можете вимкнути його в <1>Загальні налаштування .", "confirm_dns_cache_clear": "Ви впевнені, що бажаєте очистити кеш DNS?", "cache_cleared": "Кеш DNS успішно очищено", - "clear_cache": "Очистити кеш" + "clear_cache": "Очистити кеш", + "protection_section_label": "Захист" } diff --git a/client/src/__locales/vi.json b/client/src/__locales/vi.json index 0bba7a84..1f560612 100644 --- a/client/src/__locales/vi.json +++ b/client/src/__locales/vi.json @@ -642,5 +642,6 @@ "anonymizer_notification": "<0> Lưu ý: Tính năng ẩn danh IP được bật. Bạn có thể tắt nó trong <1> Cài đặt chung.", "confirm_dns_cache_clear": "Bạn có chắc chắn muốn xóa bộ đệm ẩn DNS không?", "cache_cleared": "Đã xóa thành công bộ đệm DNS", - "clear_cache": "Xóa bộ nhớ cache" + "clear_cache": "Xóa bộ nhớ cache", + "protection_section_label": "Sự bảo vệ" } diff --git a/client/src/__locales/zh-cn.json b/client/src/__locales/zh-cn.json index e0235dae..5c8f2b2e 100644 --- a/client/src/__locales/zh-cn.json +++ b/client/src/__locales/zh-cn.json @@ -257,12 +257,12 @@ "query_log_cleared": "查询日志已成功清除", "query_log_updated": "已成功更新查询日志", "query_log_clear": "清除查询日志", - "query_log_retention": "查询记录保留时间", + "query_log_retention": "查询日志保留时间", "query_log_enable": "启用日志", "query_log_configuration": "日志配置", "query_log_disabled": "查询日志已禁用,在<0>这些设置中能配置它们", "query_log_strict_search": "使用双引号进行严谨搜索", - "query_log_retention_confirm": "您确定要更改查询记录保留时间吗? 如果您减少间隔时间的值, 某些数据可能会丢失。", + "query_log_retention_confirm": "您确定要更改查询记录保留时间吗?如果减少时间间隔数值,某些数据可能会丢失", "anonymize_client_ip": "匿名化客户端IP", "anonymize_client_ip_desc": "不要在日志和统计信息中保存客户端的完整 IP 地址", "dns_config": "DNS 服务配置", @@ -668,5 +668,11 @@ "disable_notify_for_hours": "禁用保护 {{count}} 小时", "disable_notify_for_hours_plural": "禁用保护 {{count}} 小时", "disable_notify_until_tomorrow": "禁用保护直到明天", - "enable_protection_timer": "保护将于 {{time}} 启用" + "enable_protection_timer": "保护将于 {{time}} 启用", + "custom_retention_input": "输入保留时间(小时)", + "custom_rotation_input": "输入旋转时间(小时)", + "protection_section_label": "防护", + "log_and_stats_section_label": "查询日志和统计数据", + "ignore_query_log": "在查询日志中忽略此客户端", + "ignore_statistics": "在统计数据中忽略此客户端" } diff --git a/client/src/__locales/zh-tw.json b/client/src/__locales/zh-tw.json index 80b156ff..309e71c1 100644 --- a/client/src/__locales/zh-tw.json +++ b/client/src/__locales/zh-tw.json @@ -257,12 +257,12 @@ "query_log_cleared": "該查詢記錄已被成功地清除", "query_log_updated": "該查詢記錄已被成功地更新", "query_log_clear": "清除查詢記錄", - "query_log_retention": "查詢記錄保留", + "query_log_retention": "查詢記錄保留時間", "query_log_enable": "啟用記錄", "query_log_configuration": "記錄配置", "query_log_disabled": "查詢記錄被禁用並可在<0>設定中被配置", "query_log_strict_search": "使用雙引號於嚴謹的搜尋", - "query_log_retention_confirm": "您確定您想要更改查詢記錄保留嗎?如果您減少該間隔值,某些資料將被丟失", + "query_log_retention_confirm": "您確定要更改記錄檔保存期限嗎?如果您縮短期限部分資料可能將會遺失", "anonymize_client_ip": "將用戶端 IP 匿名", "anonymize_client_ip_desc": "不要儲存用戶端之完整的 IP 位址到記錄或統計資料裡", "dns_config": "DNS 伺服器配置", @@ -668,5 +668,11 @@ "disable_notify_for_hours": "計 {{count}} 小時禁用防護", "disable_notify_for_hours_plural": "計 {{count}} 小時禁用防護", "disable_notify_until_tomorrow": "禁用防護直到明天", - "enable_protection_timer": "防護將於 {{time}} 被啟用" + "enable_protection_timer": "防護將於 {{time}} 被啟用", + "custom_retention_input": "輸入保留時間(小時)", + "custom_rotation_input": "輸入旋轉時間(小時)", + "protection_section_label": "防護", + "log_and_stats_section_label": "查詢記錄和統計資料", + "ignore_query_log": "在查詢記錄中忽略此用戶端", + "ignore_statistics": "在統計資料中忽略此用戶端" } diff --git a/client/src/actions/services.js b/client/src/actions/services.js index f360081e..2f5f20db 100644 --- a/client/src/actions/services.js +++ b/client/src/actions/services.js @@ -2,21 +2,6 @@ import { createAction } from 'redux-actions'; import apiClient from '../api/Api'; import { addErrorToast, addSuccessToast } from './toasts'; -export const getBlockedServicesAvailableServicesRequest = createAction('GET_BLOCKED_SERVICES_AVAILABLE_SERVICES_REQUEST'); -export const getBlockedServicesAvailableServicesFailure = createAction('GET_BLOCKED_SERVICES_AVAILABLE_SERVICES_FAILURE'); -export const getBlockedServicesAvailableServicesSuccess = createAction('GET_BLOCKED_SERVICES_AVAILABLE_SERVICES_SUCCESS'); - -export const getBlockedServicesAvailableServices = () => async (dispatch) => { - dispatch(getBlockedServicesAvailableServicesRequest()); - try { - const data = await apiClient.getBlockedServicesAvailableServices(); - dispatch(getBlockedServicesAvailableServicesSuccess(data)); - } catch (error) { - dispatch(addErrorToast({ error })); - dispatch(getBlockedServicesAvailableServicesFailure()); - } -}; - export const getBlockedServicesRequest = createAction('GET_BLOCKED_SERVICES_REQUEST'); export const getBlockedServicesFailure = createAction('GET_BLOCKED_SERVICES_FAILURE'); export const getBlockedServicesSuccess = createAction('GET_BLOCKED_SERVICES_SUCCESS'); diff --git a/client/src/api/Api.js b/client/src/api/Api.js index 7ca33293..30c2db1d 100644 --- a/client/src/api/Api.js +++ b/client/src/api/Api.js @@ -479,19 +479,12 @@ class Api { } // Blocked services - BLOCKED_SERVICES_SERVICES = { path: 'blocked_services/services', method: 'GET' }; - BLOCKED_SERVICES_LIST = { path: 'blocked_services/list', method: 'GET' }; BLOCKED_SERVICES_SET = { path: 'blocked_services/set', method: 'POST' }; BLOCKED_SERVICES_ALL = { path: 'blocked_services/all', method: 'GET' }; - getBlockedServicesAvailableServices() { - const { path, method } = this.BLOCKED_SERVICES_SERVICES; - return this.makeRequest(path, method); - } - getAllBlockedServices() { const { path, method } = this.BLOCKED_SERVICES_ALL; return this.makeRequest(path, method); diff --git a/client/src/components/Settings/Clients/Form.js b/client/src/components/Settings/Clients/Form.js index 6e5763e6..190996e4 100644 --- a/client/src/components/Settings/Clients/Form.js +++ b/client/src/components/Settings/Clients/Form.js @@ -41,6 +41,17 @@ const settingsCheckboxes = [ placeholder: 'use_adguard_parental', }, ]; + +const logAndStatsCheckboxes = [ + { + name: 'ignore_querylog', + placeholder: 'ignore_query_log', + }, + { + name: 'ignore_statistics', + placeholder: 'ignore_statistics', + }, +]; const validate = (values) => { const errors = {}; const { name, ids } = values; @@ -148,6 +159,9 @@ let Form = (props) => { settings: { title: 'settings', component:
+
+ {t('protection_section_label')} +
{settingsCheckboxes.map((setting) => (
{
))}
+
+ {t('log_and_stats_section_label')} +
+ {logAndStatsCheckboxes.map((setting) => ( +
+ +
+ ))} , }, block_services: { diff --git a/client/src/components/Settings/LogsConfig/Form.js b/client/src/components/Settings/LogsConfig/Form.js index b29b974e..dbecc183 100644 --- a/client/src/components/Settings/LogsConfig/Form.js +++ b/client/src/components/Settings/LogsConfig/Form.js @@ -1,25 +1,37 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; -import { Field, reduxForm } from 'redux-form'; +import { + change, + Field, + formValueSelector, + reduxForm, +} from 'redux-form'; +import { connect } from 'react-redux'; import { Trans, withTranslation } from 'react-i18next'; import flow from 'lodash/flow'; import { CheckboxField, - renderRadioField, toFloatNumber, - renderTextareaField, + renderTextareaField, renderInputField, renderRadioField, } from '../../../helpers/form'; import { FORM_NAME, QUERY_LOG_INTERVALS_DAYS, HOUR, DAY, + RETENTION_CUSTOM, + RETENTION_CUSTOM_INPUT, + RETENTION_RANGE, + CUSTOM_INTERVAL, } from '../../../helpers/constants'; import '../FormButton.css'; + const getIntervalTitle = (interval, t) => { switch (interval) { + case RETENTION_CUSTOM: + return t('settings_custom'); case 6 * HOUR: return t('interval_6_hour'); case DAY: @@ -42,11 +54,26 @@ const getIntervalFields = (processing, t, toNumber) => QUERY_LOG_INTERVALS_DAYS. /> )); -const Form = (props) => { +let Form = (props) => { const { - handleSubmit, submitting, invalid, processing, processingClear, handleClear, t, + handleSubmit, + submitting, + invalid, + processing, + processingClear, + handleClear, + t, + interval, + customInterval, + dispatch, } = props; + useEffect(() => { + if (QUERY_LOG_INTERVALS_DAYS.includes(interval)) { + dispatch(change(FORM_NAME.LOG_CONFIG, CUSTOM_INTERVAL, null)); + } + }, [interval]); + return (
@@ -73,6 +100,37 @@ const Form = (props) => {
+ + {!QUERY_LOG_INTERVALS_DAYS.includes(interval) && ( +
+
+ {t('custom_rotation_input')} +
+ +
+ )} {getIntervalFields(processing, t, toFloatNumber)}
@@ -96,7 +154,12 @@ const Form = (props) => { @@ -121,8 +184,22 @@ Form.propTypes = { processing: PropTypes.bool.isRequired, processingClear: PropTypes.bool.isRequired, t: PropTypes.func.isRequired, + interval: PropTypes.number, + customInterval: PropTypes.number, + dispatch: PropTypes.func.isRequired, }; +const selector = formValueSelector(FORM_NAME.LOG_CONFIG); + +Form = connect((state) => { + const interval = selector(state, 'interval'); + const customInterval = selector(state, CUSTOM_INTERVAL); + return { + interval, + customInterval, + }; +})(Form); + export default flow([ withTranslation(), reduxForm({ form: FORM_NAME.LOG_CONFIG }), diff --git a/client/src/components/Settings/LogsConfig/index.js b/client/src/components/Settings/LogsConfig/index.js index 146a77b0..3e609a2d 100644 --- a/client/src/components/Settings/LogsConfig/index.js +++ b/client/src/components/Settings/LogsConfig/index.js @@ -4,15 +4,22 @@ import { withTranslation } from 'react-i18next'; import Card from '../../ui/Card'; import Form from './Form'; +import { HOUR } from '../../../helpers/constants'; class LogsConfig extends Component { handleFormSubmit = (values) => { const { t, interval: prevInterval } = this.props; - const { interval } = values; + const { interval, customInterval, ...rest } = values; - const data = { ...values, ignored: values.ignored ? values.ignored.split('\n') : [] }; + const newInterval = customInterval ? customInterval * HOUR : interval; - if (interval !== prevInterval) { + const data = { + ...rest, + ignored: values.ignored ? values.ignored.split('\n') : [], + interval: newInterval, + }; + + if (newInterval < prevInterval) { // eslint-disable-next-line no-alert if (window.confirm(t('query_log_retention_confirm'))) { this.props.setLogsConfig(data); @@ -32,7 +39,14 @@ class LogsConfig extends Component { render() { const { - t, enabled, interval, processing, processingClear, anonymize_client_ip, ignored, + t, + enabled, + interval, + processing, + processingClear, + anonymize_client_ip, + ignored, + customInterval, } = this.props; return ( @@ -46,6 +60,7 @@ class LogsConfig extends Component { initialValues={{ enabled, interval, + customInterval, anonymize_client_ip, ignored: ignored.join('\n'), }} @@ -62,6 +77,7 @@ class LogsConfig extends Component { LogsConfig.propTypes = { interval: PropTypes.number.isRequired, + customInterval: PropTypes.number, enabled: PropTypes.bool.isRequired, anonymize_client_ip: PropTypes.bool.isRequired, processing: PropTypes.bool.isRequired, diff --git a/client/src/components/Settings/Settings.css b/client/src/components/Settings/Settings.css index 0db0fb65..25532b45 100644 --- a/client/src/components/Settings/Settings.css +++ b/client/src/components/Settings/Settings.css @@ -18,6 +18,11 @@ font-size: 14px; } +.form__group--input { + max-width: 300px; + margin: 0 1.5rem 10px; +} + .form__group--checkbox { margin-bottom: 25px; } @@ -100,6 +105,14 @@ margin-bottom: 0; } +.form__label--bot { + margin-bottom: 10px; +} + +.form__label--top { + margin-top: 10px; +} + .form__status { margin-top: 10px; font-size: 14px; diff --git a/client/src/components/Settings/StatsConfig/Form.js b/client/src/components/Settings/StatsConfig/Form.js index e9cd02fd..087e9578 100644 --- a/client/src/components/Settings/StatsConfig/Form.js +++ b/client/src/components/Settings/StatsConfig/Form.js @@ -1,32 +1,44 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; -import { Field, reduxForm } from 'redux-form'; +import { + change, Field, formValueSelector, reduxForm, +} from 'redux-form'; import { Trans, withTranslation } from 'react-i18next'; import flow from 'lodash/flow'; +import { connect } from 'react-redux'; + import { renderRadioField, toNumber, CheckboxField, renderTextareaField, + toFloatNumber, + renderInputField, } from '../../../helpers/form'; import { FORM_NAME, STATS_INTERVALS_DAYS, DAY, + RETENTION_CUSTOM, + RETENTION_CUSTOM_INPUT, + CUSTOM_INTERVAL, + RETENTION_RANGE, } from '../../../helpers/constants'; import '../FormButton.css'; const getIntervalTitle = (intervalMs, t) => { - switch (intervalMs / DAY) { - case 1: + switch (intervalMs) { + case RETENTION_CUSTOM: + return t('settings_custom'); + case DAY: return t('interval_24_hour'); default: return t('interval_days', { count: intervalMs / DAY }); } }; -const Form = (props) => { +let Form = (props) => { const { handleSubmit, processing, @@ -35,8 +47,17 @@ const Form = (props) => { handleReset, processingReset, t, + interval, + customInterval, + dispatch, } = props; + useEffect(() => { + if (STATS_INTERVALS_DAYS.includes(interval)) { + dispatch(change(FORM_NAME.STATS_CONFIG, CUSTOM_INTERVAL, null)); + } + }, [interval]); + return (
@@ -56,6 +77,37 @@ const Form = (props) => {
+ + {!STATS_INTERVALS_DAYS.includes(interval) && ( +
+
+ {t('custom_retention_input')} +
+ +
+ )} {STATS_INTERVALS_DAYS.map((interval) => ( { @@ -116,8 +173,22 @@ Form.propTypes = { processing: PropTypes.bool.isRequired, processingReset: PropTypes.bool.isRequired, t: PropTypes.func.isRequired, + interval: PropTypes.number, + customInterval: PropTypes.number, + dispatch: PropTypes.func.isRequired, }; +const selector = formValueSelector(FORM_NAME.STATS_CONFIG); + +Form = connect((state) => { + const interval = selector(state, 'interval'); + const customInterval = selector(state, CUSTOM_INTERVAL); + return { + interval, + customInterval, + }; +})(Form); + export default flow([ withTranslation(), reduxForm({ form: FORM_NAME.STATS_CONFIG }), diff --git a/client/src/components/Settings/StatsConfig/index.js b/client/src/components/Settings/StatsConfig/index.js index 83807afa..7b68064c 100644 --- a/client/src/components/Settings/StatsConfig/index.js +++ b/client/src/components/Settings/StatsConfig/index.js @@ -4,13 +4,18 @@ import { withTranslation } from 'react-i18next'; import Card from '../../ui/Card'; import Form from './Form'; +import { HOUR } from '../../../helpers/constants'; class StatsConfig extends Component { - handleFormSubmit = ({ enabled, interval, ignored }) => { + handleFormSubmit = ({ + enabled, interval, ignored, customInterval, + }) => { const { t, interval: prevInterval } = this.props; + const newInterval = customInterval ? customInterval * HOUR : interval; + const config = { enabled, - interval, + interval: newInterval, ignored: ignored ? ignored.split('\n') : [], }; @@ -33,7 +38,13 @@ class StatsConfig extends Component { render() { const { - t, interval, processing, processingReset, ignored, enabled, + t, + interval, + customInterval, + processing, + processingReset, + ignored, + enabled, } = this.props; return ( @@ -46,6 +57,7 @@ class StatsConfig extends Component { { const isLoggedIn = profileName !== ''; const [currentThemeLocal, setCurrentThemeLocal] = useState(THEMES.auto); - useEffect(() => { - if (!isLoggedIn) { - setUITheme(currentThemeLocal); - } - }, []); - const getYear = () => { const today = new Date(); return today.getFullYear(); diff --git a/client/src/components/ui/Overlay.css b/client/src/components/ui/Overlay.css index d12a55b7..6941af52 100644 --- a/client/src/components/ui/Overlay.css +++ b/client/src/components/ui/Overlay.css @@ -13,7 +13,7 @@ font-size: 28px; font-weight: 600; text-align: center; - background-color: rgba(255, 255, 255, 0.8); + background-color: var(--rt-nodata-bgcolor); } .overlay--visible { diff --git a/client/src/helpers/constants.js b/client/src/helpers/constants.js index 98bc99e2..1a3182c5 100644 --- a/client/src/helpers/constants.js +++ b/client/src/helpers/constants.js @@ -220,6 +220,12 @@ export const STATS_INTERVALS_DAYS = [DAY, DAY * 7, DAY * 30, DAY * 90]; export const QUERY_LOG_INTERVALS_DAYS = [HOUR * 6, DAY, DAY * 7, DAY * 30, DAY * 90]; +export const RETENTION_CUSTOM = 1; + +export const RETENTION_CUSTOM_INPUT = 'custom_retention_input'; + +export const CUSTOM_INTERVAL = 'customInterval'; + export const FILTERS_INTERVALS_HOURS = [0, 1, 12, 24, 72, 168]; // Note that translation strings contain these modes (blocking_mode_CONSTANT) @@ -462,6 +468,11 @@ export const UINT32_RANGE = { MAX: 4294967295, }; +export const RETENTION_RANGE = { + MIN: 1, + MAX: 365 * 24, +}; + export const DHCP_VALUES_PLACEHOLDERS = { ipv4: { subnet_mask: '255.255.255.0', @@ -537,3 +548,5 @@ export const DISABLE_PROTECTION_TIMINGS = { HOUR: 60 * 60 * 1000, TOMORROW: 24 * 60 * 60 * 1000, }; + +export const LOCAL_STORAGE_THEME_KEY = 'account_theme'; diff --git a/client/src/helpers/helpers.js b/client/src/helpers/helpers.js index 3c1c606f..127b2902 100644 --- a/client/src/helpers/helpers.js +++ b/client/src/helpers/helpers.js @@ -26,6 +26,7 @@ import { STANDARD_WEB_PORT, SPECIAL_FILTER_ID, THEMES, + LOCAL_STORAGE_THEME_KEY, } from './constants'; /** @@ -679,19 +680,60 @@ export const setHtmlLangAttr = (language) => { window.document.documentElement.lang = language; }; +/** + * Set local storage field + * + * @param {string} key + * @param {string} value + */ + +export const setStorageItem = (key, value) => { + if (window.localStorage) { + window.localStorage.setItem(key, value); + } +}; + +/** + * Get local storage field + * + * @param {string} key + */ + +export const getStorageItem = (key) => (window.localStorage + ? window.localStorage.getItem(key) + : null); + +/** + * Set local storage theme field + * + * @param {string} theme + */ + +export const setTheme = (theme) => { + setStorageItem(LOCAL_STORAGE_THEME_KEY, theme); +}; + +/** + * Get local storage theme field + * + * @returns {string} + */ + +export const getTheme = () => getStorageItem(LOCAL_STORAGE_THEME_KEY) || THEMES.light; + /** * Sets UI theme. * * @param theme */ export const setUITheme = (theme) => { - let currentTheme = theme; + let currentTheme = theme || getTheme(); if (currentTheme === THEMES.auto) { const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; currentTheme = prefersDark ? THEMES.dark : THEMES.light; } - + setTheme(currentTheme); document.body.dataset.theme = currentTheme; }; diff --git a/client/src/reducers/dashboard.js b/client/src/reducers/dashboard.js index d8b48766..f23717f9 100644 --- a/client/src/reducers/dashboard.js +++ b/client/src/reducers/dashboard.js @@ -177,7 +177,7 @@ const dashboard = handleActions( autoClients: [], supportedTags: [], name: '', - theme: 'auto', + theme: undefined, checkUpdateFlag: false, }, ); diff --git a/client/src/reducers/queryLogs.js b/client/src/reducers/queryLogs.js index 26d47025..89cc0041 100644 --- a/client/src/reducers/queryLogs.js +++ b/client/src/reducers/queryLogs.js @@ -1,7 +1,9 @@ import { handleActions } from 'redux-actions'; import * as actions from '../actions/queryLogs'; -import { DEFAULT_LOGS_FILTER, DAY } from '../helpers/constants'; +import { + DEFAULT_LOGS_FILTER, DAY, QUERY_LOG_INTERVALS_DAYS, HOUR, +} from '../helpers/constants'; const queryLogs = handleActions( { @@ -59,6 +61,9 @@ const queryLogs = handleActions( [actions.getLogsConfigSuccess]: (state, { payload }) => ({ ...state, ...payload, + customInterval: !QUERY_LOG_INTERVALS_DAYS.includes(payload.interval) + ? payload.interval / HOUR + : null, processingGetConfig: false, }), @@ -95,6 +100,7 @@ const queryLogs = handleActions( anonymize_client_ip: false, isDetailed: true, isEntireLog: false, + customInterval: null, }, ); diff --git a/client/src/reducers/stats.js b/client/src/reducers/stats.js index 2e5a7e48..a1c63e14 100644 --- a/client/src/reducers/stats.js +++ b/client/src/reducers/stats.js @@ -1,6 +1,6 @@ import { handleActions } from 'redux-actions'; import { normalizeTopClients } from '../helpers/helpers'; -import { DAY } from '../helpers/constants'; +import { DAY, HOUR, STATS_INTERVALS_DAYS } from '../helpers/constants'; import * as actions from '../actions/stats'; @@ -27,6 +27,9 @@ const stats = handleActions( [actions.getStatsConfigSuccess]: (state, { payload }) => ({ ...state, ...payload, + customInterval: !STATS_INTERVALS_DAYS.includes(payload.interval) + ? payload.interval / HOUR + : null, processingGetConfig: false, }), @@ -93,6 +96,7 @@ const stats = handleActions( processingStats: true, processingReset: false, interval: DAY, + customInterval: null, ...defaultStats, }, ); diff --git a/docker/dns-bind.awk b/docker/dns-bind.awk index 4580638e..4173614c 100644 --- a/docker/dns-bind.awk +++ b/docker/dns-bind.awk @@ -4,19 +4,27 @@ /^[[:space:]]+- .+/ { if (FNR - prev_line == 1) { - addrs[addrsnum++] = $2 + addrs[$2] = true prev_line = FNR + + if ($2 == "0.0.0.0" || $2 == "::") { + delete addrs + addrs["localhost"] = true + + # Drop all the other addresses. + prev_line = -1 + } } } /^[[:space:]]+port:/ { if (is_dns) port = $2 } END { - for (i in addrs) { - if (match(addrs[i], ":")) { - print "[" addrs[i] "]:" port + for (addr in addrs) { + if (match(addr, ":")) { + print "[" addr "]:" port } else { - print addrs[i] ":" port + print addr ":" port } } } diff --git a/go.mod b/go.mod index 9af8eced..911624bf 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/AdguardTeam/golibs v0.13.2 github.com/AdguardTeam/urlfilter v0.16.1 github.com/NYTimes/gziphandler v1.1.1 - github.com/ameshkov/dnscrypt/v2 v2.2.6 + github.com/ameshkov/dnscrypt/v2 v2.2.7 github.com/digineo/go-ipset/v2 v2.2.1 github.com/dimfeld/httptreemux/v5 v5.5.0 github.com/fsnotify/fsnotify v1.6.0 @@ -22,14 +22,17 @@ require ( github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118 github.com/mdlayher/netlink v1.7.1 github.com/mdlayher/packet v1.1.1 + // TODO(a.garipov): This package is deprecated; find a new one or use our + // own code for that. Perhaps, use gopacket. + github.com/mdlayher/raw v0.1.0 github.com/miekg/dns v1.1.53 github.com/quic-go/quic-go v0.33.0 github.com/stretchr/testify v1.8.2 github.com/ti-mo/netfilter v0.5.0 go.etcd.io/bbolt v1.3.7 - golang.org/x/crypto v0.7.0 + golang.org/x/crypto v0.8.0 golang.org/x/exp v0.0.0-20230321023759-10a507213a29 - golang.org/x/net v0.8.0 + golang.org/x/net v0.9.0 golang.org/x/sys v0.7.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v3 v3.0.1 @@ -45,8 +48,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/golang/mock v1.6.0 // indirect - github.com/google/pprof v0.0.0-20230323073829-e72429f035bd // indirect - github.com/mdlayher/raw v0.1.0 // indirect + github.com/google/pprof v0.0.0-20230406165453-00490a63f317 // indirect github.com/mdlayher/socket v0.4.0 // indirect github.com/onsi/ginkgo/v2 v2.9.2 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect @@ -54,11 +56,11 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect - github.com/quic-go/qtls-go1-19 v0.3.0 // indirect - github.com/quic-go/qtls-go1-20 v0.2.0 // indirect + github.com/quic-go/qtls-go1-19 v0.3.2 // indirect + github.com/quic-go/qtls-go1-20 v0.2.2 // indirect github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 // indirect - golang.org/x/mod v0.9.0 // indirect + golang.org/x/mod v0.10.0 // indirect golang.org/x/sync v0.1.0 // indirect - golang.org/x/text v0.8.0 // indirect - golang.org/x/tools v0.7.0 // indirect + golang.org/x/text v0.9.0 // indirect + golang.org/x/tools v0.8.0 // indirect ) diff --git a/go.sum b/go.sum index d522e255..6442a7db 100644 --- a/go.sum +++ b/go.sum @@ -15,8 +15,8 @@ github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmH github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw= github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us= -github.com/ameshkov/dnscrypt/v2 v2.2.6 h1:rE7AFbPWebq7me7RVS66Cipd1m7ef1yf2+C8QzjQXXE= -github.com/ameshkov/dnscrypt/v2 v2.2.6/go.mod h1:qPWhwz6FdSmuK7W4sMyvogrez4MWdtzosdqlr0Rg3ow= +github.com/ameshkov/dnscrypt/v2 v2.2.7 h1:aEitLIR8HcxVodZ79mgRcCiC0A0I5kZPBuWGFwwulAw= +github.com/ameshkov/dnscrypt/v2 v2.2.7/go.mod h1:qPWhwz6FdSmuK7W4sMyvogrez4MWdtzosdqlr0Rg3ow= github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo= github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A= github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 h1:0b2vaepXIfMsG++IsjHiI2p4bxALD1Y2nQKGMR5zDQM= @@ -55,8 +55,8 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= -github.com/google/pprof v0.0.0-20230323073829-e72429f035bd h1:r8yyd+DJDmsUhGrRBxH5Pj7KeFK5l+Y3FsgT8keqKtk= -github.com/google/pprof v0.0.0-20230323073829-e72429f035bd/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk= +github.com/google/pprof v0.0.0-20230406165453-00490a63f317 h1:hFhpt7CTmR3DX+b4R19ydQFtofxT0Sv3QsKNMVQYTMQ= +github.com/google/pprof v0.0.0-20230406165453-00490a63f317/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk= github.com/google/renameio v1.0.1 h1:Lh/jXZmvZxb0BBeSY5VKEfidcbcbenKjZFzM/q0fSeU= github.com/google/renameio v1.0.1/go.mod h1:t/HQoYBZSsWSNK35C6CO/TpPLDVWvxOHboWUAweKUpk= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -123,10 +123,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= -github.com/quic-go/qtls-go1-19 v0.3.0 h1:aUBoQdpHzUWtPw5tQZbsD2GnrWCNu7/RIX1PtqGeLYY= -github.com/quic-go/qtls-go1-19 v0.3.0/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI= -github.com/quic-go/qtls-go1-20 v0.2.0 h1:jUHn+obJ6WI5JudqBO0Iy1ra5Vh5vsitQ1gXQvkmN+E= -github.com/quic-go/qtls-go1-20 v0.2.0/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM= +github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U= +github.com/quic-go/qtls-go1-19 v0.3.2/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI= +github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8GEa3E= +github.com/quic-go/qtls-go1-20 v0.2.2/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM= github.com/quic-go/quic-go v0.33.0 h1:ItNoTDN/Fm/zBlq769lLJc8ECe9gYaW40veHCCco7y0= github.com/quic-go/quic-go v0.33.0/go.mod h1:YMuhaAV9/jIu0XclDXwZPAsP/2Kgr5yMYhe9oxhhOFA= github.com/shirou/gopsutil/v3 v3.21.8 h1:nKct+uP0TV8DjjNiHanKf8SAuub+GNsbrOtM9Nl9biA= @@ -161,15 +161,15 @@ go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= +golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug= golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= -golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= +golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -185,8 +185,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= @@ -227,15 +227,15 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= -golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y= +golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/dhcpd/config.go b/internal/dhcpd/config.go index c942039a..f7919fdf 100644 --- a/internal/dhcpd/config.go +++ b/internal/dhcpd/config.go @@ -31,8 +31,16 @@ type ServerConfig struct { Conf4 V4ServerConf `yaml:"dhcpv4"` Conf6 V6ServerConf `yaml:"dhcpv6"` - WorkDir string `yaml:"-"` - DBFilePath string `yaml:"-"` + // WorkDir is used to store DHCP leases. + // + // Deprecated: Remove it when migration of DHCP leases will not be needed. + WorkDir string `yaml:"-"` + + // DataDir is used to store DHCP leases. + DataDir string `yaml:"-"` + + // dbFilePath is the path to the file with stored DHCP leases. + dbFilePath string `yaml:"-"` } // DHCPServer - DHCP server interface diff --git a/internal/dhcpd/conn_darwin.go b/internal/dhcpd/conn_darwin.go new file mode 100644 index 00000000..a80ae482 --- /dev/null +++ b/internal/dhcpd/conn_darwin.go @@ -0,0 +1,293 @@ +//go:build darwin + +package dhcpd + +import ( + "fmt" + "net" + "os" + "time" + + "github.com/AdguardTeam/golibs/errors" + "github.com/AdguardTeam/golibs/log" + "github.com/AdguardTeam/golibs/netutil" + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/insomniacslk/dhcp/dhcpv4" + "github.com/insomniacslk/dhcp/dhcpv4/server4" + "github.com/mdlayher/ethernet" + + //lint:ignore SA1019 See the TODO in go.mod. + "github.com/mdlayher/raw" +) + +// dhcpUnicastAddr is the combination of MAC and IP addresses for responding to +// the unconfigured host. +type dhcpUnicastAddr struct { + // raw.Addr is embedded here to make *dhcpUcastAddr a net.Addr without + // actually implementing all methods. It also contains the client's + // hardware address. + raw.Addr + + // yiaddr is an IP address just allocated by server for the host. + yiaddr net.IP +} + +// dhcpConn is the net.PacketConn capable of handling both net.UDPAddr and +// net.HardwareAddr. +type dhcpConn struct { + // udpConn is the connection for UDP addresses. + udpConn net.PacketConn + // bcastIP is the broadcast address specific for the configured + // interface's subnet. + bcastIP net.IP + + // rawConn is the connection for MAC addresses. + rawConn net.PacketConn + // srcMAC is the hardware address of the configured network interface. + srcMAC net.HardwareAddr + // srcIP is the IP address of the configured network interface. + srcIP net.IP +} + +// newDHCPConn creates the special connection for DHCP server. +func (s *v4Server) newDHCPConn(iface *net.Interface) (c net.PacketConn, err error) { + var ucast net.PacketConn + if ucast, err = raw.ListenPacket(iface, uint16(ethernet.EtherTypeIPv4), nil); err != nil { + return nil, fmt.Errorf("creating raw udp connection: %w", err) + } + + // Create the UDP connection. + var bcast net.PacketConn + bcast, err = server4.NewIPv4UDPConn(iface.Name, &net.UDPAddr{ + // TODO(e.burkov): Listening on zeroes makes the server handle + // requests from all the interfaces. Inspect the ways to + // specify the interface-specific listening addresses. + // + // See https://github.com/AdguardTeam/AdGuardHome/issues/3539. + IP: net.IP{0, 0, 0, 0}, + Port: dhcpv4.ServerPort, + }) + if err != nil { + return nil, fmt.Errorf("creating ipv4 udp connection: %w", err) + } + + return &dhcpConn{ + udpConn: bcast, + bcastIP: s.conf.broadcastIP.AsSlice(), + rawConn: ucast, + srcMAC: iface.HardwareAddr, + srcIP: s.conf.dnsIPAddrs[0].AsSlice(), + }, nil +} + +// wrapErrs is a helper to wrap the errors from two independent underlying +// connections. +func (*dhcpConn) wrapErrs(action string, udpConnErr, rawConnErr error) (err error) { + switch { + case udpConnErr != nil && rawConnErr != nil: + return errors.List(fmt.Sprintf("%s both connections", action), udpConnErr, rawConnErr) + case udpConnErr != nil: + return fmt.Errorf("%s udp connection: %w", action, udpConnErr) + case rawConnErr != nil: + return fmt.Errorf("%s raw connection: %w", action, rawConnErr) + default: + return nil + } +} + +// WriteTo implements net.PacketConn for *dhcpConn. It selects the underlying +// connection to write to based on the type of addr. +func (c *dhcpConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { + switch addr := addr.(type) { + case *dhcpUnicastAddr: + // Unicast the message to the client's MAC address. Use the raw + // connection. + // + // Note: unicasting is performed on the only network interface + // that is configured. For now it may be not what users expect + // so additionally broadcast the message via UDP connection. + // + // See https://github.com/AdguardTeam/AdGuardHome/issues/3539. + var rerr error + n, rerr = c.unicast(p, addr) + + _, uerr := c.broadcast(p, &net.UDPAddr{ + IP: netutil.IPv4bcast(), + Port: dhcpv4.ClientPort, + }) + + return n, c.wrapErrs("writing to", uerr, rerr) + case *net.UDPAddr: + if addr.IP.Equal(net.IPv4bcast) { + // Broadcast the message for the client which supports + // it. Use the UDP connection. + return c.broadcast(p, addr) + } + + // Unicast the message to the client's IP address. Use the UDP + // connection. + return c.udpConn.WriteTo(p, addr) + default: + return 0, fmt.Errorf("addr has an unexpected type %T", addr) + } +} + +// ReadFrom implements net.PacketConn for *dhcpConn. +func (c *dhcpConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + return c.udpConn.ReadFrom(p) +} + +// unicast wraps respData with required frames and writes it to the peer. +func (c *dhcpConn) unicast(respData []byte, peer *dhcpUnicastAddr) (n int, err error) { + var data []byte + data, err = c.buildEtherPkt(respData, peer) + if err != nil { + return 0, err + } + + return c.rawConn.WriteTo(data, &peer.Addr) +} + +// Close implements net.PacketConn for *dhcpConn. +func (c *dhcpConn) Close() (err error) { + rerr := c.rawConn.Close() + if errors.Is(rerr, os.ErrClosed) { + // Ignore the error since the actual file is closed already. + rerr = nil + } + + return c.wrapErrs("closing", c.udpConn.Close(), rerr) +} + +// LocalAddr implements net.PacketConn for *dhcpConn. +func (c *dhcpConn) LocalAddr() (a net.Addr) { + return c.udpConn.LocalAddr() +} + +// SetDeadline implements net.PacketConn for *dhcpConn. +func (c *dhcpConn) SetDeadline(t time.Time) (err error) { + return c.wrapErrs("setting deadline on", c.udpConn.SetDeadline(t), c.rawConn.SetDeadline(t)) +} + +// SetReadDeadline implements net.PacketConn for *dhcpConn. +func (c *dhcpConn) SetReadDeadline(t time.Time) error { + return c.wrapErrs( + "setting reading deadline on", + c.udpConn.SetReadDeadline(t), + c.rawConn.SetReadDeadline(t), + ) +} + +// SetWriteDeadline implements net.PacketConn for *dhcpConn. +func (c *dhcpConn) SetWriteDeadline(t time.Time) error { + return c.wrapErrs( + "setting writing deadline on", + c.udpConn.SetWriteDeadline(t), + c.rawConn.SetWriteDeadline(t), + ) +} + +// ipv4DefaultTTL is the default Time to Live value in seconds as recommended by +// RFC-1700. +// +// See https://datatracker.ietf.org/doc/html/rfc1700. +const ipv4DefaultTTL = 64 + +// buildEtherPkt wraps the payload with IPv4, UDP and Ethernet frames. +// Validation of the payload is a caller's responsibility. +func (c *dhcpConn) buildEtherPkt(payload []byte, peer *dhcpUnicastAddr) (pkt []byte, err error) { + udpLayer := &layers.UDP{ + SrcPort: dhcpv4.ServerPort, + DstPort: dhcpv4.ClientPort, + } + + ipv4Layer := &layers.IPv4{ + Version: uint8(layers.IPProtocolIPv4), + Flags: layers.IPv4DontFragment, + TTL: ipv4DefaultTTL, + Protocol: layers.IPProtocolUDP, + SrcIP: c.srcIP, + DstIP: peer.yiaddr, + } + + // Ignore the error since it's only returned for invalid network layer's + // type. + _ = udpLayer.SetNetworkLayerForChecksum(ipv4Layer) + + ethLayer := &layers.Ethernet{ + SrcMAC: c.srcMAC, + DstMAC: peer.HardwareAddr, + EthernetType: layers.EthernetTypeIPv4, + } + + buf := gopacket.NewSerializeBuffer() + setts := gopacket.SerializeOptions{ + FixLengths: true, + ComputeChecksums: true, + } + + err = gopacket.SerializeLayers( + buf, + setts, + ethLayer, + ipv4Layer, + udpLayer, + gopacket.Payload(payload), + ) + if err != nil { + return nil, fmt.Errorf("serializing layers: %w", err) + } + + return buf.Bytes(), nil +} + +// send writes resp for peer to conn considering the req's parameters according +// to RFC-2131. +// +// See https://datatracker.ietf.org/doc/html/rfc2131#section-4.1. +func (s *v4Server) send(peer net.Addr, conn net.PacketConn, req, resp *dhcpv4.DHCPv4) { + switch giaddr, ciaddr, mtype := req.GatewayIPAddr, req.ClientIPAddr, resp.MessageType(); { + case giaddr != nil && !giaddr.IsUnspecified(): + // Send any return messages to the server port on the BOOTP + // relay agent whose address appears in giaddr. + peer = &net.UDPAddr{ + IP: giaddr, + Port: dhcpv4.ServerPort, + } + if mtype == dhcpv4.MessageTypeNak { + // Set the broadcast bit in the DHCPNAK, so that the relay agent + // broadcasts it to the client, because the client may not have + // a correct network address or subnet mask, and the client may not + // be answering ARP requests. + resp.SetBroadcast() + } + case mtype == dhcpv4.MessageTypeNak: + // Broadcast any DHCPNAK messages to 0xffffffff. + case ciaddr != nil && !ciaddr.IsUnspecified(): + // Unicast DHCPOFFER and DHCPACK messages to the address in + // ciaddr. + peer = &net.UDPAddr{ + IP: ciaddr, + Port: dhcpv4.ClientPort, + } + case !req.IsBroadcast() && req.ClientHWAddr != nil: + // Unicast DHCPOFFER and DHCPACK messages to the client's + // hardware address and yiaddr. + peer = &dhcpUnicastAddr{ + Addr: raw.Addr{HardwareAddr: req.ClientHWAddr}, + yiaddr: resp.YourIPAddr, + } + default: + // Go on since peer is already set to broadcast. + } + + pktData := resp.ToBytes() + + log.Debug("dhcpv4: sending %d bytes to %s: %s", len(pktData), peer, resp.Summary()) + + _, err := conn.WriteTo(pktData, peer) + if err != nil { + log.Error("dhcpv4: conn.Write to %s failed: %s", peer, err) + } +} diff --git a/internal/dhcpd/conn_darwin_internal_test.go b/internal/dhcpd/conn_darwin_internal_test.go new file mode 100644 index 00000000..e0522a0f --- /dev/null +++ b/internal/dhcpd/conn_darwin_internal_test.go @@ -0,0 +1,219 @@ +//go:build darwin + +package dhcpd + +import ( + "net" + "testing" + + "github.com/AdguardTeam/golibs/testutil" + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/insomniacslk/dhcp/dhcpv4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + //lint:ignore SA1019 See the TODO in go.mod. + "github.com/mdlayher/raw" +) + +func TestDHCPConn_WriteTo_common(t *testing.T) { + respData := (&dhcpv4.DHCPv4{}).ToBytes() + udpAddr := &net.UDPAddr{ + IP: net.IP{1, 2, 3, 4}, + Port: dhcpv4.ClientPort, + } + + t.Run("unicast_ip", func(t *testing.T) { + writeTo := func(_ []byte, addr net.Addr) (_ int, _ error) { + assert.Equal(t, udpAddr, addr) + + return 0, nil + } + + conn := &dhcpConn{udpConn: &fakePacketConn{writeTo: writeTo}} + + _, err := conn.WriteTo(respData, udpAddr) + assert.NoError(t, err) + }) + + t.Run("unexpected_addr_type", func(t *testing.T) { + type unexpectedAddrType struct { + net.Addr + } + + conn := &dhcpConn{} + n, err := conn.WriteTo(nil, &unexpectedAddrType{}) + require.Error(t, err) + + testutil.AssertErrorMsg(t, "addr has an unexpected type *dhcpd.unexpectedAddrType", err) + assert.Zero(t, n) + }) +} + +func TestBuildEtherPkt(t *testing.T) { + conn := &dhcpConn{ + srcMAC: net.HardwareAddr{1, 2, 3, 4, 5, 6}, + srcIP: net.IP{1, 2, 3, 4}, + } + peer := &dhcpUnicastAddr{ + Addr: raw.Addr{HardwareAddr: net.HardwareAddr{6, 5, 4, 3, 2, 1}}, + yiaddr: net.IP{4, 3, 2, 1}, + } + payload := (&dhcpv4.DHCPv4{}).ToBytes() + + t.Run("success", func(t *testing.T) { + pkt, err := conn.buildEtherPkt(payload, peer) + require.NoError(t, err) + + assert.NotEmpty(t, pkt) + + actualPkt := gopacket.NewPacket(pkt, layers.LayerTypeEthernet, gopacket.DecodeOptions{ + NoCopy: true, + }) + require.NotNil(t, actualPkt) + + wantTypes := []gopacket.LayerType{ + layers.LayerTypeEthernet, + layers.LayerTypeIPv4, + layers.LayerTypeUDP, + layers.LayerTypeDHCPv4, + } + actualLayers := actualPkt.Layers() + require.Len(t, actualLayers, len(wantTypes)) + + for i, wantType := range wantTypes { + layer := actualLayers[i] + require.NotNil(t, layer) + + assert.Equal(t, wantType, layer.LayerType()) + } + }) + + t.Run("bad_payload", func(t *testing.T) { + // Create an invalid DHCP packet. + invalidPayload := []byte{1, 2, 3, 4} + pkt, err := conn.buildEtherPkt(invalidPayload, peer) + require.NoError(t, err) + + assert.NotEmpty(t, pkt) + }) + + t.Run("serializing_error", func(t *testing.T) { + // Create a peer with invalid MAC. + badPeer := &dhcpUnicastAddr{ + Addr: raw.Addr{HardwareAddr: net.HardwareAddr{5, 4, 3, 2, 1}}, + yiaddr: net.IP{4, 3, 2, 1}, + } + + pkt, err := conn.buildEtherPkt(payload, badPeer) + require.Error(t, err) + + assert.Empty(t, pkt) + }) +} + +func TestV4Server_Send(t *testing.T) { + s := &v4Server{} + + var ( + defaultIP = net.IP{99, 99, 99, 99} + knownIP = net.IP{4, 2, 4, 2} + knownMAC = net.HardwareAddr{6, 5, 4, 3, 2, 1} + ) + + defaultPeer := &net.UDPAddr{ + IP: defaultIP, + // Use neither client nor server port to check it actually + // changed. + Port: dhcpv4.ClientPort + dhcpv4.ServerPort, + } + defaultResp := &dhcpv4.DHCPv4{} + + testCases := []struct { + want net.Addr + req *dhcpv4.DHCPv4 + resp *dhcpv4.DHCPv4 + name string + }{{ + name: "giaddr", + req: &dhcpv4.DHCPv4{GatewayIPAddr: knownIP}, + resp: defaultResp, + want: &net.UDPAddr{ + IP: knownIP, + Port: dhcpv4.ServerPort, + }, + }, { + name: "nak", + req: &dhcpv4.DHCPv4{}, + resp: &dhcpv4.DHCPv4{ + Options: dhcpv4.OptionsFromList( + dhcpv4.OptMessageType(dhcpv4.MessageTypeNak), + ), + }, + want: defaultPeer, + }, { + name: "ciaddr", + req: &dhcpv4.DHCPv4{ClientIPAddr: knownIP}, + resp: &dhcpv4.DHCPv4{}, + want: &net.UDPAddr{ + IP: knownIP, + Port: dhcpv4.ClientPort, + }, + }, { + name: "chaddr", + req: &dhcpv4.DHCPv4{ClientHWAddr: knownMAC}, + resp: &dhcpv4.DHCPv4{YourIPAddr: knownIP}, + want: &dhcpUnicastAddr{ + Addr: raw.Addr{HardwareAddr: knownMAC}, + yiaddr: knownIP, + }, + }, { + name: "who_are_you", + req: &dhcpv4.DHCPv4{}, + resp: &dhcpv4.DHCPv4{}, + want: defaultPeer, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + conn := &fakePacketConn{ + writeTo: func(_ []byte, addr net.Addr) (_ int, _ error) { + assert.Equal(t, tc.want, addr) + + return 0, nil + }, + } + + s.send(cloneUDPAddr(defaultPeer), conn, tc.req, tc.resp) + }) + } + + t.Run("giaddr_nak", func(t *testing.T) { + req := &dhcpv4.DHCPv4{ + GatewayIPAddr: knownIP, + } + // Ensure the request is for unicast. + req.SetUnicast() + resp := &dhcpv4.DHCPv4{ + Options: dhcpv4.OptionsFromList( + dhcpv4.OptMessageType(dhcpv4.MessageTypeNak), + ), + } + want := &net.UDPAddr{ + IP: req.GatewayIPAddr, + Port: dhcpv4.ServerPort, + } + + conn := &fakePacketConn{ + writeTo: func(_ []byte, addr net.Addr) (n int, err error) { + assert.Equal(t, want, addr) + + return 0, nil + }, + } + + s.send(cloneUDPAddr(defaultPeer), conn, req, resp) + assert.True(t, resp.IsBroadcast()) + }) +} diff --git a/internal/dhcpd/conn_unix.go b/internal/dhcpd/conn_unix.go index 37994e31..5602d126 100644 --- a/internal/dhcpd/conn_unix.go +++ b/internal/dhcpd/conn_unix.go @@ -1,4 +1,4 @@ -//go:build darwin || freebsd || linux || openbsd +//go:build freebsd || linux || openbsd package dhcpd @@ -9,6 +9,7 @@ import ( "time" "github.com/AdguardTeam/golibs/errors" + "github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/netutil" "github.com/google/gopacket" "github.com/google/gopacket/layers" @@ -238,3 +239,53 @@ func (c *dhcpConn) buildEtherPkt(payload []byte, peer *dhcpUnicastAddr) (pkt []b return buf.Bytes(), nil } + +// send writes resp for peer to conn considering the req's parameters according +// to RFC-2131. +// +// See https://datatracker.ietf.org/doc/html/rfc2131#section-4.1. +func (s *v4Server) send(peer net.Addr, conn net.PacketConn, req, resp *dhcpv4.DHCPv4) { + switch giaddr, ciaddr, mtype := req.GatewayIPAddr, req.ClientIPAddr, resp.MessageType(); { + case giaddr != nil && !giaddr.IsUnspecified(): + // Send any return messages to the server port on the BOOTP + // relay agent whose address appears in giaddr. + peer = &net.UDPAddr{ + IP: giaddr, + Port: dhcpv4.ServerPort, + } + if mtype == dhcpv4.MessageTypeNak { + // Set the broadcast bit in the DHCPNAK, so that the relay agent + // broadcasts it to the client, because the client may not have + // a correct network address or subnet mask, and the client may not + // be answering ARP requests. + resp.SetBroadcast() + } + case mtype == dhcpv4.MessageTypeNak: + // Broadcast any DHCPNAK messages to 0xffffffff. + case ciaddr != nil && !ciaddr.IsUnspecified(): + // Unicast DHCPOFFER and DHCPACK messages to the address in + // ciaddr. + peer = &net.UDPAddr{ + IP: ciaddr, + Port: dhcpv4.ClientPort, + } + case !req.IsBroadcast() && req.ClientHWAddr != nil: + // Unicast DHCPOFFER and DHCPACK messages to the client's + // hardware address and yiaddr. + peer = &dhcpUnicastAddr{ + Addr: packet.Addr{HardwareAddr: req.ClientHWAddr}, + yiaddr: resp.YourIPAddr, + } + default: + // Go on since peer is already set to broadcast. + } + + pktData := resp.ToBytes() + + log.Debug("dhcpv4: sending %d bytes to %s: %s", len(pktData), peer, resp.Summary()) + + _, err := conn.WriteTo(pktData, peer) + if err != nil { + log.Error("dhcpv4: conn.Write to %s failed: %s", peer, err) + } +} diff --git a/internal/dhcpd/conn_unix_test.go b/internal/dhcpd/conn_unix_internal_test.go similarity index 53% rename from internal/dhcpd/conn_unix_test.go rename to internal/dhcpd/conn_unix_internal_test.go index 65e64682..ca68c1b9 100644 --- a/internal/dhcpd/conn_unix_test.go +++ b/internal/dhcpd/conn_unix_internal_test.go @@ -1,4 +1,4 @@ -//go:build darwin || freebsd || linux || openbsd +//go:build freebsd || linux || openbsd package dhcpd @@ -110,3 +110,108 @@ func TestBuildEtherPkt(t *testing.T) { assert.Empty(t, pkt) }) } + +func TestV4Server_Send(t *testing.T) { + s := &v4Server{} + + var ( + defaultIP = net.IP{99, 99, 99, 99} + knownIP = net.IP{4, 2, 4, 2} + knownMAC = net.HardwareAddr{6, 5, 4, 3, 2, 1} + ) + + defaultPeer := &net.UDPAddr{ + IP: defaultIP, + // Use neither client nor server port to check it actually + // changed. + Port: dhcpv4.ClientPort + dhcpv4.ServerPort, + } + defaultResp := &dhcpv4.DHCPv4{} + + testCases := []struct { + want net.Addr + req *dhcpv4.DHCPv4 + resp *dhcpv4.DHCPv4 + name string + }{{ + name: "giaddr", + req: &dhcpv4.DHCPv4{GatewayIPAddr: knownIP}, + resp: defaultResp, + want: &net.UDPAddr{ + IP: knownIP, + Port: dhcpv4.ServerPort, + }, + }, { + name: "nak", + req: &dhcpv4.DHCPv4{}, + resp: &dhcpv4.DHCPv4{ + Options: dhcpv4.OptionsFromList( + dhcpv4.OptMessageType(dhcpv4.MessageTypeNak), + ), + }, + want: defaultPeer, + }, { + name: "ciaddr", + req: &dhcpv4.DHCPv4{ClientIPAddr: knownIP}, + resp: &dhcpv4.DHCPv4{}, + want: &net.UDPAddr{ + IP: knownIP, + Port: dhcpv4.ClientPort, + }, + }, { + name: "chaddr", + req: &dhcpv4.DHCPv4{ClientHWAddr: knownMAC}, + resp: &dhcpv4.DHCPv4{YourIPAddr: knownIP}, + want: &dhcpUnicastAddr{ + Addr: packet.Addr{HardwareAddr: knownMAC}, + yiaddr: knownIP, + }, + }, { + name: "who_are_you", + req: &dhcpv4.DHCPv4{}, + resp: &dhcpv4.DHCPv4{}, + want: defaultPeer, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + conn := &fakePacketConn{ + writeTo: func(_ []byte, addr net.Addr) (_ int, _ error) { + assert.Equal(t, tc.want, addr) + + return 0, nil + }, + } + + s.send(cloneUDPAddr(defaultPeer), conn, tc.req, tc.resp) + }) + } + + t.Run("giaddr_nak", func(t *testing.T) { + req := &dhcpv4.DHCPv4{ + GatewayIPAddr: knownIP, + } + // Ensure the request is for unicast. + req.SetUnicast() + resp := &dhcpv4.DHCPv4{ + Options: dhcpv4.OptionsFromList( + dhcpv4.OptMessageType(dhcpv4.MessageTypeNak), + ), + } + want := &net.UDPAddr{ + IP: req.GatewayIPAddr, + Port: dhcpv4.ServerPort, + } + + conn := &fakePacketConn{ + writeTo: func(_ []byte, addr net.Addr) (n int, err error) { + assert.Equal(t, want, addr) + + return 0, nil + }, + } + + s.send(cloneUDPAddr(defaultPeer), conn, req, resp) + assert.True(t, resp.IsBroadcast()) + }) +} diff --git a/internal/dhcpd/db.go b/internal/dhcpd/db.go index 453470a1..3a6e98b7 100644 --- a/internal/dhcpd/db.go +++ b/internal/dhcpd/db.go @@ -5,43 +5,34 @@ package dhcpd import ( "encoding/json" "fmt" - "net" - "net/netip" "os" - "time" "github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/log" "github.com/google/renameio/maybe" + "golang.org/x/exp/slices" ) -const dbFilename = "leases.db" +const ( + // dataFilename contains saved leases. + dataFilename = "leases.json" -type leaseJSON struct { - HWAddr []byte `json:"mac"` - IP []byte `json:"ip"` - Hostname string `json:"host"` - Expiry int64 `json:"exp"` + // dataVersion is the current version of the stored DHCP leases structure. + dataVersion = 1 +) + +// dataLeases is the structure of the stored DHCP leases. +type dataLeases struct { + // Version is the current version of the structure. + Version int `json:"version"` + + // Leases is the list containing stored DHCP leases. + Leases []*Lease `json:"leases"` } -func normalizeIP(ip net.IP) net.IP { - ip4 := ip.To4() - if ip4 != nil { - return ip4 - } - return ip -} - -// Load lease table from DB -// -// TODO(s.chzhen): Decrease complexity. +// dbLoad loads stored leases. func (s *server) dbLoad() (err error) { - dynLeases := []*Lease{} - staticLeases := []*Lease{} - v6StaticLeases := []*Lease{} - v6DynLeases := []*Lease{} - - data, err := os.ReadFile(s.conf.DBFilePath) + data, err := os.ReadFile(s.conf.dbFilePath) if err != nil { if !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("reading db: %w", err) @@ -50,52 +41,30 @@ func (s *server) dbLoad() (err error) { return nil } - obj := []leaseJSON{} - err = json.Unmarshal(data, &obj) + dl := &dataLeases{} + err = json.Unmarshal(data, dl) if err != nil { return fmt.Errorf("decoding db: %w", err) } - numLeases := len(obj) - for i := range obj { - obj[i].IP = normalizeIP(obj[i].IP) + leases := dl.Leases - ip, ok := netip.AddrFromSlice(obj[i].IP) - if !ok { - log.Info("dhcp: invalid IP: %s", obj[i].IP) - continue - } + leases4 := []*Lease{} + leases6 := []*Lease{} - lease := Lease{ - HWAddr: obj[i].HWAddr, - IP: ip, - Hostname: obj[i].Hostname, - Expiry: time.Unix(obj[i].Expiry, 0), - IsStatic: obj[i].Expiry == leaseExpireStatic, - } - - if len(obj[i].IP) == 16 { - if lease.IsStatic { - v6StaticLeases = append(v6StaticLeases, &lease) - } else { - v6DynLeases = append(v6DynLeases, &lease) - } + for _, l := range leases { + if l.IP.Is4() { + leases4 = append(leases4, l) } else { - if lease.IsStatic { - staticLeases = append(staticLeases, &lease) - } else { - dynLeases = append(dynLeases, &lease) - } + leases6 = append(leases6, l) } } - leases4 := normalizeLeases(staticLeases, dynLeases) err = s.srv4.ResetLeases(leases4) if err != nil { return fmt.Errorf("resetting dhcpv4 leases: %w", err) } - leases6 := normalizeLeases(v6StaticLeases, v6DynLeases) if s.srv6 != nil { err = s.srv6.ResetLeases(leases6) if err != nil { @@ -104,90 +73,54 @@ func (s *server) dbLoad() (err error) { } log.Info("dhcp: loaded leases v4:%d v6:%d total-read:%d from DB", - len(leases4), len(leases6), numLeases) + len(leases4), len(leases6), len(leases)) return nil } -// Skip duplicate leases -// Static leases have a priority over dynamic leases -func normalizeLeases(staticLeases, dynLeases []*Lease) []*Lease { - leases := []*Lease{} - index := map[string]int{} - - for i, lease := range staticLeases { - _, ok := index[lease.HWAddr.String()] - if ok { - continue // skip the lease with the same HW address - } - index[lease.HWAddr.String()] = i - leases = append(leases, lease) - } - - for i, lease := range dynLeases { - _, ok := index[lease.HWAddr.String()] - if ok { - continue // skip the lease with the same HW address - } - index[lease.HWAddr.String()] = i - leases = append(leases, lease) - } - - return leases -} - -// Store lease table in DB +// dbStore stores DHCP leases. func (s *server) dbStore() (err error) { // Use an empty slice here as opposed to nil so that it doesn't write // "null" into the database file if leases are empty. - leases := []leaseJSON{} + leases := []*Lease{} leases4 := s.srv4.getLeasesRef() - for _, l := range leases4 { - if l.Expiry.Unix() == 0 { - continue - } - - lease := leaseJSON{ - HWAddr: l.HWAddr, - IP: l.IP.AsSlice(), - Hostname: l.Hostname, - Expiry: l.Expiry.Unix(), - } - - leases = append(leases, lease) - } + leases = append(leases, leases4...) if s.srv6 != nil { leases6 := s.srv6.getLeasesRef() - for _, l := range leases6 { - if l.Expiry.Unix() == 0 { - continue - } - - lease := leaseJSON{ - HWAddr: l.HWAddr, - IP: l.IP.AsSlice(), - Hostname: l.Hostname, - Expiry: l.Expiry.Unix(), - } - - leases = append(leases, lease) - } + leases = append(leases, leases6...) } - var data []byte - data, err = json.Marshal(leases) + return writeDB(s.conf.dbFilePath, leases) +} + +// writeDB writes leases to file at path. +func writeDB(path string, leases []*Lease) (err error) { + defer func() { err = errors.Annotate(err, "writing db: %w") }() + + slices.SortFunc(leases, func(a, b *Lease) bool { + return a.Hostname < b.Hostname + }) + + dl := &dataLeases{ + Version: dataVersion, + Leases: leases, + } + + buf, err := json.Marshal(dl) if err != nil { - return fmt.Errorf("encoding db: %w", err) + // Don't wrap the error since it's informative enough as is. + return err } - err = maybe.WriteFile(s.conf.DBFilePath, data, 0o644) + err = maybe.WriteFile(path, buf, 0o644) if err != nil { - return fmt.Errorf("writing db: %w", err) + // Don't wrap the error since it's informative enough as is. + return err } - log.Info("dhcp: stored %d leases in db", len(leases)) + log.Info("dhcp: stored %d leases in %q", len(leases), path) return nil } diff --git a/internal/dhcpd/dhcpd.go b/internal/dhcpd/dhcpd.go index 6ac830d6..69082c0c 100644 --- a/internal/dhcpd/dhcpd.go +++ b/internal/dhcpd/dhcpd.go @@ -15,13 +15,6 @@ import ( ) const ( - // leaseExpireStatic is used to define the Expiry field for static - // leases. - // - // TODO(e.burkov): Remove it when static leases determining mechanism - // will be improved. - leaseExpireStatic = 1 - // DefaultDHCPLeaseTTL is the default time-to-live for leases. DefaultDHCPLeaseTTL = uint32(timeutil.Day / time.Second) @@ -35,10 +28,10 @@ const ( defaultBackoff time.Duration = 500 * time.Millisecond ) -// Lease contains the necessary information about a DHCP lease +// Lease contains the necessary information about a DHCP lease. It's used in +// various places. So don't change it without good reason. type Lease struct { - // Expiry is the expiration time of the lease. The unix timestamp value - // of 1 means that this is a static lease. + // Expiry is the expiration time of the lease. Expiry time.Time `json:"expires"` // Hostname of the client. @@ -238,7 +231,7 @@ func Create(conf *ServerConfig) (s *server, err error) { LocalDomainName: conf.LocalDomainName, - DBFilePath: filepath.Join(conf.WorkDir, dbFilename), + dbFilePath: filepath.Join(conf.DataDir, dataFilename), }, } @@ -279,6 +272,13 @@ func Create(conf *ServerConfig) (s *server, err error) { return nil, fmt.Errorf("neither dhcpv4 nor dhcpv6 srv is configured") } + // Migrate leases db if needed. + err = migrateDB(conf) + if err != nil { + // Don't wrap the error since it's informative enough as is. + return nil, err + } + // Don't delay database loading until the DHCP server is started, // because we need static leases functionality available beforehand. err = s.dbLoad() diff --git a/internal/dhcpd/dhcpd_unix_test.go b/internal/dhcpd/dhcpd_unix_test.go index 40e83697..7eced536 100644 --- a/internal/dhcpd/dhcpd_unix_test.go +++ b/internal/dhcpd/dhcpd_unix_test.go @@ -5,7 +5,7 @@ package dhcpd import ( "net" "net/netip" - "os" + "path/filepath" "testing" "time" @@ -27,7 +27,7 @@ func TestDB(t *testing.T) { var err error s := server{ conf: &ServerConfig{ - DBFilePath: dbFilename, + dbFilePath: filepath.Join(t.TempDir(), dataFilename), }, } @@ -67,8 +67,6 @@ func TestDB(t *testing.T) { err = s.dbStore() require.NoError(t, err) - testutil.CleanupAndRequireSuccess(t, func() (err error) { return os.Remove(dbFilename) }) - err = s.srv4.ResetLeases(nil) require.NoError(t, err) @@ -78,36 +76,13 @@ func TestDB(t *testing.T) { ll := s.srv4.GetLeases(LeasesAll) require.Len(t, ll, len(leases)) - assert.Equal(t, leases[1].HWAddr, ll[0].HWAddr) - assert.Equal(t, leases[1].IP, ll[0].IP) - assert.True(t, ll[0].IsStatic) + assert.Equal(t, leases[0].HWAddr, ll[0].HWAddr) + assert.Equal(t, leases[0].IP, ll[0].IP) + assert.Equal(t, leases[0].Expiry.Unix(), ll[0].Expiry.Unix()) - assert.Equal(t, leases[0].HWAddr, ll[1].HWAddr) - assert.Equal(t, leases[0].IP, ll[1].IP) - assert.Equal(t, leases[0].Expiry.Unix(), ll[1].Expiry.Unix()) -} - -func TestNormalizeLeases(t *testing.T) { - dynLeases := []*Lease{{ - HWAddr: net.HardwareAddr{1, 2, 3, 4}, - }, { - HWAddr: net.HardwareAddr{1, 2, 3, 5}, - }} - - staticLeases := []*Lease{{ - HWAddr: net.HardwareAddr{1, 2, 3, 4}, - IP: netip.MustParseAddr("0.2.3.4"), - }, { - HWAddr: net.HardwareAddr{2, 2, 3, 4}, - }} - - leases := normalizeLeases(staticLeases, dynLeases) - require.Len(t, leases, 3) - - assert.Equal(t, leases[0].HWAddr, dynLeases[0].HWAddr) - assert.Equal(t, leases[0].IP, staticLeases[0].IP) - assert.Equal(t, leases[1].HWAddr, staticLeases[1].HWAddr) - assert.Equal(t, leases[2].HWAddr, dynLeases[1].HWAddr) + assert.Equal(t, leases[1].HWAddr, ll[1].HWAddr) + assert.Equal(t, leases[1].IP, ll[1].IP) + assert.True(t, ll[1].IsStatic) } func TestV4Server_badRange(t *testing.T) { diff --git a/internal/dhcpd/http_unix.go b/internal/dhcpd/http_unix.go index 9eb4eb47..6430afdc 100644 --- a/internal/dhcpd/http_unix.go +++ b/internal/dhcpd/http_unix.go @@ -639,7 +639,7 @@ func (s *server) handleReset(w http.ResponseWriter, r *http.Request) { return } - err = os.Remove(s.conf.DBFilePath) + err = os.Remove(s.conf.dbFilePath) if err != nil && !errors.Is(err, os.ErrNotExist) { log.Error("dhcp: removing db: %s", err) } @@ -651,8 +651,8 @@ func (s *server) handleReset(w http.ResponseWriter, r *http.Request) { LocalDomainName: s.conf.LocalDomainName, - WorkDir: s.conf.WorkDir, - DBFilePath: s.conf.DBFilePath, + DataDir: s.conf.DataDir, + dbFilePath: s.conf.dbFilePath, } v4conf := &V4ServerConf{ diff --git a/internal/dhcpd/http_unix_test.go b/internal/dhcpd/http_unix_test.go index d23614c3..2a569f4e 100644 --- a/internal/dhcpd/http_unix_test.go +++ b/internal/dhcpd/http_unix_test.go @@ -31,8 +31,7 @@ func TestServer_handleDHCPStatus(t *testing.T) { s, err := Create(&ServerConfig{ Enabled: true, Conf4: *defaultV4ServerConf(), - WorkDir: t.TempDir(), - DBFilePath: dbFilename, + DataDir: t.TempDir(), ConfigModified: func() {}, }) require.NoError(t, err) diff --git a/internal/dhcpd/migrate.go b/internal/dhcpd/migrate.go new file mode 100644 index 00000000..aafee9b6 --- /dev/null +++ b/internal/dhcpd/migrate.go @@ -0,0 +1,106 @@ +package dhcpd + +import ( + "encoding/json" + "net" + "net/netip" + "os" + "path/filepath" + "time" + + "github.com/AdguardTeam/golibs/errors" + "github.com/AdguardTeam/golibs/log" +) + +const ( + // leaseExpireStatic is used to define the Expiry field for static + // leases. + // + // Deprecated: Remove it when migration of DHCP leases will be not needed. + leaseExpireStatic = 1 + + // dbFilename contains saved leases. + // + // Deprecated: Use dataFilename. + dbFilename = "leases.db" +) + +// leaseJSON is the structure of stored lease. +// +// Deprecated: Use [Lease]. +type leaseJSON struct { + HWAddr []byte `json:"mac"` + IP []byte `json:"ip"` + Hostname string `json:"host"` + Expiry int64 `json:"exp"` +} + +func normalizeIP(ip net.IP) net.IP { + ip4 := ip.To4() + if ip4 != nil { + return ip4 + } + + return ip +} + +// migrateDB migrates stored leases if necessary. +func migrateDB(conf *ServerConfig) (err error) { + defer func() { err = errors.Annotate(err, "migrating db: %w") }() + + oldLeasesPath := filepath.Join(conf.WorkDir, dbFilename) + dataDirPath := filepath.Join(conf.DataDir, dataFilename) + + file, err := os.Open(oldLeasesPath) + if errors.Is(err, os.ErrNotExist) { + // Nothing to migrate. + return nil + } else if err != nil { + // Don't wrap the error since it's informative enough as is. + return err + } + + ljs := []leaseJSON{} + err = json.NewDecoder(file).Decode(&ljs) + if err != nil { + // Don't wrap the error since it's informative enough as is. + return err + } + + err = file.Close() + if err != nil { + // Don't wrap the error since it's informative enough as is. + return err + } + + leases := []*Lease{} + + for _, lj := range ljs { + lj.IP = normalizeIP(lj.IP) + + ip, ok := netip.AddrFromSlice(lj.IP) + if !ok { + log.Info("dhcp: invalid IP: %s", lj.IP) + + continue + } + + lease := &Lease{ + Expiry: time.Unix(lj.Expiry, 0), + Hostname: lj.Hostname, + HWAddr: lj.HWAddr, + IP: ip, + IsStatic: lj.Expiry == leaseExpireStatic, + } + + leases = append(leases, lease) + } + + err = writeDB(dataDirPath, leases) + if err != nil { + // Don't wrap the error since it's informative enough as is. + return err + } + + return os.Remove(oldLeasesPath) +} diff --git a/internal/dhcpd/migrate_internal_test.go b/internal/dhcpd/migrate_internal_test.go new file mode 100644 index 00000000..2c0e6ecd --- /dev/null +++ b/internal/dhcpd/migrate_internal_test.go @@ -0,0 +1,73 @@ +package dhcpd + +import ( + "encoding/json" + "net" + "net/netip" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const testData = `[ +{"mac":"ESIzRFVm","ip":"AQIDBA==","host":"test1","exp":1}, +{"mac":"ZlVEMyIR","ip":"BAMCAQ==","host":"test2","exp":1231231231} +]` + +func TestMigrateDB(t *testing.T) { + dir := t.TempDir() + + oldLeasesPath := filepath.Join(dir, dbFilename) + dataDirPath := filepath.Join(dir, dataFilename) + + err := os.WriteFile(oldLeasesPath, []byte(testData), 0o644) + require.NoError(t, err) + + wantLeases := []*Lease{{ + Expiry: time.Time{}, + Hostname: "test1", + HWAddr: net.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66}, + IP: netip.MustParseAddr("1.2.3.4"), + IsStatic: true, + }, { + Expiry: time.Unix(1231231231, 0), + Hostname: "test2", + HWAddr: net.HardwareAddr{0x66, 0x55, 0x44, 0x33, 0x22, 0x11}, + IP: netip.MustParseAddr("4.3.2.1"), + IsStatic: false, + }} + + conf := &ServerConfig{ + WorkDir: dir, + DataDir: dir, + } + + err = migrateDB(conf) + require.NoError(t, err) + + _, err = os.Stat(oldLeasesPath) + require.ErrorIs(t, err, os.ErrNotExist) + + var data []byte + data, err = os.ReadFile(dataDirPath) + require.NoError(t, err) + + dl := &dataLeases{} + err = json.Unmarshal(data, dl) + require.NoError(t, err) + + leases := dl.Leases + + for i, wl := range wantLeases { + assert.Equal(t, wl.Hostname, leases[i].Hostname) + assert.Equal(t, wl.HWAddr, leases[i].HWAddr) + assert.Equal(t, wl.IP, leases[i].IP) + assert.Equal(t, wl.IsStatic, leases[i].IsStatic) + + require.True(t, wl.Expiry.Equal(leases[i].Expiry)) + } +} diff --git a/internal/dhcpd/v4_unix.go b/internal/dhcpd/v4_unix.go index 4ac46db6..20b2c96e 100644 --- a/internal/dhcpd/v4_unix.go +++ b/internal/dhcpd/v4_unix.go @@ -20,7 +20,6 @@ import ( "github.com/go-ping/ping" "github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4/server4" - "github.com/mdlayher/packet" "golang.org/x/exp/slices" ) @@ -257,6 +256,8 @@ func (s *v4Server) rmLeaseByIndex(i int) { // Remove a dynamic lease with the same properties // Return error if a static lease is found +// +// TODO(s.chzhen): Refactor the code. func (s *v4Server) rmDynamicLease(lease *Lease) (err error) { for i, l := range s.leases { isStatic := l.IsStatic @@ -358,7 +359,6 @@ func (s *v4Server) AddStaticLease(l *Lease) (err error) { return fmt.Errorf("can't assign the gateway IP %s to the lease", gwIP) } - l.Expiry = time.Unix(leaseExpireStatic, 0) l.IsStatic = true err = netutil.ValidateMAC(l.HWAddr) @@ -1132,56 +1132,6 @@ func (s *v4Server) packetHandler(conn net.PacketConn, peer net.Addr, req *dhcpv4 s.send(peer, conn, req, resp) } -// send writes resp for peer to conn considering the req's parameters according -// to RFC-2131. -// -// See https://datatracker.ietf.org/doc/html/rfc2131#section-4.1. -func (s *v4Server) send(peer net.Addr, conn net.PacketConn, req, resp *dhcpv4.DHCPv4) { - switch giaddr, ciaddr, mtype := req.GatewayIPAddr, req.ClientIPAddr, resp.MessageType(); { - case giaddr != nil && !giaddr.IsUnspecified(): - // Send any return messages to the server port on the BOOTP - // relay agent whose address appears in giaddr. - peer = &net.UDPAddr{ - IP: giaddr, - Port: dhcpv4.ServerPort, - } - if mtype == dhcpv4.MessageTypeNak { - // Set the broadcast bit in the DHCPNAK, so that the relay agent - // broadcasts it to the client, because the client may not have - // a correct network address or subnet mask, and the client may not - // be answering ARP requests. - resp.SetBroadcast() - } - case mtype == dhcpv4.MessageTypeNak: - // Broadcast any DHCPNAK messages to 0xffffffff. - case ciaddr != nil && !ciaddr.IsUnspecified(): - // Unicast DHCPOFFER and DHCPACK messages to the address in - // ciaddr. - peer = &net.UDPAddr{ - IP: ciaddr, - Port: dhcpv4.ClientPort, - } - case !req.IsBroadcast() && req.ClientHWAddr != nil: - // Unicast DHCPOFFER and DHCPACK messages to the client's - // hardware address and yiaddr. - peer = &dhcpUnicastAddr{ - Addr: packet.Addr{HardwareAddr: req.ClientHWAddr}, - yiaddr: resp.YourIPAddr, - } - default: - // Go on since peer is already set to broadcast. - } - - pktData := resp.ToBytes() - - log.Debug("dhcpv4: sending %d bytes to %s: %s", len(pktData), peer, resp.Summary()) - - _, err := conn.WriteTo(pktData, peer) - if err != nil { - log.Error("dhcpv4: conn.Write to %s failed: %s", peer, err) - } -} - // Start starts the IPv4 DHCP server. func (s *v4Server) Start() (err error) { defer func() { err = errors.Annotate(err, "dhcpv4: %w") }() diff --git a/internal/dhcpd/v4_unix_test.go b/internal/dhcpd/v4_unix_test.go index 7ce3cdc3..a5ce5e0e 100644 --- a/internal/dhcpd/v4_unix_test.go +++ b/internal/dhcpd/v4_unix_test.go @@ -15,7 +15,6 @@ import ( "github.com/AdguardTeam/golibs/stringutil" "github.com/AdguardTeam/golibs/testutil" "github.com/insomniacslk/dhcp/dhcpv4" - "github.com/mdlayher/packet" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -69,7 +68,6 @@ func TestV4Server_leasing(t *testing.T) { t.Run("add_static", func(t *testing.T) { err := s.AddStaticLease(&Lease{ - Expiry: time.Unix(leaseExpireStatic, 0), Hostname: staticName, HWAddr: staticMAC, IP: staticIP, @@ -79,7 +77,6 @@ func TestV4Server_leasing(t *testing.T) { t.Run("same_name", func(t *testing.T) { err = s.AddStaticLease(&Lease{ - Expiry: time.Unix(leaseExpireStatic, 0), Hostname: staticName, HWAddr: anotherMAC, IP: anotherIP, @@ -94,7 +91,6 @@ func TestV4Server_leasing(t *testing.T) { " (" + staticMAC.String() + "): static lease already exists" err = s.AddStaticLease(&Lease{ - Expiry: time.Unix(leaseExpireStatic, 0), Hostname: anotherName, HWAddr: staticMAC, IP: anotherIP, @@ -109,7 +105,6 @@ func TestV4Server_leasing(t *testing.T) { " (" + anotherMAC.String() + "): static lease already exists" err = s.AddStaticLease(&Lease{ - Expiry: time.Unix(leaseExpireStatic, 0), Hostname: anotherName, HWAddr: anotherMAC, IP: staticIP, @@ -771,111 +766,6 @@ func (fc *fakePacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { return fc.writeTo(p, addr) } -func TestV4Server_Send(t *testing.T) { - s := &v4Server{} - - var ( - defaultIP = net.IP{99, 99, 99, 99} - knownIP = net.IP{4, 2, 4, 2} - knownMAC = net.HardwareAddr{6, 5, 4, 3, 2, 1} - ) - - defaultPeer := &net.UDPAddr{ - IP: defaultIP, - // Use neither client nor server port to check it actually - // changed. - Port: dhcpv4.ClientPort + dhcpv4.ServerPort, - } - defaultResp := &dhcpv4.DHCPv4{} - - testCases := []struct { - want net.Addr - req *dhcpv4.DHCPv4 - resp *dhcpv4.DHCPv4 - name string - }{{ - name: "giaddr", - req: &dhcpv4.DHCPv4{GatewayIPAddr: knownIP}, - resp: defaultResp, - want: &net.UDPAddr{ - IP: knownIP, - Port: dhcpv4.ServerPort, - }, - }, { - name: "nak", - req: &dhcpv4.DHCPv4{}, - resp: &dhcpv4.DHCPv4{ - Options: dhcpv4.OptionsFromList( - dhcpv4.OptMessageType(dhcpv4.MessageTypeNak), - ), - }, - want: defaultPeer, - }, { - name: "ciaddr", - req: &dhcpv4.DHCPv4{ClientIPAddr: knownIP}, - resp: &dhcpv4.DHCPv4{}, - want: &net.UDPAddr{ - IP: knownIP, - Port: dhcpv4.ClientPort, - }, - }, { - name: "chaddr", - req: &dhcpv4.DHCPv4{ClientHWAddr: knownMAC}, - resp: &dhcpv4.DHCPv4{YourIPAddr: knownIP}, - want: &dhcpUnicastAddr{ - Addr: packet.Addr{HardwareAddr: knownMAC}, - yiaddr: knownIP, - }, - }, { - name: "who_are_you", - req: &dhcpv4.DHCPv4{}, - resp: &dhcpv4.DHCPv4{}, - want: defaultPeer, - }} - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - conn := &fakePacketConn{ - writeTo: func(_ []byte, addr net.Addr) (_ int, _ error) { - assert.Equal(t, tc.want, addr) - - return 0, nil - }, - } - - s.send(cloneUDPAddr(defaultPeer), conn, tc.req, tc.resp) - }) - } - - t.Run("giaddr_nak", func(t *testing.T) { - req := &dhcpv4.DHCPv4{ - GatewayIPAddr: knownIP, - } - // Ensure the request is for unicast. - req.SetUnicast() - resp := &dhcpv4.DHCPv4{ - Options: dhcpv4.OptionsFromList( - dhcpv4.OptMessageType(dhcpv4.MessageTypeNak), - ), - } - want := &net.UDPAddr{ - IP: req.GatewayIPAddr, - Port: dhcpv4.ServerPort, - } - - conn := &fakePacketConn{ - writeTo: func(_ []byte, addr net.Addr) (n int, err error) { - assert.Equal(t, want, addr) - - return 0, nil - }, - } - - s.send(cloneUDPAddr(defaultPeer), conn, req, resp) - assert.True(t, resp.IsBroadcast()) - }) -} - func TestV4Server_FindMACbyIP(t *testing.T) { const ( staticName = "static-client" @@ -890,7 +780,6 @@ func TestV4Server_FindMACbyIP(t *testing.T) { s := &v4Server{ leases: []*Lease{{ - Expiry: time.Unix(leaseExpireStatic, 0), Hostname: staticName, HWAddr: staticMAC, IP: staticIP, diff --git a/internal/dhcpd/v6_unix.go b/internal/dhcpd/v6_unix.go index 2655a343..cbe67eaa 100644 --- a/internal/dhcpd/v6_unix.go +++ b/internal/dhcpd/v6_unix.go @@ -66,8 +66,7 @@ func (s *v6Server) ResetLeases(leases []*Lease) (err error) { s.leases = nil for _, l := range leases { ip := net.IP(l.IP.AsSlice()) - if l.Expiry.Unix() != leaseExpireStatic && - !ip6InRange(s.conf.ipStart, ip) { + if !l.IsStatic && !ip6InRange(s.conf.ipStart, ip) { log.Debug("dhcpv6: skipping a lease with IP %v: not within current IP range", l.IP) @@ -89,7 +88,7 @@ func (s *v6Server) GetLeases(flags GetLeasesFlags) (leases []*Lease) { leases = []*Lease{} s.leasesLock.Lock() for _, l := range s.leases { - if l.Expiry.Unix() == leaseExpireStatic { + if l.IsStatic { if (flags & LeasesStatic) != 0 { leases = append(leases, l.Clone()) } @@ -150,7 +149,7 @@ func (s *v6Server) rmDynamicLease(lease *Lease) (err error) { l := s.leases[i] if bytes.Equal(l.HWAddr, lease.HWAddr) { - if l.Expiry.Unix() == leaseExpireStatic { + if l.IsStatic { return fmt.Errorf("static lease already exists") } @@ -163,7 +162,7 @@ func (s *v6Server) rmDynamicLease(lease *Lease) (err error) { } if l.IP == lease.IP { - if l.Expiry.Unix() == leaseExpireStatic { + if l.IsStatic { return fmt.Errorf("static lease already exists") } @@ -187,7 +186,7 @@ func (s *v6Server) AddStaticLease(l *Lease) (err error) { return fmt.Errorf("validating lease: %w", err) } - l.Expiry = time.Unix(leaseExpireStatic, 0) + l.IsStatic = true s.leasesLock.Lock() err = s.rmDynamicLease(l) @@ -274,8 +273,7 @@ func (s *v6Server) findLease(mac net.HardwareAddr) *Lease { func (s *v6Server) findExpiredLease() int { now := time.Now().Unix() for i, lease := range s.leases { - if lease.Expiry.Unix() != leaseExpireStatic && - lease.Expiry.Unix() <= now { + if !lease.IsStatic && lease.Expiry.Unix() <= now { return i } } @@ -421,7 +419,7 @@ func (s *v6Server) commitLease(msg *dhcpv6.Message, lease *Lease) time.Duration dhcpv6.MessageTypeRenew, dhcpv6.MessageTypeRebind: - if lease.Expiry.Unix() != leaseExpireStatic { + if !lease.IsStatic { s.commitDynamicLease(lease) } } diff --git a/internal/dhcpd/v6_unix_test.go b/internal/dhcpd/v6_unix_test.go index 85c29e3e..c5034e47 100644 --- a/internal/dhcpd/v6_unix_test.go +++ b/internal/dhcpd/v6_unix_test.go @@ -44,7 +44,7 @@ func TestV6_AddRemove_static(t *testing.T) { assert.Equal(t, l.IP, ls[0].IP) assert.Equal(t, l.HWAddr, ls[0].HWAddr) - assert.EqualValues(t, leaseExpireStatic, ls[0].Expiry.Unix()) + assert.True(t, ls[0].IsStatic) // Try to remove non-existent static lease. err = s.RemoveStaticLease(&Lease{ @@ -103,7 +103,7 @@ func TestV6_AddReplace(t *testing.T) { for i, l := range ls { assert.Equal(t, stLeases[i].IP, l.IP) assert.Equal(t, stLeases[i].HWAddr, l.HWAddr) - assert.EqualValues(t, leaseExpireStatic, l.Expiry.Unix()) + assert.True(t, l.IsStatic) } } @@ -327,7 +327,6 @@ func TestV6_FindMACbyIP(t *testing.T) { s := &v6Server{ leases: []*Lease{{ - Expiry: time.Unix(leaseExpireStatic, 0), Hostname: staticName, HWAddr: staticMAC, IP: staticIP, @@ -341,7 +340,6 @@ func TestV6_FindMACbyIP(t *testing.T) { } s.leases = []*Lease{{ - Expiry: time.Unix(leaseExpireStatic, 0), Hostname: staticName, HWAddr: staticMAC, IP: staticIP, diff --git a/internal/filtering/servicelist.go b/internal/filtering/servicelist.go index 119b8eb7..7cf33bfc 100644 --- a/internal/filtering/servicelist.go +++ b/internal/filtering/servicelist.go @@ -1438,6 +1438,8 @@ var blockedServices = []blockedService{{ "||mindly.social^", "||mstdn.ca^", "||mstdn.jp^", + "||mstdn.party^", + "||mstdn.plus^", "||mstdn.social^", "||muenchen.social^", "||muenster.im^", @@ -1447,7 +1449,6 @@ var blockedServices = []blockedService{{ "||nrw.social^", "||o3o.ca^", "||ohai.social^", - "||pewtix.com^", "||piaille.fr^", "||pol.social^", "||ravenation.club^", @@ -1469,7 +1470,6 @@ var blockedServices = []blockedService{{ "||techhub.social^", "||theblower.au^", "||tkz.one^", - "||todon.eu^", "||toot.aquilenet.fr^", "||toot.community^", "||toot.funami.tech^", diff --git a/internal/home/clients_test.go b/internal/home/clients_test.go index 410ef6d4..ebf879ef 100644 --- a/internal/home/clients_test.go +++ b/internal/home/clients_test.go @@ -3,14 +3,12 @@ package home import ( "net" "net/netip" - "os" "runtime" "testing" "time" "github.com/AdguardTeam/AdGuardHome/internal/dhcpd" "github.com/AdguardTeam/AdGuardHome/internal/filtering" - "github.com/AdguardTeam/golibs/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -283,8 +281,8 @@ func TestClientsAddExisting(t *testing.T) { // First, init a DHCP server with a single static lease. config := &dhcpd.ServerConfig{ - Enabled: true, - DBFilePath: "leases.db", + Enabled: true, + DataDir: t.TempDir(), Conf4: dhcpd.V4ServerConf{ Enabled: true, GatewayIP: netip.MustParseAddr("1.2.3.1"), @@ -296,9 +294,6 @@ func TestClientsAddExisting(t *testing.T) { dhcpServer, err := dhcpd.Create(config) require.NoError(t, err) - testutil.CleanupAndRequireSuccess(t, func() (err error) { - return os.Remove("leases.db") - }) clients.dhcpServer = dhcpServer diff --git a/internal/home/clientshttp.go b/internal/home/clientshttp.go index 9a948d1e..82a16713 100644 --- a/internal/home/clientshttp.go +++ b/internal/home/clientshttp.go @@ -6,6 +6,7 @@ import ( "net/http" "net/netip" + "github.com/AdguardTeam/AdGuardHome/internal/aghalg" "github.com/AdguardTeam/AdGuardHome/internal/aghhttp" "github.com/AdguardTeam/AdGuardHome/internal/filtering" ) @@ -44,6 +45,9 @@ type clientJSON struct { SafeSearchEnabled bool `json:"safesearch_enabled"` UseGlobalBlockedServices bool `json:"use_global_blocked_services"` UseGlobalSettings bool `json:"use_global_settings"` + + IgnoreQueryLog aghalg.NullBool `json:"ignore_querylog"` + IgnoreStatistics aghalg.NullBool `json:"ignore_statistics"` } type runtimeClientJSON struct { @@ -90,7 +94,7 @@ func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, r *http } // jsonToClient converts JSON object to Client object. -func (clients *clientsContainer) jsonToClient(cj clientJSON) (c *Client, err error) { +func (clients *clientsContainer) jsonToClient(cj clientJSON, prev *Client) (c *Client, err error) { var safeSearchConf filtering.SafeSearchConfig if cj.SafeSearchConf != nil { safeSearchConf = *cj.SafeSearchConf @@ -129,6 +133,18 @@ func (clients *clientsContainer) jsonToClient(cj clientJSON) (c *Client, err err UseOwnBlockedServices: !cj.UseGlobalBlockedServices, } + if cj.IgnoreQueryLog != aghalg.NBNull { + c.IgnoreQueryLog = cj.IgnoreQueryLog == aghalg.NBTrue + } else if prev != nil { + c.IgnoreQueryLog = prev.IgnoreQueryLog + } + + if cj.IgnoreStatistics != aghalg.NBNull { + c.IgnoreStatistics = cj.IgnoreStatistics == aghalg.NBTrue + } else if prev != nil { + c.IgnoreStatistics = prev.IgnoreStatistics + } + if safeSearchConf.Enabled { err = c.setSafeSearch( safeSearchConf, @@ -165,6 +181,9 @@ func clientToJSON(c *Client) (cj *clientJSON) { BlockedServices: c.BlockedServices, Upstreams: c.Upstreams, + + IgnoreQueryLog: aghalg.BoolToNullBool(c.IgnoreQueryLog), + IgnoreStatistics: aghalg.BoolToNullBool(c.IgnoreStatistics), } } @@ -178,7 +197,7 @@ func (clients *clientsContainer) handleAddClient(w http.ResponseWriter, r *http. return } - c, err := clients.jsonToClient(cj) + c, err := clients.jsonToClient(cj, nil) if err != nil { aghhttp.Error(r, w, http.StatusBadRequest, "%s", err) @@ -232,6 +251,8 @@ type updateJSON struct { } // handleUpdateClient is the handler for POST /control/clients/update HTTP API. +// +// TODO(s.chzhen): Accept updated parameters instead of whole structure. func (clients *clientsContainer) handleUpdateClient(w http.ResponseWriter, r *http.Request) { dj := updateJSON{} err := json.NewDecoder(r.Body).Decode(&dj) @@ -247,7 +268,21 @@ func (clients *clientsContainer) handleUpdateClient(w http.ResponseWriter, r *ht return } - c, err := clients.jsonToClient(dj.Data) + var prev *Client + var ok bool + + func() { + clients.lock.Lock() + defer clients.lock.Unlock() + + prev, ok = clients.list[dj.Name] + }() + + if !ok { + aghhttp.Error(r, w, http.StatusBadRequest, "client not found") + } + + c, err := clients.jsonToClient(dj.Data, prev) if err != nil { aghhttp.Error(r, w, http.StatusBadRequest, "%s", err) diff --git a/internal/home/config.go b/internal/home/config.go index 8eb7694f..c97e671c 100644 --- a/internal/home/config.go +++ b/internal/home/config.go @@ -296,12 +296,26 @@ var config = &configuration{ MaxGoroutines: 300, }, DnsfilterConf: &filtering.Config{ - SafeBrowsingCacheSize: 1 * 1024 * 1024, - SafeSearchCacheSize: 1 * 1024 * 1024, - ParentalCacheSize: 1 * 1024 * 1024, - CacheTime: 30, FilteringEnabled: true, FiltersUpdateIntervalHours: 24, + + ParentalEnabled: false, + SafeBrowsingEnabled: false, + + SafeBrowsingCacheSize: 1 * 1024 * 1024, + SafeSearchCacheSize: 1 * 1024 * 1024, + ParentalCacheSize: 1 * 1024 * 1024, + CacheTime: 30, + + SafeSearchConf: filtering.SafeSearchConfig{ + Enabled: false, + Bing: true, + DuckDuckGo: true, + Google: true, + Pixabay: true, + Yandex: true, + YouTube: true, + }, }, UpstreamTimeout: timeutil.Duration{Duration: dnsforward.DefaultTimeout}, UsePrivateRDNS: true, diff --git a/internal/home/home.go b/internal/home/home.go index 7f9762dc..443bdc0f 100644 --- a/internal/home/home.go +++ b/internal/home/home.go @@ -306,7 +306,9 @@ func setupConfig(opts options) (err error) { return fmt.Errorf("initializing safesearch: %w", err) } + //lint:ignore SA1019 Migration is not over. config.DHCP.WorkDir = Context.workDir + config.DHCP.DataDir = Context.getDataDir() config.DHCP.HTTPRegister = httpRegister config.DHCP.ConfigModified = onConfigModified diff --git a/internal/home/options.go b/internal/home/options.go index 9d435b6e..1cfac3d9 100644 --- a/internal/home/options.go +++ b/internal/home/options.go @@ -249,13 +249,15 @@ var cmdLineOpts = []cmdLineOpt{{ updateNoValue: func(o options) (options, error) { o.noEtcHosts = true; return o, nil }, effect: func(_ options, _ string) (f effect, err error) { log.Info( - "warning: --no-etc-hosts flag is deprecated and will be removed in the future versions", + "warning: --no-etc-hosts flag is deprecated " + + "and will be removed in the future versions; " + + "set clients.runtime_sources.hosts in the configuration file to false instead", ) return nil, nil }, serialize: func(o options) (val string, ok bool) { return "", o.noEtcHosts }, - description: "Deprecated. Do not use the OS-provided hosts.", + description: "Deprecated: use clients.runtime_sources.hosts instead. Do not use the OS-provided hosts.", longName: "no-etc-hosts", shortName: "", }, { diff --git a/internal/querylog/entry.go b/internal/querylog/entry.go index 0433744c..eae99385 100644 --- a/internal/querylog/entry.go +++ b/internal/querylog/entry.go @@ -58,11 +58,11 @@ func (e *logEntry) addResponse(resp *dns.Msg, isOrig bool) { var err error if isOrig { - e.Answer, err = resp.Pack() - err = errors.Annotate(err, "packing answer: %w") - } else { e.OrigAnswer, err = resp.Pack() err = errors.Annotate(err, "packing orig answer: %w") + } else { + e.Answer, err = resp.Pack() + err = errors.Annotate(err, "packing answer: %w") } if err != nil { log.Error("querylog: %s", err) diff --git a/internal/querylog/search.go b/internal/querylog/search.go index e371111d..db2d3474 100644 --- a/internal/querylog/search.go +++ b/internal/querylog/search.go @@ -288,6 +288,10 @@ func (l *queryLog) readNextEntry( // Go on and try to match anyway. } + if e.client != nil && e.client.IgnoreQueryLog { + return nil, ts, nil + } + ts = e.Time.UnixNano() if !params.match(e) { return nil, ts, nil diff --git a/internal/stats/unit.go b/internal/stats/unit.go index fc635075..8de01aa0 100644 --- a/internal/stats/unit.go +++ b/internal/stats/unit.go @@ -423,7 +423,7 @@ func (s *StatsCtx) getData(limit uint32) (StatsResp, bool) { ReplacedParental: statsCollector(units, firstID, timeUnit, func(u *unitDB) (num uint64) { return u.NResult[RParental] }), TopQueried: topsCollector(units, maxDomains, s.ignored, func(u *unitDB) (pairs []countPair) { return u.Domains }), TopBlocked: topsCollector(units, maxDomains, s.ignored, func(u *unitDB) (pairs []countPair) { return u.BlockedDomains }), - TopClients: topsCollector(units, maxClients, nil, func(u *unitDB) (pairs []countPair) { return u.Clients }), + TopClients: topsCollector(units, maxClients, nil, topClientPairs(s)), } // Total counters: @@ -460,3 +460,17 @@ func (s *StatsCtx) getData(limit uint32) (StatsResp, bool) { return data, true } + +func topClientPairs(s *StatsCtx) (pg pairsGetter) { + return func(u *unitDB) (clients []countPair) { + for _, c := range u.Clients { + if c.Name != "" && !s.shouldCountClient([]string{c.Name}) { + continue + } + + clients = append(clients, c) + } + + return clients + } +} diff --git a/internal/tools/go.mod b/internal/tools/go.mod index b52e6e0b..9795d39d 100644 --- a/internal/tools/go.mod +++ b/internal/tools/go.mod @@ -9,10 +9,10 @@ require ( github.com/kisielk/errcheck v1.6.3 github.com/kyoh86/looppointer v0.2.1 github.com/securego/gosec/v2 v2.15.0 - golang.org/x/tools v0.7.0 - golang.org/x/vuln v0.0.0-20230404205743-41aec7335792 + golang.org/x/tools v0.8.0 + golang.org/x/vuln v0.0.0-20230418010118-28ba02ac73db honnef.co/go/tools v0.4.3 - mvdan.cc/gofumpt v0.4.0 + mvdan.cc/gofumpt v0.5.0 mvdan.cc/unparam v0.0.0-20230312165513-e84e2d14e3b8 ) diff --git a/internal/tools/go.sum b/internal/tools/go.sum index 82a84b72..21f23c63 100644 --- a/internal/tools/go.sum +++ b/internal/tools/go.sum @@ -3,7 +3,7 @@ github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= @@ -21,7 +21,7 @@ github.com/gordonklaus/ineffassign v0.0.0-20230107090616-13ace0543b28 h1:9alfqbr github.com/gordonklaus/ineffassign v0.0.0-20230107090616-13ace0543b28/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0= github.com/kisielk/errcheck v1.6.3 h1:dEKh+GLHcWm2oN34nMvDzn1sqI0i0WxPvrgiJA5JuM8= github.com/kisielk/errcheck v1.6.3/go.mod h1:nXw/i/MfnvRHqXa7XXmQMUB0oNFGuBrNI8d8NLy0LPw= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kyoh86/looppointer v0.2.1 h1:Jx9fnkBj/JrIryBLMTYNTj9rvc2SrPS98Dg0w7fxdJg= github.com/kyoh86/looppointer v0.2.1/go.mod h1:q358WcM8cMWU+5vzqukvaZtnJi1kw/MpRHQm3xvTrjw= @@ -31,10 +31,9 @@ github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 h1:4kuARK6Y6Fx github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354/go.mod h1:KSVJerMDfblTH7p5MZaTt+8zaT2iEk3AkVb9PQdZuE8= github.com/onsi/ginkgo/v2 v2.8.0 h1:pAM+oBNPrpXRs+E/8spkeGx9QgekbRVyr74EUvRVOUI= github.com/onsi/gomega v1.26.0 h1:03cDLK28U6hWvCAns6NeydX3zIm4SF3ci69ulidS32Q= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/securego/gosec/v2 v2.15.0 h1:v4Ym7FF58/jlykYmmhZ7mTm7FQvN/setNm++0fgIAtw= github.com/securego/gosec/v2 v2.15.0/go.mod h1:VOjTrZOkUtSDt2QLSJmQBMWnvwiQPEjg0l+5juIqGk8= github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -63,7 +62,7 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -91,10 +90,10 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20201007032633-0806396f153e/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= -golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= -golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= -golang.org/x/vuln v0.0.0-20230404205743-41aec7335792 h1:NybXXIgk5dslpSHRStwyfI74htFvi9Cyk3mCr9ubE2I= -golang.org/x/vuln v0.0.0-20230404205743-41aec7335792/go.mod h1:8gQW8OCBfaUiPaWAPDQf/9V1w+ymmmB/05SwB/EXZNs= +golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y= +golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= +golang.org/x/vuln v0.0.0-20230418010118-28ba02ac73db h1:tLxfII6jPR3mfwEMkyOakawu+Lldo9hIA7vliXnDZYg= +golang.org/x/vuln v0.0.0-20230418010118-28ba02ac73db/go.mod h1:64LpnL2PuSMzFYeCmJjYiRbroOUG9aCZYznINnF5PHE= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -104,7 +103,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.4.3 h1:o/n5/K5gXqk8Gozvs2cnL0F2S1/g1vcGCAx2vETjITw= honnef.co/go/tools v0.4.3/go.mod h1:36ZgoUOrqOk1GxwHhyryEkq8FQWkUO2xGuSMhUCcdvA= -mvdan.cc/gofumpt v0.4.0 h1:JVf4NN1mIpHogBj7ABpgOyZc65/UUOkKQFkoURsz4MM= -mvdan.cc/gofumpt v0.4.0/go.mod h1:PljLOHDeZqgS8opHRKLzp2It2VBuSdteAgqUfzMTxlQ= +mvdan.cc/gofumpt v0.5.0 h1:0EQ+Z56k8tXjj/6TQD25BFNKQXpCvT0rnansIc7Ug5E= +mvdan.cc/gofumpt v0.5.0/go.mod h1:HBeVDtMKRZpXyxFciAirzdKklDlGu8aAy1wEbH5Y9js= mvdan.cc/unparam v0.0.0-20230312165513-e84e2d14e3b8 h1:VuJo4Mt0EVPychre4fNlDWDuE5AjXtPJpRUWqZDQhaI= mvdan.cc/unparam v0.0.0-20230312165513-e84e2d14e3b8/go.mod h1:Oh/d7dEtzsNHGOq1Cdv8aMm3KdKhVvPbRQcM8WFpBR8= diff --git a/openapi/CHANGELOG.md b/openapi/CHANGELOG.md index d710bb88..922788bb 100644 --- a/openapi/CHANGELOG.md +++ b/openapi/CHANGELOG.md @@ -4,6 +4,18 @@ ## v0.108.0: API changes +## v0.107.29: API changes + +### `GET /control/clients` And `GET /control/clients/find` +* The new optional fields `"ignore_querylog"` and `"ignore_statistics"` are set + if AdGuard Home excludes client activity from query log or statistics. + +### `POST /control/clients/add` And `POST /control/clients/update` +* The new optional fields `"ignore_querylog"` and `"ignore_statistics"` make + AdGuard Home exclude client activity from query log or statistics. If not + set AdGuard Home will use default value (false). It can be changed in the + future versions. + ## v0.107.27: API changes ### The new optional fields `"edns_cs_use_custom"` and `"edns_cs_custom_ip"` in `DNSConfig` diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 6d1d02f3..6d74d4fd 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -2494,6 +2494,26 @@ 'items': 'type': 'string' 'type': 'array' + 'ignore_querylog': + 'description': | + NOTE: If `ignore_querylog` is not set in HTTP API `GET /clients/add` + request then default value (false) will be used. + + If `ignore_querylog` is not set in HTTP API `GET /clients/update` + request then the existing value will not be changed. + + This behaviour can be changed in the future versions. + 'type': 'boolean' + 'ignore_statistics': + 'description': | + NOTE: If `ignore_statistics` is not set in HTTP API `GET + /clients/add` request then default value (false) will be used. + + If `ignore_statistics` is not set in HTTP API `GET /clients/update` + request then the existing value will not be changed. + + This behaviour can be changed in the future versions. + 'type': 'boolean' 'ClientAuto': 'type': 'object' 'description': 'Auto-Client information' @@ -2547,6 +2567,8 @@ 'whois_info': {} 'disallowed': false 'disallowed_rule': '' + 'ignore_querylog': false + 'ignore_statistics': false - '1.2.3.4': 'name': 'Client 1-2-3-4' 'ids': ['1.2.3.4'] @@ -2562,6 +2584,8 @@ 'whois_info': {} 'disallowed': false 'disallowed_rule': '' + 'ignore_querylog': false + 'ignore_statistics': false 'AccessListResponse': '$ref': '#/components/schemas/AccessList' 'AccessSetRequest': @@ -2643,7 +2667,10 @@ set to true, and this string is empty, then the client IP is disallowed by the "allowed IP list", that is it is not included in the allowed list. - + 'ignore_querylog': + 'type': 'boolean' + 'ignore_statistics': + 'type': 'boolean' 'WhoisInfo': 'type': 'object' 'additionalProperties': diff --git a/scripts/make/txt-lint.sh b/scripts/make/txt-lint.sh index 5b7c1dd4..06273185 100644 --- a/scripts/make/txt-lint.sh +++ b/scripts/make/txt-lint.sh @@ -3,7 +3,7 @@ # This comment is used to simplify checking local copies of the script. Bump # this number every time a remarkable change is made to this script. # -# AdGuard-Project-Version: 2 +# AdGuard-Project-Version: 3 verbose="${VERBOSE:-0}" readonly verbose @@ -31,12 +31,11 @@ set -f -u # trailing_newlines is a simple check that makes sure that all plain-text files # have a trailing newlines to make sure that all tools work correctly with them. -# -# TODO(a.garipov): Add to the standard skeleton project. trailing_newlines() { nl="$( printf "\n" )" readonly nl + # NOTE: Adjust for your project. git ls-files\ ':!*.png'\ ':!*.tar.gz'\