Merge with master

This commit is contained in:
Andrey Meshkov 2019-03-22 10:36:48 +03:00
commit 7ebfaccc6e
76 changed files with 3489 additions and 1135 deletions

View File

@ -2,7 +2,7 @@ language: go
sudo: false sudo: false
go: go:
- 1.11.x - 1.12.x
- 1.x - 1.x
os: os:
- linux - linux
@ -38,7 +38,7 @@ matrix:
# Release build configuration # Release build configuration
- name: release - name: release
go: go:
- 1.11.x - 1.12.x
os: os:
- linux - linux
@ -66,7 +66,7 @@ matrix:
- name: docker - name: docker
if: type != pull_request AND (branch = master OR tag IS present) if: type != pull_request AND (branch = master OR tag IS present)
go: go:
- 1.11.x - 1.12.x
os: os:
- linux - linux
services: services:

View File

@ -70,7 +70,7 @@ Alternatively, you can use our [official Docker image](https://hub.docker.com/r/
You will need: You will need:
* [go](https://golang.org/dl/) v1.11 or later. * [go](https://golang.org/dl/) v1.12 or later.
* [node.js](https://nodejs.org/en/download/) v10 or later. * [node.js](https://nodejs.org/en/download/) v10 or later.
You can either install it via the provided links or use [brew.sh](https://brew.sh/) if you're on Mac: You can either install it via the provided links or use [brew.sh](https://brew.sh/) if you're on Mac:

76
app.go
View File

@ -3,7 +3,6 @@ package main
import ( import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
stdlog "log"
"net" "net"
"net/http" "net/http"
"os" "os"
@ -13,11 +12,9 @@ import (
"strconv" "strconv"
"sync" "sync"
"syscall" "syscall"
"time"
"github.com/AdguardTeam/golibs/log"
"github.com/gobuffalo/packr" "github.com/gobuffalo/packr"
"github.com/hmage/golibs/log"
) )
// VersionString will be set through ldflags, contains current version // VersionString will be set through ldflags, contains current version
@ -71,11 +68,14 @@ func run(args options) {
// configure log level and output // configure log level and output
configureLogger(args) configureLogger(args)
// enable TLS 1.3
enableTLS13()
// print the first message after logger is configured // print the first message after logger is configured
log.Printf("AdGuard Home, version %s\n", VersionString) log.Printf("AdGuard Home, version %s\n", VersionString)
log.Tracef("Current working directory is %s", config.ourWorkingDir) log.Debug("Current working directory is %s", config.ourWorkingDir)
if args.runningAsService { if args.runningAsService {
log.Printf("AdGuard Home is running as a service") log.Info("AdGuard Home is running as a service")
} }
config.firstRun = detectFirstRun() config.firstRun = detectFirstRun()
@ -100,24 +100,7 @@ func run(args options) {
config.BindPort = args.bindPort config.BindPort = args.bindPort
} }
// Load filters from the disk loadFilters()
// And if any filter has zero ID, assign a new one
for i := range config.Filters {
filter := &config.Filters[i] // otherwise we're operating on a copy
if filter.ID == 0 {
filter.ID = assignUniqueFilterID()
}
err = filter.load()
if err != nil {
// This is okay for the first start, the filter will be loaded later
log.Printf("Couldn't load filter %d contents due to %s", filter.ID, err)
// clear LastUpdated so it gets fetched right away
}
if len(filter.Rules) == 0 {
filter.LastUpdated = time.Time{}
}
}
// Save the updated config // Save the updated config
err = config.write() err = config.write()
@ -144,11 +127,6 @@ func run(args options) {
// Update filters we've just loaded right away, don't wait for periodic update timer // Update filters we've just loaded right away, don't wait for periodic update timer
go func() { go func() {
refreshFiltersIfNecessary(false) refreshFiltersIfNecessary(false)
// Save the updated config
err := config.write()
if err != nil {
log.Fatal(err)
}
}() }()
// Schedule automatic filters updates // Schedule automatic filters updates
go periodicallyRefreshFilters() go periodicallyRefreshFilters()
@ -161,7 +139,7 @@ func run(args options) {
// add handlers for /install paths, we only need them when we're not configured yet // add handlers for /install paths, we only need them when we're not configured yet
if config.firstRun { if config.firstRun {
log.Printf("This is the first launch of AdGuard Home, redirecting everything to /install.html ") log.Info("This is the first launch of AdGuard Home, redirecting everything to /install.html ")
http.Handle("/install.html", preInstallHandler(http.FileServer(box))) http.Handle("/install.html", preInstallHandler(http.FileServer(box)))
registerInstallHandlers() registerInstallHandlers()
} }
@ -178,13 +156,13 @@ func run(args options) {
} }
address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.TLS.PortHTTPS)) address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.TLS.PortHTTPS))
// validate current TLS config and update warnings (it could have been loaded from file) // validate current TLS config and update warnings (it could have been loaded from file)
data := validateCertificates(config.TLS) data := validateCertificates(config.TLS.CertificateChain, config.TLS.PrivateKey, config.TLS.ServerName)
if !data.usable { if !data.ValidPair {
log.Fatal(data.WarningValidation) log.Fatal(data.WarningValidation)
os.Exit(1) os.Exit(1)
} }
config.Lock() config.Lock()
config.TLS = data // update warnings config.TLS.tlsConfigStatus = data // update warnings
config.Unlock() config.Unlock()
// prepare certs for HTTPS server // prepare certs for HTTPS server
@ -263,7 +241,11 @@ func configureLogger(args options) {
ls.LogFile = args.logFile ls.LogFile = args.logFile
} }
log.Verbose = ls.Verbose level := log.INFO
if ls.Verbose {
level = log.DEBUG
}
log.SetLevel(level)
if args.runningAsService && ls.LogFile == "" && runtime.GOOS == "windows" { if args.runningAsService && ls.LogFile == "" && runtime.GOOS == "windows" {
// When running as a Windows service, use eventlog by default if nothing else is configured // When running as a Windows service, use eventlog by default if nothing else is configured
@ -283,24 +265,36 @@ func configureLogger(args options) {
} }
} else { } else {
logFilePath := filepath.Join(config.ourWorkingDir, ls.LogFile) logFilePath := filepath.Join(config.ourWorkingDir, ls.LogFile)
file, err := os.OpenFile(logFilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0755) if filepath.IsAbs(ls.LogFile) {
logFilePath = ls.LogFile
}
file, err := os.OpenFile(logFilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil { if err != nil {
log.Fatalf("cannot create a log file: %s", err) log.Fatalf("cannot create a log file: %s", err)
} }
stdlog.SetOutput(file) log.SetOutput(file)
}
}
// TODO after GO 1.13 release TLS 1.3 will be enabled by default. Remove this afterward
func enableTLS13() {
err := os.Setenv("GODEBUG", os.Getenv("GODEBUG")+",tls13=1")
if err != nil {
log.Fatalf("Failed to enable TLS 1.3: %s", err)
} }
} }
func cleanup() { func cleanup() {
log.Printf("Stopping AdGuard Home") log.Info("Stopping AdGuard Home")
err := stopDNSServer() err := stopDNSServer()
if err != nil { if err != nil {
log.Printf("Couldn't stop DNS server: %s", err) log.Error("Couldn't stop DNS server: %s", err)
} }
err = stopDHCPServer() err = stopDHCPServer()
if err != nil { if err != nil {
log.Printf("Couldn't stop DHCP server: %s", err) log.Error("Couldn't stop DHCP server: %s", err)
} }
} }
@ -373,7 +367,7 @@ func loadOptions() options {
if v == "--"+opt.longName || (opt.shortName != "" && v == "-"+opt.shortName) { if v == "--"+opt.longName || (opt.shortName != "" && v == "-"+opt.shortName) {
if opt.callbackWithValue != nil { if opt.callbackWithValue != nil {
if i+1 >= len(os.Args) { if i+1 >= len(os.Args) {
log.Printf("ERROR: Got %s without argument\n", v) log.Error("Got %s without argument\n", v)
os.Exit(64) os.Exit(64)
} }
i++ i++
@ -386,7 +380,7 @@ func loadOptions() options {
} }
} }
if !knownParam { if !knownParam {
log.Printf("ERROR: unknown option %v\n", v) log.Error("unknown option %v\n", v)
printHelp() printHelp()
os.Exit(64) os.Exit(64)
} }

32
client/package-lock.json generated vendored
View File

@ -3260,6 +3260,38 @@
"integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=",
"dev": true "dev": true
}, },
"copy-webpack-plugin": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-4.6.0.tgz",
"integrity": "sha512-Y+SQCF+0NoWQryez2zXn5J5knmr9z/9qSQt7fbL78u83rxmigOy8X5+BFn8CFSuX+nKT8gpYwJX68ekqtQt6ZA==",
"dev": true,
"requires": {
"cacache": "^10.0.4",
"find-cache-dir": "^1.0.0",
"globby": "^7.1.1",
"is-glob": "^4.0.0",
"loader-utils": "^1.1.0",
"minimatch": "^3.0.4",
"p-limit": "^1.0.0",
"serialize-javascript": "^1.4.0"
},
"dependencies": {
"globby": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/globby/-/globby-7.1.1.tgz",
"integrity": "sha1-+yzP+UAfhgCUXfral0QMypcrhoA=",
"dev": true,
"requires": {
"array-union": "^1.0.1",
"dir-glob": "^2.0.0",
"glob": "^7.1.2",
"ignore": "^3.3.5",
"pify": "^3.0.0",
"slash": "^1.0.0"
}
}
}
},
"core-js": { "core-js": {
"version": "1.2.7", "version": "1.2.7",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz",

1
client/package.json vendored
View File

@ -47,6 +47,7 @@
"babel-runtime": "6.26.0", "babel-runtime": "6.26.0",
"clean-webpack-plugin": "^0.1.19", "clean-webpack-plugin": "^0.1.19",
"compression-webpack-plugin": "^1.1.11", "compression-webpack-plugin": "^1.1.11",
"copy-webpack-plugin": "^4.6.0",
"css-loader": "^0.28.11", "css-loader": "^0.28.11",
"eslint": "^4.19.1", "eslint": "^4.19.1",
"eslint-config-airbnb-base": "^12.1.0", "eslint-config-airbnb-base": "^12.1.0",

BIN
client/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -4,7 +4,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000"> <meta name="theme-color" content="#000000">
<link rel="shortcut icon" href="https://adguard.com/img/favicons/favicon.ico"> <link rel="shortcut icon" href="favicon.ico">
<title>AdGuard Home</title> <title>AdGuard Home</title>
</head> </head>
<body> <body>

View File

@ -4,7 +4,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000"> <meta name="theme-color" content="#000000">
<link rel="shortcut icon" href="https://adguard.com/img/favicons/favicon.ico"> <link rel="shortcut icon" href="favicon.ico">
<title>Setup AdGuard Home</title> <title>Setup AdGuard Home</title>
</head> </head>
<body> <body>

View File

@ -0,0 +1,250 @@
{
"url_added_successfully": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0435\u043d URL",
"check_dhcp_servers": "\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u0437\u0430 \u0430\u043a\u0442\u0438\u0432\u0435\u043d DHCP \u0441\u044a\u0440\u0432\u044a\u0440",
"save_config": "\u0417\u0430\u043f\u0438\u0448\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u0442\u0435",
"enabled_dhcp": "DHCP \u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d",
"disabled_dhcp": "DHCP \u0435 \u0437\u0430\u0431\u0440\u0430\u043d\u0435\u043d",
"dhcp_title": "DHCP \u0441\u044a\u0440\u0432\u044a\u0440 (\u0442\u0435\u0441\u0442\u043e\u0432\u0438!)",
"dhcp_description": "\u0410\u043a\u043e \u0440\u0443\u0442\u0435\u0440\u0430 \u0432\u0438 \u043d\u0435 \u0440\u0430\u0437\u0434\u0430\u0432\u0430 DHCP \u0430\u0434\u0440\u0435\u0441\u0438, \u043c\u043e\u0436\u0435 \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 \u0432\u0433\u0440\u0430\u0434\u0435\u043d\u0438\u044f \u0432 AdGuard DHCP \u0441\u044a\u0440\u0432\u044a\u0440.",
"dhcp_enable": "\u0420\u0437\u0440\u0435\u0448\u0438 DHCP \u0441\u044a\u0440\u0432\u044a\u0440\u0430",
"dhcp_disable": "\u0417\u0430\u0431\u0440\u0430\u043d\u0438 DHCP \u0441\u044a\u0440\u0432\u044a\u0440\u0430",
"dhcp_not_found": "\u0412\u0430\u0448\u0430\u0442\u0430 \u043c\u0440\u0435\u0436\u0430 \u043d\u044f\u043c\u0430 \u0430\u043a\u0442\u0438\u0432\u0435\u043d DHCP \u0441\u044a\u0440\u0432\u044a\u0440. \u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e \u0435 \u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435\u0442\u043e \u043d\u0430 \u0432\u0433\u0440\u0430\u0434\u0435\u043d\u0438\u044f DHCP \u0441\u044a\u0440\u0432\u044a\u0440.",
"dhcp_found": "\u0412\u0430\u0448\u0430\u0442\u0430 \u043c\u0440\u0435\u0436\u0430 \u0432\u0435\u0447\u0435 \u0438\u043c\u0430 \u0430\u043a\u0442\u0438\u0432\u0435\u043d DHCP \u0441\u044a\u0440\u0432\u044a\u0440. \u041d\u0435 \u0435 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e \u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435\u0442\u043e \u043d\u0430 \u0432\u0442\u043e\u0440\u0438!",
"dhcp_leases": "DHCP \u0440\u0430\u0437\u0434\u0430\u0434\u0435\u043d\u0438 \u0430\u0434\u0440\u0435\u0441\u0438",
"dhcp_leases_not_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0430\u043a\u0442\u0438\u0432\u043d\u0438 DHCP \u0430\u0434\u0440\u0435\u0441\u0438",
"dhcp_config_saved": "\u0417\u0430\u043f\u0438\u0448\u0438 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 DHCP \u0441\u044a\u0440\u0432\u044a\u0440\u0430",
"form_error_required": "\u0417\u0430\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u043f\u043e\u043b\u0435",
"form_error_ip_format": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d IPv4 \u0430\u0434\u0440\u0435\u0441",
"form_error_positive": "\u041f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0434\u0430\u043b\u0438 \u0435 \u043f\u043e\u043b\u043e\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u0447\u0438\u0441\u043b\u043e",
"dhcp_form_gateway_input": "IP \u0448\u043b\u044e\u0437",
"dhcp_form_subnet_input": "\u041c\u0440\u0435\u0436\u043e\u0432\u0430 \u043c\u0430\u0441\u043a\u0430",
"dhcp_form_range_title": "\u0413\u0440\u0443\u043f\u0430 \u043e\u0442 IP \u0430\u0434\u0440\u0435\u0441\u0438",
"dhcp_form_range_start": "\u041f\u044a\u0440\u0432\u0438 \u0430\u0434\u0440\u0435\u0441",
"dhcp_form_range_end": "\u041f\u043e\u0441\u043b\u0435\u0434\u0435\u043d \u0430\u0434\u0440\u0435\u0441",
"dhcp_form_lease_title": "\u041e\u0442\u0434\u0430\u0434\u0435\u043d\u0438 \u0430\u0434\u0440\u0435\u0441\u0438 (\u0441\u0435\u043a\u0443\u043d\u0434\u0438)",
"dhcp_form_lease_input": "\u041e\u0442\u0447\u0435\u0442 \u0437\u0430 \u0440\u0430\u0437\u0434\u0430\u0434\u0435\u043d\u0438 \u0430\u0434\u0440\u0435\u0441\u0438",
"dhcp_interface_select": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043c\u0440\u0435\u0436\u043e\u0432 \u0430\u0434\u0430\u043f\u0442\u0435\u0440 \u0437\u0430 DHCP",
"dhcp_hardware_address": "\u0425\u0430\u0440\u0434\u0443\u0435\u0440\u043d\u0438 \u0430\u0434\u0440\u0435\u0441\u0438 (MAC)",
"dhcp_ip_addresses": "IP \u0430\u0434\u0440\u0435\u0441\u0438",
"dhcp_table_hostname": "\u0418\u043c\u0435 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e",
"dhcp_table_expires": "\u0418\u0441\u0442\u043e\u0440\u0438\u044f",
"dhcp_warning": "\u0410\u043a\u043e \u0438\u0441\u043a\u0430\u0442\u0435 \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 \u0432\u0433\u0440\u0430\u0434\u0435\u043d\u0438\u044f DHCP \u0441\u044a\u0440\u0432\u044a\u0440, \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043d\u044f\u043c\u0430 \u0434\u0440\u0443\u0433 \u0430\u043a\u0442\u0438\u0432\u0435\u043d DHCP \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430 \u0412\u0438!",
"back": "\u041d\u0430\u0437\u0430\u0434",
"dashboard": "\u0422\u0430\u0431\u043b\u043e",
"settings": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438",
"filters": "\u0424\u0438\u043b\u0442\u0440\u0438",
"query_log": "\u0418\u0441\u0442\u043e\u0440\u0438\u044f \u043d\u0430 \u0437\u0430\u044f\u0432\u043a\u0438\u0442\u0435",
"faq": "\u0427\u0417\u0412",
"version": "\u0432\u0435\u0440\u0441\u0438\u044f",
"address": "\u0430\u0434\u0440\u0435\u0441",
"on": "\u0412\u041a\u041b\u042e\u0427\u0415\u041d\u041e",
"off": "\u0418\u0417\u041a\u041b\u042e\u0427\u0415\u041d\u041e",
"copyright": "\u0410\u0432\u0442\u043e\u0440\u0441\u043a\u043e \u043f\u0440\u0430\u0432\u043e",
"homepage": "\u0414\u043e\u043c\u0430\u0448\u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430",
"report_an_issue": "\u0421\u044a\u043e\u0431\u0449\u0438 \u0437\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c",
"enable_protection": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438 \u0437\u0430\u0449\u0438\u0442\u0430",
"enabled_protection": "\u0417\u0430\u0449\u0438\u0442\u0430\u0442\u0430 \u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430",
"disable_protection": "\u0417\u0430\u0431\u0440\u0430\u043d\u0438 \u0437\u0430\u0449\u0438\u0442\u0430",
"disabled_protection": "\u0417\u0430\u0449\u0438\u0442\u0430\u0442\u0430 \u0435 \u0437\u0430\u0431\u0440\u0430\u043d\u0435\u043d\u0430",
"refresh_statics": "\u041e\u0431\u043d\u043e\u0432\u0438 \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0430\u0442\u0430",
"dns_query": "DNS \u0437\u0430\u043f\u0438\u0442\u0432\u0430\u043d\u0438\u044f",
"blocked_by": "\u0411\u043b\u043e\u043a\u0438\u0440\u0430\u043d\u0438 \u043e\u0442",
"stats_malware_phishing": "\u0432\u0438\u0440\u0443\u0441\u0438\/\u0430\u0442\u0430\u043a\u0438",
"stats_adult": "\u0441\u0430\u0439\u0442\u043e\u0432\u0435 \u0437\u0430 \u0432\u044a\u0437\u0440\u0430\u0441\u0442\u043d\u0438",
"stats_query_domain": "\u041d\u0430\u0439-\u043e\u0442\u0432\u0430\u0440\u044f\u043d\u0438 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0438",
"for_last_24_hours": "\u0437\u0430 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0442\u0435 24 \u0447\u0430\u0441\u0430",
"no_domains_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0440\u0435\u0437\u0443\u043b\u0442\u0430\u0442\u0438",
"requests_count": "\u0421\u0443\u043c\u0430 \u043d\u0430 \u0437\u0430\u044f\u0432\u043a\u0438\u0442\u0435",
"top_blocked_domains": "\u041d\u0430\u0439-\u0431\u043b\u043e\u043a\u0438\u0440\u0430\u043d\u0438 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0438",
"top_clients": "\u041d\u0430\u0439-\u0430\u043a\u0442\u0438\u0432\u043d\u0438 IP \u0430\u0434\u0440\u0435\u0441\u0438",
"no_clients_found": "\u041d\u044f\u043ca \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0430\u0434\u0440\u0435\u0441\u0438",
"general_statistics": "\u041e\u0431\u0449\u0430 \u0441\u0442\u0430\u0442\u0438\u0441\u0438\u043a\u0430",
"number_of_dns_query_24_hours": "\u0421\u0443\u043c\u0430 \u043d\u0430 DNS \u0437\u0430\u044f\u0432\u043a\u0438 \u0437\u0430 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0442\u0435 24 \u0447\u0430\u0441\u0430",
"number_of_dns_query_blocked_24_hours": "\u0421\u0443\u043c\u0430 \u043d\u0430 \u0431\u043b\u043e\u043a\u0438\u0440\u0430\u043d\u0438 DNS \u0437\u0430\u044f\u0432\u043a\u0438 \u043e\u0442 \u0444\u0438\u043b\u0442\u0440\u0438\u0442\u0435 \u0437\u0430 \u0440\u0435\u043a\u043b\u0430\u043c\u0430 \u0438 \u043c\u0435\u0441\u0442\u043d\u0438",
"number_of_dns_query_blocked_24_hours_by_sec": "\u0421\u0443\u043c\u0430 \u043d\u0430 \u0431\u043b\u043e\u043a\u0438\u0440\u0430\u043d\u0438 DNS \u0437\u0430\u044f\u0432\u043a\u0438 \u043e\u0442 AdGuard \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u0438 \u0441\u044a\u0441 \u0441\u0438\u0433\u0443\u0440\u043d\u043e\u0441\u0442\u0442\u0430",
"number_of_dns_query_blocked_24_hours_adult": "\u0421\u0443\u043c\u0430 \u043d\u0430 \u0431\u043b\u043e\u043a\u0438\u0440\u0430\u043d\u0438 \u0441\u0430\u0439\u0442\u043e\u0432\u0435 \u0437\u0430 \u0432\u044a\u0437\u0440\u0430\u0441\u0442\u043d\u0438",
"enforced_save_search": "\u0410\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u043e \u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e \u0422\u044a\u0440\u0441\u0435\u043d\u0435",
"number_of_dns_query_to_safe_search": "\u0421\u0443\u043c\u0430 \u043d\u0430 DNS \u0437\u0430\u044f\u0432\u043a\u0438 \u043f\u0440\u0438 \u043a\u043e\u0439\u0442\u043e \u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u043e \u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e \u0422\u044a\u0440\u0441\u0435\u043d\u0435",
"average_processing_time": "\u0421\u0440\u0435\u0434\u043d\u043e \u0432\u0440\u0435\u043c\u0435 \u0437\u0430 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430",
"average_processing_time_hint": "\u0421\u0440\u0435\u0434\u043d\u043e \u0432\u0440\u0435\u043c\u0435 \u0437\u0430 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u043d\u0430 DNS \u0437\u0430\u044f\u0432\u043a\u0438 \u0432 \u043c\u0438\u043b\u0438\u0441\u0435\u043a\u0443\u043d\u0434\u0438",
"block_domain_use_filters_and_hosts": "\u0411\u043b\u043e\u043a\u0438\u0440\u0430\u043d\u0438 \u0434\u043e\u043c\u0435\u0439\u043d\u0438 - \u043e\u0431\u0449\u0438 \u0438 \u043c\u0435\u0441\u0442\u043d\u0438 \u0444\u0438\u043b\u0442\u0440\u0438",
"filters_block_toggle_hint": "\u041c\u043e\u0436\u0435 \u0434\u0430 \u0437\u0430\u0434\u0430\u0434\u0435\u0442\u0435 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432 <a href='#filters'>\u0424\u0438\u043b\u0442\u0440\u0438<\/a>.",
"use_adguard_browsing_sec": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0439\u0442\u0435 AdGuard \u043c\u043e\u0434\u0443\u043b \u0437\u0430 \u0441\u0438\u0433\u0443\u0440\u043d\u043e\u0441\u0442\u0442\u0430",
"use_adguard_browsing_sec_hint": "\u041c\u043e\u0434\u0443\u043b \u0421\u0438\u0433\u0443\u0440\u043d\u043e\u0441\u0442 \u0432 AdGuard Home \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0432\u0430 \u0432\u0441\u044f\u043a\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430 \u043a\u043e\u044f\u0442\u043e \u043e\u0442\u0432\u0430\u0440\u044f\u0442\u0435 \u0434\u0430\u043b\u0438 \u0435 \u0432 \u0447\u0435\u0440\u043d\u0438\u0442\u0435 \u0441\u043f\u0438\u0441\u044a\u0446\u0438 \u0437\u0430\u0441\u0442\u0440\u0430\u0448\u0430\u0432\u0430\u0449\u0438 \u0432\u0430\u0448\u0430\u0442\u0430 \u0441\u0438\u0433\u0443\u0440\u043d\u043e\u0441\u0442. \u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u0441\u0435 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u0435\u043d \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u043a\u043e\u0439\u0442\u043e \u0437\u0430\u0449\u0438\u0442\u0430\u0432\u0430 \u0432\u0430\u0448\u0430\u0442\u0430 \u0430\u043d\u043e\u043d\u0438\u043c\u043d\u043e\u0441\u0442 \u0438 \u0438\u0437\u043f\u0440\u0430\u0449\u0430 \u0441\u0430\u043c\u043e SHA256 \u0441\u0443\u043c\u0430 \u0431\u0430\u0437\u0438\u0440\u0430\u043d\u0430 \u043d\u0430 \u0447\u0430\u0441\u0442 \u043e\u0442 \u0434\u043e\u043c\u0435\u0439\u043d\u0430 \u043a\u043e\u0439\u0442\u043e \u043f\u043e\u0441\u0435\u0449\u0430\u0432\u0430\u0442\u0435.",
"use_adguard_parental": "\u0412\u043a\u043b\u044e\u0447\u0438 AdGuard \u0420\u043e\u0434\u0438\u0442\u0435\u043b\u0441\u043a\u0438 \u041d\u0430\u0434\u0437\u043e\u0440",
"use_adguard_parental_hint": "\u041c\u043e\u0434\u0443\u043b XXX \u0432 AdGuard Home \u0449\u0435 \u043f\u0440\u043e\u0432\u0435\u0440\u0438 \u0434\u0430\u043b\u0438 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430\u0442\u0430 \u0438\u043c\u0430 \u043c\u0430\u0442\u0435\u0440\u0438\u0430\u043b\u0438 \u0437\u0430 \u0432\u044a\u0437\u0432\u044a\u0441\u0442\u043d\u0438. \u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u0441\u0435 \u0441\u044a\u0449\u0438\u044f API \u0437\u0430 \u0430\u043d\u043e\u043d\u0438\u043c\u043d\u043e\u0441\u0442 \u043a\u0430\u0442\u043e \u043f\u0440\u0438 \u043c\u043e\u0434\u0443\u043b\u0430 \u0437\u0430 \u0421\u0438\u0433\u0443\u0440\u043d\u043e\u0441\u0442.",
"enforce_safe_search": "\u0412\u043a\u043b\u044e\u0447\u0438 \u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e \u0422\u044a\u0440\u0441\u0435\u043d\u0435",
"enforce_save_search_hint": "AdGuard Home \u043f\u0440\u0438\u043b\u0430\u0433\u0430 \u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e \u0422\u044a\u0440\u0441\u0435\u043d\u0435 \u0432 \u0441\u043b\u0435\u0434\u043d\u0438\u0442\u0435 \u0442\u044a\u0440\u0441\u0430\u0447\u043a\u0438 \u0438 \u0441\u0430\u0439\u0442\u043e\u0432\u0435: Google, Youtube, Bing, \u0438 Yandex.",
"no_servers_specified": "\u041d\u044f\u043c\u0430 \u0438\u0437\u0431\u0440\u0430\u043d\u0438 \u0443\u0441\u043b\u0443\u0433\u0438",
"no_settings": "\u041d\u044f\u043c\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438",
"general_settings": "\u041e\u0431\u0449\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438",
"upstream_dns": "\u0413\u043b\u0430\u0432\u0435\u043d DNS \u0441\u044a\u0440\u0432\u044a\u0440",
"upstream_dns_hint": "\u0410\u043a\u043e \u043e\u0441\u0442\u0430\u0432\u0438\u0442\u0435 \u043f\u0440\u0430\u0437\u043d\u043e, AdGuard Home \u0449\u0435 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 <a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a> \u0437\u0430 \u0433\u043b\u0430\u0432\u0435\u043d. \u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0439 tls:\/\/ \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043a\u0430 \u0437\u0430 DNS \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0449\u0438 TLS \u0432\u0440\u044a\u0437\u043a\u0430.",
"test_upstream_btn": "\u0422\u0435\u0441\u0442\u0432\u0430\u0439 \u0433\u043b\u0430\u0432\u043d\u0438\u044f DNS",
"apply_btn": "\u041f\u0440\u0438\u043b\u043e\u0436\u0438",
"disabled_filtering_toast": "\u0417\u0430\u0431\u0440\u0430\u043d\u0438 \u0444\u0438\u043b\u0442\u0440\u0438\u0440\u0430\u043d\u0435\u0442\u043e",
"enabled_filtering_toast": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438 \u0444\u0438\u0442\u0440\u0438\u0440\u0430\u043d\u0435\u0442\u043e",
"disabled_safe_browsing_toast": "\u0417\u0430\u0431\u0440\u0430\u043d\u0438 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e-\u0441\u044a\u0440\u0444\u0438\u0440\u0430\u043d\u0435",
"enabled_safe_browsing_toast": "\u0420\u0437\u0440\u0435\u0448\u0438 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e-\u0441\u044a\u0440\u0444\u0438\u0440\u0430\u043d\u0435",
"disabled_parental_toast": "\u0417\u0430\u0431\u0440\u0430\u043d\u0438 \u0420\u043e\u0434\u0438\u0442\u0435\u043b\u0441\u043a\u0438 \u041d\u0430\u0434\u0437\u043e\u0440",
"enabled_parental_toast": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438 \u0420\u043e\u0434\u0438\u0442\u0435\u043b\u0441\u043a\u0438 \u041d\u0430\u0434\u0437\u043e\u0440",
"disabled_safe_search_toast": "\u0417\u0430\u0431\u0440\u0430\u043d\u0438 \u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e \u0422\u044a\u0440\u0441\u0435\u043d\u0435",
"enabled_save_search_toast": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438 \u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e \u0422\u044a\u0440\u0441\u0435\u043d\u0435",
"enabled_table_header": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438",
"name_table_header": "\u0418\u043c\u0435",
"filter_url_table_header": "URL \u0444\u0438\u043b\u0442\u044a\u0440",
"rules_count_table_header": "\u041f\u0440\u0430\u0432\u0438\u043b\u0430 \u043e\u0431\u0449\u043e",
"last_time_updated_table_header": "\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u043e \u043e\u0431\u043d\u043e\u0432\u0435\u043d",
"actions_table_header": "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044f",
"delete_table_action": "\u0418\u0437\u0442\u0440\u0438\u0439",
"filters_and_hosts": "\u0427\u0435\u0440\u043d\u0438 \u0441\u043f\u0438\u0441\u044a\u0446\u0438 \u0441 \u043e\u0431\u0449\u0438 \u0438 \u043c\u0435\u0441\u0442\u043d\u0438 \u0444\u0438\u043b\u0442\u0440\u0438",
"filters_and_hosts_hint": "AdGuard Home \u0440\u0430\u0437\u0431\u0438\u0440\u0430 adblock \u0438 host \u0441\u0438\u043d\u0442\u0430\u043a\u0441\u0438\u0441.",
"no_filters_added": "\u041d\u044f\u043c\u0430 \u0434\u043e\u0431\u0430\u0432\u0435\u043d\u0438 \u0444\u0438\u043b\u0442\u0440\u0438",
"add_filter_btn": "\u0414\u043e\u0431\u0430\u0432\u0438 \u0444\u0438\u043b\u0442\u044a\u0440",
"cancel_btn": "\u041e\u0442\u043a\u0430\u0436\u0438",
"enter_name_hint": "\u0412\u044a\u0432\u0435\u0434\u0438 \u0438\u043c\u0435",
"enter_url_hint": "\u0412\u044a\u0432\u0435\u0434\u0438 URL",
"check_updates_btn": "\u041f\u0440\u043e\u0432\u0435\u0440\u0438 \u0437\u0430 \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f",
"new_filter_btn": "\u0412\u044a\u0432\u0435\u0434\u0438 \u043d\u043e\u0432 \u0444\u0438\u043b\u0442\u044a\u0440",
"enter_valid_filter_url": "\u041c\u043e\u043b\u044f \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0432\u0430\u043b\u0438\u0434\u0435\u043d URL \u0437\u0430 \u0444\u0438\u043b\u0442\u044a\u0440\u0430 \u0438\u043b\u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 host \u043f\u0440\u0430\u0432\u043e\u043f\u0438\u0441\u0430.",
"custom_filter_rules": "\u041c\u0435\u0441\u0442\u043d\u0438 \u043f\u0440\u0430\u0432\u0438\u043b\u0430 \u0437\u0430 \u0444\u0438\u043b\u0442\u0440\u0438\u0440\u0430\u043d\u0435",
"custom_filter_rules_hint": "\u0412\u044a\u0432\u0435\u0436\u0434\u0430\u0439\u0442\u0435 \u0432\u0441\u044f\u043a\u043e \u043f\u0440\u0430\u0432\u0438\u043b\u043e \u043d\u0430 \u043d\u043e\u0432 \u0440\u0435\u0434. \u041c\u043e\u0436\u0435 \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 adblock \u0438\u043b\u0438 hosts \u0444\u0430\u0439\u043b\u043e\u0432 \u0441\u0438\u043d\u0442\u0430\u043a\u0441\u0438\u0441.",
"examples_title": "\u041f\u0440\u0438\u043c\u0435\u0440\u0438",
"example_meaning_filter_block": "\u0411\u043b\u043e\u043a\u0438\u0440\u0430\u0439 \u0434\u043e\u0441\u0442\u044a\u043f \u0434\u043e \u0434\u043e\u043c\u0435\u0439\u043d example.org \u0438 \u0432\u0441\u0438\u0447\u043a\u0438 \u043f\u043e\u0434 \u0434\u043e\u043c\u0435\u0439\u043d\u0438.",
"example_meaning_filter_whitelist": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438 \u0434\u043e\u0441\u0442\u044a\u043f \u0434\u043e \u0434\u043e\u043c\u0435\u0439\u043d example.org \u0438 \u0432\u0441\u0438\u0447\u043a\u0438\u0442\u0435 \u043c\u0443 \u043f\u043e\u0434 \u0434\u043e\u043c\u0435\u0439\u043d\u0438.",
"example_meaning_host_block": "AdGuard Home \u0449\u0435 \u043e\u0442\u0433\u043e\u0432\u043e\u0440\u0438 \u0441 127.0.0.1 = \u043f\u0440\u0430\u0437\u0435\u043d \u0430\u0434\u0440\u0435\u0441 \u0437\u0430 \u0434\u043e\u043c\u0435\u0439\u043d example.org (\u043d\u043e \u043d\u0435 \u0438 \u0437\u0430 \u043f\u043e\u0434 \u0434\u043e\u043c\u0435\u0439\u043d\u0438).",
"example_comment": "! \u0422\u043e\u0432\u0430 \u0435 \u043a\u043e\u043c\u0435\u043d\u0442\u0430\u0440",
"example_comment_meaning": "\u043f\u0440\u0438\u043c\u0435\u0440 \u0437\u0430 \u043a\u043e\u043c\u0435\u043d\u0442\u0430\u0440",
"example_comment_hash": "# \u0422\u043e\u0432\u0430 \u0435 \u0441\u044a\u0449\u043e \u043a\u043e\u043c\u0435\u043d\u0442\u0430\u0440",
"example_regex_meaning": "\u0411\u043b\u043e\u043a\u0438\u0440\u0430\u0439 \u0434\u043e\u0441\u0442\u044a\u043f \u0434\u043e \u0434\u043e\u043c\u0435\u0439\u043d\u0438 \u043a\u043e\u0439\u0442\u043e \u0441\u044a\u0432\u043f\u0430\u0434\u0430\u0442 \u0441\u044a\u0441 \u0441\u043b\u0435\u0434\u043d\u043e\u0442\u043e",
"example_upstream_regular": "\u043a\u043b\u0430\u0441\u0438\u0447\u0435\u0441\u043a\u0438 DNS (UDP \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b)",
"example_upstream_dot": "\u043a\u0440\u0438\u043f\u0442\u0438\u0440\u0430\u043d <0>DNS-\u0432\u044a\u0440\u0445\u0443-TLS<\/0>",
"example_upstream_doh": "\u043a\u0440\u0438\u043f\u0442\u0438\u0440\u0430\u043d <0>DNS-\u0432\u044a\u0440\u0445\u0443-HTTPS<\/0>",
"example_upstream_sdns": "\u043c\u043e\u0436\u0435 \u0434\u0430 \u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 <0>DNS \u041f\u043e\u0434\u043f\u0438\u0441\u0432\u0430\u043d\u0435<\/0> \u0437\u0430 <1>DNSCrypt<\/1> \u0438\u043b\u0438 <2>DNS-\u0432\u044a\u0440\u0445\u0443-HTTPS<\/2> \u0441\u044a\u0440\u0432\u044a\u0440\u0438",
"example_upstream_tcp": "\u043a\u043b\u0430\u0441\u0438\u0447\u0435\u0441\u043a\u0438 DNS (TCP \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b)",
"all_filters_up_to_date_toast": "\u0412\u0441\u0438\u0447\u043a\u0438 \u0444\u0438\u043b\u0442\u0438 \u0441\u0430 \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0438",
"updated_upstream_dns_toast": "\u0413\u043b\u043e\u0431\u0430\u043b\u043d\u0438\u0442\u0435 DNS \u0441\u044a\u0440\u0432\u044a\u0440\u0438 \u0441\u0430 \u043e\u0431\u043d\u043e\u0432\u0435\u043d\u0438",
"dns_test_ok_toast": "\u0412\u044a\u0432\u0435\u0434\u0435\u043d\u0438\u0442\u0435 DNS \u0441\u044a\u0440\u0432\u044a\u0440\u0438 \u0440\u0430\u0431\u043e\u0442\u044f\u0442 \u043a\u043e\u0440\u0435\u043a\u0442\u043d\u043e",
"dns_test_not_ok_toast": "\u0421\u044a\u0440\u0432\u044a\u0440 \"{{key}}\": \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0438, \u043c\u043e\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0434\u0430\u043b\u0438 \u0435 \u0432\u044a\u0432\u0435\u0434\u0435\u043d \u043a\u043e\u0440\u0435\u043a\u0442\u043d\u043e",
"unblock_btn": "\u041e\u0442\u0431\u043b\u043e\u043a\u0438\u0440\u0430\u0439",
"block_btn": "\u0411\u043b\u043e\u043a\u0438\u0440\u0430\u0439",
"time_table_header": "\u0412\u0440\u0435\u043c\u0435",
"domain_name_table_header": "\u0418\u043c\u0435 \u043d\u0430 \u0434\u043e\u043c\u0435\u0439\u043d",
"type_table_header": "\u0422\u0438\u043f",
"response_table_header": "\u041e\u0442\u0433\u043e\u0432\u043e\u0440",
"client_table_header": "\u041a\u043b\u0438\u0435\u043d\u0442",
"empty_response_status": "\u041f\u0440\u0430\u0437\u0435\u043d",
"show_all_filter_type": "\u041f\u043e\u043a\u0430\u0436\u0438 \u0432\u0441\u0438\u0447\u043a\u0438",
"show_filtered_type": "\u041f\u043e\u043a\u0430\u0436\u0438 \u0444\u0438\u043b\u0442\u0440\u0438\u0440\u0430\u043d\u0438",
"no_logs_found": "\u041d\u044f\u043c\u0430 \u0438\u0441\u0442\u043e\u0440\u0438\u044f",
"disabled_log_btn": "\u0417\u0430\u0431\u0440\u0430\u043d\u0438 \u0438\u0441\u0442\u043e\u0440\u0438\u044f\u0442\u0430",
"download_log_file_btn": "\u0421\u043c\u044a\u043a\u043d\u0438 \u0438\u0441\u0442\u043e\u0440\u0438\u044f\u0442\u0430",
"refresh_btn": "\u041e\u0431\u043d\u043e\u0432\u0438",
"enabled_log_btn": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438 \u0438\u0441\u0442\u043e\u0440\u0438\u044f\u0442\u0430",
"last_dns_queries": "\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0442\u0435 5000 DNS \u0437\u0430\u044f\u0432\u043a\u0438",
"previous_btn": "\u041f\u0440\u0435\u0434\u0445\u043e\u0434\u0435\u043d",
"next_btn": "\u0421\u043b\u0435\u0434\u0432\u0430\u0449",
"loading_table_status": "\u0417\u0430\u0440\u0435\u0436\u0434\u0430\u043d\u0435...",
"page_table_footer_text": "\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u0430",
"of_table_footer_text": "\u043e\u0442",
"rows_table_footer_text": "\u0440\u0435\u0434\u043e\u0432\u0435",
"updated_custom_filtering_toast": "\u041e\u0431\u043d\u043e\u0432\u0435\u043d\u0438 \u043c\u0435\u0441\u0442\u043d\u0438 \u043f\u0440\u0430\u0432\u0438\u043b\u0430 \u0437\u0430 \u0444\u0438\u043b\u0442\u0440\u0438\u0440\u0430\u043d\u0435",
"rule_removed_from_custom_filtering_toast": "\u041f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u043e \u043e\u0442 \u043c\u0435\u0441\u0442\u043d\u0438 \u043f\u0440\u0430\u0432\u0438\u043b\u0430 \u0437\u0430 \u0444\u0438\u043b\u0442\u0440\u0438\u0440\u0430\u043d\u0435",
"rule_added_to_custom_filtering_toast": "\u0414\u043e\u0431\u0430\u0432\u0435\u043d\u043e \u0434\u043e \u043c\u0435\u0441\u0442\u043d\u0438 \u043f\u0440\u0430\u0432\u0438\u043b\u0430 \u0437\u0430 \u0444\u0438\u043b\u0442\u0440\u0438\u0440\u0430\u043d\u0435",
"query_log_disabled_toast": "\u0418\u0441\u0442\u043e\u0440\u0438\u044f\u0442\u0430 \u0435 \u0437\u0430\u0431\u0440\u0430\u043d\u0435\u043d\u0430",
"query_log_enabled_toast": "\u0418\u0441\u0442\u043e\u0440\u0438\u044f\u0442\u0430 \u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430",
"source_label": "\u0418\u0437\u0442\u043e\u0447\u043d\u0438\u043a",
"found_in_known_domain_db": "\u041d\u0430\u043c\u0435\u0440\u0435\u043d \u0432 \u0441\u043f\u0438\u0441\u044a\u0446\u0438\u0442\u0435 \u0441 \u0434\u043e\u043c\u0435\u0439\u043d\u0438.",
"category_label": "\u041a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u044f",
"rule_label": "\u041f\u0440\u0430\u0432\u0438\u043b\u043e",
"filter_label": "\u0424\u0438\u043b\u0442\u044a\u0440",
"unknown_filter": "\u041d\u0435\u043f\u043e\u0437\u043d\u0430\u0442 \u0444\u0438\u043b\u0442\u044a\u0440 {{filterId}}",
"install_welcome_title": "\u0414\u043e\u0431\u0440\u0435 \u0434\u043e\u0448\u043b\u0438 \u0432 AdGuard Home!",
"install_welcome_desc": "AdGuard Home e \u043c\u0440\u0435\u0436\u043e\u0432\u043e \u0440\u0435\u0448\u0435\u043d\u0438\u0435 \u0437\u0430 \u0431\u043b\u043e\u043a\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0440\u0435\u043a\u043b\u0430\u043c\u0438 \u0438 \u0442\u0440\u0430\u043a\u0435\u0440\u0438 \u043d\u0430 DNS \u043d\u0438\u0432\u043e. \u0421\u044a\u0437\u0434\u0430\u0434\u0435\u043d\u043e \u0435 \u0437\u0430 \u0434\u0430 \u0432\u0438 \u0434\u0430\u0434\u0435 \u043f\u044a\u043b\u0435\u043d \u043a\u043e\u043d\u0442\u0440\u043e\u043b \u043d\u0430\u0434 \u043c\u0440\u0435\u0436\u0430\u0442\u0430 \u0438 \u0432\u0441\u0438\u0447\u043a\u0438\u0442\u0435 \u0432\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u0431\u0435\u0437 \u0434\u0430 \u0435 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0434\u043e\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b\u043d\u043e \u0438\u043d\u0441\u0442\u0430\u043b\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0434\u0440\u0443\u0433 \u0441\u043e\u0444\u0442\u0443\u0435\u0440.",
"install_settings_title": "\u0410\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044f",
"install_settings_listen": "\u0410\u043a\u0442\u0438\u0432\u043d\u0438 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0438",
"install_settings_port": "\u041f\u043e\u0440\u0442",
"install_settings_interface_link": "\u0412\u0430\u0448\u0430\u0442\u0430 AdGuard Home \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430 \u0437\u0430 \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044f \u0449\u0435 \u0435 \u0434\u043e\u0441\u0442\u044a\u043f\u043d\u0430 \u043d\u0430 \u0442\u043e\u0437\u0438 \u0430\u0434\u0440\u0435\u0441:",
"form_error_port": "\u041c\u043e\u043b\u044f \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0432\u0430\u043b\u0438\u0434\u0435\u043d \u043f\u043e\u0440\u0442",
"install_settings_dns": "DNS \u0441\u044a\u0440\u0432\u044a\u0440",
"install_settings_dns_desc": "\u0417\u0430 \u0434\u0430 \u0440\u0430\u0431\u043e\u0442\u0438, \u0449\u0435 \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 \u0432\u0430\u0448\u0438\u044f\u0442 \u0440\u0443\u0442\u0435\u0440 \u0438\u043b\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0434\u0430 \u043f\u043e\u043b\u0437\u0432\u0430\u0442 DNS \u0441\u044a\u0440\u0432\u044a\u0440 \u0441 \u0430\u0434\u0440\u0435\u0441:",
"install_settings_all_interfaces": "\u0412\u0441\u0438\u0447\u043a\u0438 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0438",
"install_auth_title": "\u0423\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435",
"install_auth_desc": "\u041c\u043d\u043e\u0433\u043e \u0435 \u0432\u0430\u0436\u043d\u043e \u0434\u0430 \u0437\u0430\u0434\u0430\u0434\u0435\u0442\u0435 \u0438\u043c\u0435 \u0438 \u043f\u0430\u0440\u043e\u043b\u0430 \u0437\u0430 \u0434\u043e\u0441\u0442\u044a\u043f \u0434\u043e \u0432\u0430\u0448\u0438\u044f \u043f\u0430\u043d\u0435\u043b \u0437\u0430 \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044f \u043d\u0430 AdGuard Home. \u041f\u0440\u0435\u043f\u043e\u0440\u044a\u0447\u0432\u0430\u043c\u0435 \u0432\u0438 \u0434\u0430 \u0437\u0430\u0434\u0430\u0434\u0435\u0442\u0435 \u0438\u043c\u0435 \u0438 \u043f\u0430\u0440\u043e\u043b\u0430 \u043d\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e \u0447\u0435 \u0433\u043e \u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 \u0441\u0430\u043c\u043e \u0432 \u043a\u044a\u0449\u0438.",
"install_auth_username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b",
"install_auth_password": "\u041f\u0430\u0440\u043e\u043b\u0430",
"install_auth_confirm": "\u041f\u043e\u0442\u0432\u044a\u0440\u0434\u0435\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430",
"install_auth_username_enter": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b",
"install_auth_password_enter": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u0430",
"install_step": "\u0421\u0442\u044a\u043f\u043a\u0430",
"install_devices_title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0432\u0430\u0448\u0435\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e",
"install_devices_desc": "\u0414\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 AdGuard Home, \u0435 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 \u0432\u0430\u0448\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.",
"install_submit_title": "\u041f\u043e\u0437\u0434\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f!",
"install_submit_desc": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0442\u0430 \u0435 \u0437\u0430\u0432\u044a\u0440\u0448\u0435\u043d\u0430, \u043c\u043e\u0436\u0435 \u0434\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u0434\u0430 \u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 AdGuard Home.",
"install_devices_router": "\u0420\u0443\u0442\u0435\u0440",
"install_devices_router_desc": "\u0410\u043a\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 \u0432\u0430\u0448\u0438\u044f\u0442 \u0440\u0443\u0442\u0435\u0440 \u043d\u044f\u043c\u0430 \u043d\u0443\u0436\u0434\u0430 \u0440\u044a\u0447\u043d\u043e \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0432\u0430\u0442\u0435 \u0432\u0441\u044f\u043a\u043e \u0435\u0434\u043d\u043e \u043e\u0442 \u0443\u0441\u0442\u0440\u0439\u0441\u0442\u0432\u0430\u0442\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430.",
"install_devices_address": "AdGuard Home DNS \u0441\u044a\u0440\u0432\u044a\u0440\u044a\u0442 \u0435 \u043d\u0430 \u0441\u043b\u0435\u0434\u043d\u0438\u044f \u0430\u0434\u0440\u0435\u0441",
"install_devices_router_list_1": "\u041e\u0442\u0432\u043e\u0440\u0435\u0442\u0435 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430\u0442\u0430 \u0437\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0430 \u0432\u0430\u0448\u0438\u044f \u0440\u0443\u0442\u0435\u0440. \u041e\u0431\u0438\u043a\u043d\u043e\u0432\u0435\u043d\u043e \u0442\u044f \u0441\u0435 \u043d\u0430\u043c\u0438\u0440\u0430 \u043d\u0430 URL (\u0442\u0443\u043a http:\/\/192.168.0.1\/ \u0438\u043b\u0438 \u0442\u0443\u043a http:\/\/192.168.1.1\/). \u0417\u0430 \u0434\u043e\u0441\u0442\u044a\u043f \u043c\u043e\u0436\u0435 \u0434\u0430 \u0432\u0438 \u0442\u0440\u044f\u0431\u0432\u0430 \u043f\u0430\u0440\u043e\u043b\u0430. \u0410\u043a\u043e \u0441\u0442\u0435 \u0437\u0430\u0431\u0440\u0430\u0432\u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u043c\u043e\u0436\u0435 \u0434\u0430 \u044f \u0440\u0435\u0441\u0435\u0442\u043d\u0435\u0442\u0435 \u043a\u0430\u0442\u043e \u043d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0430 \u0441\u043a\u0440\u0438\u0442\u0438\u044f \u0440\u0435\u0441\u0435\u0442 \u0431\u0443\u0442\u043e\u043d - \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435 \u0442\u043e\u0432\u0430 \u0449\u0435 \u0440\u0435\u0441\u0435\u0442\u043d\u0435 \u0432\u0441\u0438\u0447\u043a\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0430 \u0440\u0443\u0442\u0435\u0440\u0430 \u0434\u043e \u0444\u0430\u0431\u0440\u0438\u0447\u043d\u0438! \u041d\u044f\u043a\u043e\u0439 \u0440\u0443\u0442\u0435\u0440\u0438 \u043c\u043e\u0433\u0430\u0442 \u0434\u0430 \u0431\u044a\u0434\u0430\u0442\u0435 \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043d\u0438 \u043e\u0442 \u0441\u043e\u0444\u0442\u0443\u0435\u0440 \u0438\u043b\u0438 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u043a\u043e\u0439\u0442\u043e \u0431\u0438 \u0442\u0440\u044f\u0431\u0432\u0430\u043b\u043e \u0434\u0430 \u0435 \u0432\u0435\u0447\u0435 \u0438\u043d\u0441\u0442\u0430\u043b\u0438\u0440\u0430\u043d \u043d\u0430 \u043a\u043e\u043c\u043f\u044e\u0442\u044a\u0440\u0430\/\u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0430 \u0432\u0438.",
"install_devices_router_list_2": "\u041d\u0430\u043c\u0435\u0440\u0435\u0442\u0430 DHCP\/DNS \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438. \u0412 \u043f\u043e\u0434 \u0440\u0430\u0437\u0434\u0435\u043b DHCP \u0440\u0437\u0433\u043b\u0435\u0434\u0430\u0439\u0442\u0435 \u0438 \u043d\u0430\u043c\u0435\u0440\u0435\u0442\u0435 \u043a\u044a\u0434\u0435 \u0435 \u043f\u043e\u043b\u0435\u0442\u043e \u0437\u0430 DNS \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0432 \u043a\u043e\u0435\u0442\u043e \u043c\u043e\u0436\u0435 \u0434\u0430 \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0437\u0430 DNS \u0441\u044a\u0440\u0432\u044a\u0440\u0438.",
"install_devices_router_list_3": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0430\u0434\u0440\u0435\u0441\u044a\u0442 \u043d\u0430 AdGuard Home \u0441\u044a\u0440\u0432\u044a\u0440\u0430.",
"install_devices_windows_list_1": "\u041e\u0442\u0432\u043e\u0440\u0435\u0442\u0435 \u041a\u043e\u043d\u0442\u0440\u043e\u043b\u043d\u0438\u044f \u041f\u0430\u043d\u0435\u043b \u043f\u0440\u0435\u0437 \u0421\u0442\u0430\u0440\u0442 \u043c\u0435\u043d\u044e \u0438\u043b\u0438 \u0447\u0440\u0435\u0437 \u0444\u0443\u043d\u043a\u0446\u0438\u044f \u0442\u044a\u0440\u0441\u0435\u043d\u0435 \u043d\u0430 Windows.",
"install_devices_windows_list_2": "\u0412\u044a\u0440\u0432\u0435\u0442\u0435 \u0434\u043e \u041d\u0430\u0441\u0442\u0440\u0439\u043a\u0438 \u043d\u0430 \u041c\u0440\u0435\u0436\u0438 \u0438 \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442 \u0438 \u043e\u0442 \u0442\u0430\u043c \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u041c\u0440\u0435\u0436\u0438 \u0438 \u0426\u0435\u043d\u0442\u044a\u0440 \u0437\u0430 \u0421\u043f\u043e\u0434\u0435\u043b\u044f\u043d\u0435.",
"install_devices_windows_list_3": "\u041e\u0442 \u043b\u044f\u0432\u043e \u043d\u0430 \u0435\u043a\u0440\u0430\u043d\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u0442\u0435 \u0421\u043c\u0435\u043d\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u043a\u0438 \u043d\u0430 \u043c\u0440\u0435\u0436\u043e\u0432\u0438\u044f \u0430\u0434\u0430\u043f\u0442\u0435\u0440 \u0438 \u043a\u043b\u0438\u043a\u043d\u0435\u0442\u0435 \u043d\u0430 \u043d\u0435\u0433\u043e.",
"install_devices_windows_list_4": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0442\u043e\u0437\u0438 \u043a\u043e\u0439\u0442\u043e \u0435 \u0430\u043a\u0442\u0438\u0432\u0435\u043d, \u0434\u044f\u0441\u043d\u043e-\u043a\u043b\u0438\u043a\u0432\u0430\u043d\u0435 \u0438 \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0430 \u0421\u0432\u043e\u0439\u0441\u0442\u0432\u0430.",
"install_devices_windows_list_5": "\u041d\u0430\u043c\u0435\u0440\u0435\u0442\u0435 \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442 \u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b \u0412\u0435\u0440\u0441\u0438\u044f 4 (TCP\/IP) \u0432 \u0441\u043f\u0438\u0441\u044a\u043a\u0430, \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0438 \u043a\u043b\u0438\u043a\u043d\u0435\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u043d\u0430 \u0421\u0432\u043e\u0439\u0441\u0442\u0432\u0430.",
"install_devices_windows_list_6": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0439 \u0441\u043b\u0435\u0434\u043d\u0438\u0442\u0435 \u0430\u0434\u0440\u0435\u0441\u0438 \u0437\u0430 DNS \u0441\u044a\u0440\u0441\u044a\u0440\u0438 \u0438 \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0430 AdGuard Home \u0441\u044a\u0440\u0432\u044a\u0440\u0430 \u0432\u0438.",
"install_devices_macos_list_1": "\u0426\u044a\u043a\u043d\u0435\u0442\u0435 \u043d\u0430 Apple \u0438\u043a\u043e\u043d\u043a\u0430\u0442\u0430 \u0438 \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 System Preferences...",
"install_devices_macos_list_2": "\u0426\u044a\u043a\u043d\u0435\u0442\u0435 \u043d\u0430 Network.",
"install_devices_macos_list_3": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0437\u0435\u043b\u0435\u043d\u0430\u0442\u0430-\u0430\u043a\u0442\u0438\u0432\u043d\u0430 \u0432\u0440\u044a\u0437\u043a\u0430 \u0432 \u0441\u043f\u0438\u0441\u044a\u043a\u0430 \u0438 \u043a\u043b\u0438\u043a\u043d\u0435\u0442\u0435 \u043d\u0430 Advanced.",
"install_devices_macos_list_4": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 DNS \u0442\u0430\u0431 \u0438 \u043a\u043b\u0438\u043a\u043d\u0435\u0442\u0435 \u043d\u0430 + \u0437\u0430 \u0434\u0430 \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0430 AdGuard Home \u0441\u044a\u0440\u0432\u044a\u0440\u0430.",
"install_devices_android_list_1": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 Android \u041c\u0435\u043d\u044e \u043e\u0442 \u0434\u043e\u043c\u0430\u0448\u043d\u0438\u044f \u0435\u043a\u0440\u0430\u043d, \u0438 \u0446\u044a\u043a\u043d\u0435\u0442\u0435 \u043d\u0430 \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438.",
"install_devices_android_list_2": "\u0426\u044a\u043a\u043d\u0435\u0442\u0435 \u043d\u0430 Wi-Fi \u043c\u0435\u043d\u044e. \u041d\u0430 \u0435\u043a\u0440\u0430\u043d\u0430 \u0449\u0435 \u0441\u0435 \u043f\u043e\u044f\u0432\u0430\u0442 \u0432\u0441\u0438\u0447\u043a\u0438 \u0431\u0435\u0437\u0436\u0438\u0447\u043d\u0438 \u043f\u0440\u0435\u0436\u0438 (\u0442\u0430\u043c \u043d\u044f\u043c\u0430 \u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442 \u0437\u0430 \u0432\u044a\u0432\u0435\u0436\u0434\u0430\u043d\u0435 \u043d\u0430 DNS \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438).",
"install_devices_android_list_3": "\u0426\u044a\u043a\u043d\u0435\u0442\u0435 \u0438 \u0437\u0430\u0434\u0440\u044a\u0436\u0434\u0435 \u0432\u044a\u0440\u0445\u0443 \u0412\u0438\u0435 \u0441\u0442\u0435 \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u0438 \u0441.., \u0438 \u043a\u043b\u0438\u043a\u043d\u0435\u0442\u0435 \u043d\u0430 \u041c\u043e\u0434\u0438\u0444\u0438\u0446\u0438\u0440\u0430\u0439 \u043c\u0440\u0435\u0436\u0430.",
"install_devices_android_list_4": "\u041d\u0430 \u043d\u044f\u043a\u043e\u0439 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0435 \u043d\u0435\u043e\u0445\u043e\u0434\u0438\u043c\u043e \u0434\u0430 \u043c\u0430\u0440\u043a\u0438\u0440\u0430\u0442\u0435 \u043f\u043e\u043a\u0430\u0436\u0438 \u0420\u0430\u0437\u0448\u0438\u0440\u0435\u043d\u0438, \u0437\u0430 \u0434\u0430 \u0432\u0438\u0434\u0438\u0442\u0435 \u0432\u0441\u0438\u0447\u043a\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438. \u0417\u0430 \u0434\u0430 \u043f\u0440\u043e\u043c\u0435\u043d\u0438\u0442\u0435 Android DNS \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u0442\u0435, \u043c\u043e\u0436\u0435 \u0434\u0430 \u0441\u0435 \u043d\u0430\u043b\u043e\u0436\u0438 \u0434\u0430 \u043f\u0440\u043e\u043c\u0435\u043d\u0438\u0442\u0435 IP \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u0442\u0435 \u043e\u0442 DHCP \u043d\u0430 \u0421\u0442\u0430\u0442\u0438\u0447\u043d\u0438.",
"install_devices_android_list_5": "\u041f\u0440\u043e\u043c\u0435\u043d\u0435\u0442\u0435 \u0441\u0442\u043e\u0439\u043d\u043e\u0441\u0442\u0438\u0442\u0435 \u043d\u0430 DNS 1 \u0438 DNS 2 \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442 AdGuard Home \u0441\u044a\u0440\u0432\u044a\u0440\u0430.",
"install_devices_ios_list_1": "\u041e\u0442 \u043d\u0430\u0447\u0430\u043b\u0435\u043d \u0435\u043a\u0440\u0430\u043d, \u0446\u044a\u043a\u043d\u0435\u0442\u0435 \u043d\u0430 Settings.",
"install_devices_ios_list_2": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 Wi-Fi \u043e\u0442 \u043b\u044f\u0432\u043e\u0442\u043e \u043c\u0435\u043d\u044e (\u0442\u0430\u043c \u043d\u044f\u043c\u0430 \u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442 \u0437\u0430 \u0432\u044a\u0432\u0435\u0436\u0434\u0430\u043d\u0435 \u043d\u0430 DNS \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438).",
"install_devices_ios_list_3": "\u041a\u043b\u0438\u043d\u0435\u0442\u0435 \u043d\u0430 \u0438\u043c\u0435\u0442\u043e \u043d\u0430 \u0430\u043a\u0442\u0438\u0432\u043d\u0430\u0442\u0430 \u043c\u0440\u0435\u0436\u0430 \u043a\u044a\u043c \u043a\u043e\u044f\u0442\u043e \u0441\u0442\u0435 \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u0438.",
"install_devices_ios_list_4": "\u0412 \u043f\u043e\u043b\u0435\u0442\u043e \u0437\u0430 DNS \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0440\u044a\u0447\u043d\u043e \u0438 \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0430 AdGuard Home \u0441\u044a\u0440\u0432\u044a\u0440\u0430.",
"get_started": "\u0414\u0430 \u0437\u0430\u043f\u043e\u0447\u0432\u0430\u043c\u0435",
"next": "\u0421\u043b\u0435\u0434\u0432\u0430\u0449",
"open_dashboard": "\u041e\u0442\u0432\u043e\u0440\u0438 \u0442\u0430\u0431\u043b\u043e",
"install_saved": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0437\u0430\u043f\u0438\u0441\u0430\u043d\u043e",
"encryption_title": "\u041a\u0440\u0438\u043f\u0442\u0438\u0440\u0430\u043d\u0435",
"encryption_desc": "\u041f\u043e\u0434\u044a\u0440\u0436\u0430 \u0441\u0435 \u0441\u0438\u0433\u0443\u0440\u043d\u0430 \u0432\u0440\u044a\u0437\u043a\u0430 (HTTPS\/TLS) \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u0435\u043b\u043d\u043e \u0437\u0430 DNS \u0438 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430\u0442\u0430 \u0437\u0430 \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044f",
"encryption_config_saved": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0437\u0430\u043f\u0438\u0441\u0430\u043d\u0430",
"encryption_server": "\u0418\u043c\u0435 \u043d\u0430 \u0441\u044a\u0440\u0432\u044a\u0440\u0430",
"encryption_server_enter": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0438\u043c\u0435 \u043d\u0430 \u0434\u043e\u043c\u0435\u0439\u043d\u0430",
"encryption_server_desc": "\u0417\u0430 \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 HTTPS, \u0442\u0440\u044f\u0431\u0432\u0430 \u0438\u043c\u0435\u0442\u043e \u043d\u0430 \u0441\u044a\u0440\u0432\u044a\u0440\u0430 \u0434\u0430 \u0441\u044a\u0432\u043f\u0430\u0434\u0430 \u0441 \u0442\u043e\u0432\u0430 \u043d\u0430 SSL \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430.",
"encryption_redirect": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u043f\u0440\u0435\u043d\u0430\u0441\u043e\u0447\u0432\u0430\u043d\u0435 \u043a\u044a\u043c HTTPS",
"encryption_redirect_desc": "\u0421\u043b\u0443\u0436\u0438 \u0437\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u043f\u0440\u0435\u043d\u0430\u0441\u043e\u0447\u0432\u0430\u043d\u0435 \u043e\u0442 HTTP \u043a\u044a\u043c HTTPS \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430\u0442\u0430 \u0437\u0430 \u0410\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044f \u0432 AdGuard Home.",
"encryption_https": "HTTPS \u043f\u043e\u0440\u0442",
"encryption_https_desc": "\u0410\u043a\u043e \u0437\u0430\u0434\u0430\u0434\u0435\u0442\u0435 HTTPS \u043f\u043e\u0440\u0442, \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430\u0442\u0430 \u0437\u0430 \u0410\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044f \u043d\u0430 AdGuard Home \u0449\u0435 \u0431\u044a\u0434\u0435 \u0434\u043e\u0441\u0442\u044a\u043f\u043d\u0430 \u043d\u0430 HTTPS, \u0438 \u0441\u044a\u0449\u043e \u0449\u0435 \u043e\u0442\u0433\u043e\u0432\u0430\u0440\u044f \u043d\u0430 DNS-\u0432\u044a\u0440\u0445\u0443-HTTPS '\/dns-\u0437\u0430\u043f\u0438\u0442\u0432\u0430\u043d\u0438\u044f'.",
"encryption_dot": "DNS-\u0432\u044a\u0440\u0445\u0443-TLS \u043f\u043e\u0440\u0442",
"encryption_dot_desc": "\u0410\u043a\u043e \u043f\u043e\u0440\u0442\u0430 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d, AdGuard Home \u0449\u0435 \u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430 \u0438 \u0441\u044a\u0440\u0432\u044a\u0440 \u0437\u0430 DNS-\u0432\u044a\u0440\u0445\u0443-TLS.",
"encryption_certificates": "\u0421\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0438",
"encryption_certificates_desc": "\u0417\u0430 \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 \u0441\u0438\u0433\u0443\u0440\u043d\u0430 \u0432\u0440\u044a\u0437\u043a\u0430, \u0449\u0435 \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043e\u0441\u0438\u0433\u0443\u0440\u0438\u0442\u0435 SSL \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0438 \u0437\u0430 \u0432\u0430\u0448\u0438\u044f \u0434\u043e\u043c\u0435\u0439\u043d. \u041c\u043e\u0436\u0435 \u0434\u0430 \u0437\u0430\u044f\u0432\u0438\u0442\u0435 \u0431\u0435\u0437\u043f\u043b\u0430\u0442\u0435\u043d \u043e\u0442 <0>{{link}}<\/0> \u0438\u043b\u0438 \u0434\u0430 \u0437\u0430\u043a\u0443\u043f\u0438\u0442\u0435 \u043e\u0442 Certificate Authorities.",
"encryption_certificates_input": "\u041a\u043e\u043f\u0438\u0440\u0430\u0439\/\u043f\u043e\u0441\u0442\u0430\u0432\u0438 \u0432\u0430\u0448\u0438\u044f PEM-\u043a\u043e\u0434\u0438\u0440\u0430\u043d \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0442\u0443\u043a.",
"encryption_status": "\u0421\u044a\u0441\u0442\u043e\u044f\u043d\u0438\u0435",
"encryption_expire": "\u0413\u043e\u0434\u0435\u043d \u0434\u043e",
"encryption_key": "\u0427\u0430\u0441\u0442\u0435\u043d \u043a\u043b\u044e\u0447",
"encryption_key_input": "\u041a\u043e\u043f\u0438\u0440\u0430\u0439\/\u043f\u043e\u0441\u0442\u0430\u0432\u0438 \u0432\u0430\u0448\u0438\u044f PEM-\u043a\u043e\u0434\u0438\u0440\u0430\u043d \u0447p\u0430\u0441\u0442\u0435\u043d \u043a\u043b\u044e\u0447 \u0437\u0430 \u0432\u0430\u0448\u0438\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0442\u0443\u043a.",
"encryption_enable": "\u0420\u0430\u0437p\u0435\u0448\u0438 \u043a\u0440\u0438\u043f\u0442\u0438\u0440\u0430\u043d\u0435 (HTTPS, DNS-\u0432\u044a\u0440\u0445\u0443-HTTPS, \u0438 DNS-\u0432\u044a\u0440\u0445\u0443-TLS)",
"encryption_enable_desc": "\u0410\u043a\u043e \u0441\u0442\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0438\u043b\u0438 \u043a\u0440\u0438\u043f\u0442\u0438\u0440\u0430\u043d\u0435, \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430\u0442\u0430 \u0437\u0430 \u0410\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044f \u043d\u0430 AdGuard Home \u0449\u0435 \u0431\u044a\u0434\u0435 \u0434\u043e\u0441\u0442\u044a\u043f\u043d\u0430 \u043f\u0440\u0435\u0437 HTTPS, \u0438 DNS \u0441\u044a\u0440\u0432\u044a\u0440\u0430 \u0449\u0435 \u043e\u0442\u0433\u043e\u0432\u0430\u0440\u044f \u0441\u044a\u0449\u043e \u043d\u0430 \u0437\u0430\u043f\u0438\u0442\u0432\u0430\u043d\u0438\u044f DNS-\u0432\u044a\u0440\u0445\u0443-HTTPS \u0438 DNS-\u0432\u044a\u0440\u0445\u0443-TLS.",
"encryption_chain_valid": "\u0419\u0435\u0440\u0430\u0440\u0445\u0438\u044f\u0442\u0430 \u043e\u0442 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0438 \u0435 \u0432\u0430\u043b\u0438\u0434\u043d\u0430",
"encryption_chain_invalid": "\u0419\u0435\u0440\u0430\u0440\u0445\u0438\u044f\u0442\u0430 \u043e\u0442 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0438 \u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430",
"encryption_key_valid": "\u0422\u043e\u0432\u0430 \u0435 \u0432\u0430\u043b\u0438\u0434\u0435\u043d {{type}} \u0447\u0430\u0441\u0442\u0435\u043d \u043a\u043b\u044e\u0447",
"encryption_key_invalid": "\u0422\u043e\u0432\u0430 \u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d {{type}} \u0447\u0430\u0441\u0442\u0435\u043d \u043a\u043b\u044e\u0447",
"encryption_subject": "\u0422\u0435\u043c\u0430",
"encryption_issuer": "\u0418\u0437\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b",
"encryption_hostnames": "\u0418\u043c\u0435\u043d\u0430 \u043d\u0430 \u0445\u043e\u0441\u0442\u0430",
"encryption_reset": "\u0421\u0438\u0433\u0443\u0440\u043d\u0438 \u043b\u0438 \u0441\u0442\u0435 \u0447\u0435 \u0438\u0441\u043a\u0430\u0442\u0435 \u0434\u0430 \u0438\u0437\u0442\u0440\u0438\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u0442\u0435 \u0437\u0430 \u043a\u0440\u0438\u043f\u0442\u0438\u0440\u0430\u043d\u0435?",
"topline_expiring_certificate": "\u0412\u0430\u0448\u0438\u044f\u0442 SSL \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0438\u0437\u0442\u0438\u0447\u0430. \u041e\u0431\u043d\u043e\u0432\u0438 <0>\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0437\u0430 \u043a\u0440\u0438\u043f\u0442\u0438\u0440\u0430\u043d\u0435<\/0>.",
"topline_expired_certificate": "\u0412\u0430\u0448\u0438\u044f\u0442 SSL \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0435 \u0438\u0437\u0442\u0435\u043a\u044a\u043b. \u041e\u0431\u043d\u043e\u0432\u0438 <0>\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0437\u0430 \u043a\u0440\u0438\u043f\u0442\u0438\u0440\u0430\u043d\u0435<\/0>.",
"form_error_port_range": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043f\u043e\u0440\u0442 \u0432 \u0434\u0438\u0430\u043f\u0430\u0437\u043e\u043d\u0430 80-65535",
"form_error_port_unsafe": "\u041d\u0435 \u0435 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 \u0442\u043e\u0437\u0438 \u043f\u043e\u0440\u0442",
"form_error_equal": "\u041d\u0435 \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0441\u044a\u0432\u043f\u0430\u0434\u0430",
"form_error_password": "\u041f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u043d\u0435 \u0441\u044a\u0432\u043f\u0430\u0434\u0430",
"reset_settings": "\u0418\u0437\u0442\u0440\u0438\u0439 \u0432\u0441\u0438\u0447\u043a\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438",
"update_announcement": "\u0418\u043c\u0430 \u043d\u043e\u0432\u0430 AdGuard Home {{version}}! <0>\u0426\u044a\u043a\u043d\u0438 \u0442\u0443\u043a<\/0> \u0437\u0430 \u043f\u043e\u0432\u0435\u0447\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f."
}

View File

@ -1,5 +1,8 @@
{ {
"url_added_successfully": "Url added successfully", "upstream_parallel": "Use parallel queries to speed up resolving by simultaneously querying all upstream servers",
"bootstrap_dns": "Bootstrap DNS servers",
"bootstrap_dns_desc": "Bootstrap DNS servers are used to resolve IP addresses of the DoH\/DoT resolvers you specify as upstreams.",
"url_added_successfully": "URL added successfully",
"check_dhcp_servers": "Check for DHCP servers", "check_dhcp_servers": "Check for DHCP servers",
"save_config": "Save config", "save_config": "Save config",
"enabled_dhcp": "DHCP server enabled", "enabled_dhcp": "DHCP server enabled",
@ -79,7 +82,7 @@
"no_settings": "No settings", "no_settings": "No settings",
"general_settings": "General settings", "general_settings": "General settings",
"upstream_dns": "Upstream DNS servers", "upstream_dns": "Upstream DNS servers",
"upstream_dns_hint": "If you keep this field empty, AdGuard Home will use <a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a> as an upstream. Use tls:\/\/ prefix for DNS over TLS servers.", "upstream_dns_hint": "If you keep this field empty, AdGuard Home will use <a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a> as an upstream.",
"test_upstream_btn": "Test upstreams", "test_upstream_btn": "Test upstreams",
"apply_btn": "Apply", "apply_btn": "Apply",
"disabled_filtering_toast": "Disabled filtering", "disabled_filtering_toast": "Disabled filtering",
@ -118,10 +121,11 @@
"example_comment_hash": "# Also a comment", "example_comment_hash": "# Also a comment",
"example_regex_meaning": "block access to the domains matching the specified regular expression", "example_regex_meaning": "block access to the domains matching the specified regular expression",
"example_upstream_regular": "regular DNS (over UDP)", "example_upstream_regular": "regular DNS (over UDP)",
"example_upstream_dot": "encrypted <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-over-TLS<\/a>", "example_upstream_dot": "encrypted <0>DNS-over-TLS</0>",
"example_upstream_doh": "encrypted <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a>", "example_upstream_doh": "encrypted <0>DNS-over-HTTPS</0>",
"example_upstream_sdns": "you can use <a href='https:\/\/dnscrypt.info\/stamps\/' target='_blank'>DNS Stamps<\/a> for <a href='https:\/\/dnscrypt.info\/' target='_blank'>DNSCrypt<\/a> or <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a> resolvers", "example_upstream_sdns": "you can use <0>DNS Stamps</0> for <1>DNSCrypt</1> or <2>DNS-over-HTTPS</2> resolvers",
"example_upstream_tcp": "regular DNS (over TCP)", "example_upstream_tcp": "regular DNS (over TCP)",
"example_upstream_reserved": "you can specify DNS upstream <0>for a specific domain(s)</0>",
"all_filters_up_to_date_toast": "All filters are already up-to-date", "all_filters_up_to_date_toast": "All filters are already up-to-date",
"updated_upstream_dns_toast": "Updated the upstream DNS servers", "updated_upstream_dns_toast": "Updated the upstream DNS servers",
"dns_test_ok_toast": "Specified DNS servers are working correctly", "dns_test_ok_toast": "Specified DNS servers are working correctly",
@ -211,11 +215,11 @@
"open_dashboard": "Open Dashboard", "open_dashboard": "Open Dashboard",
"install_saved": "Saved successfully", "install_saved": "Saved successfully",
"encryption_title": "Encryption", "encryption_title": "Encryption",
"encryption_desc": "Encryption (HTTPS/TLS) support for both DNS and admin web interface", "encryption_desc": "Encryption (HTTPS\/TLS) support for both DNS and admin web interface",
"encryption_config_saved": "Encryption config saved", "encryption_config_saved": "Encryption config saved",
"encryption_server": "Server name", "encryption_server": "Server name",
"encryption_server_enter": "Enter your domain name", "encryption_server_enter": "Enter your domain name",
"encryption_server_desc": "In order to use HTTPS, you need yo enter the server name that matches your SSL certificate.", "encryption_server_desc": "In order to use HTTPS, you need to enter the server name that matches your SSL certificate.",
"encryption_redirect": "Redirect to HTTPS automatically", "encryption_redirect": "Redirect to HTTPS automatically",
"encryption_redirect_desc": "If checked, AdGuard Home will automatically redirect you from HTTP to HTTPS addresses.", "encryption_redirect_desc": "If checked, AdGuard Home will automatically redirect you from HTTP to HTTPS addresses.",
"encryption_https": "HTTPS port", "encryption_https": "HTTPS port",
@ -223,12 +227,12 @@
"encryption_dot": "DNS-over-TLS port", "encryption_dot": "DNS-over-TLS port",
"encryption_dot_desc": "If this port is configured, AdGuard Home will run a DNS-over-TLS server on this port.", "encryption_dot_desc": "If this port is configured, AdGuard Home will run a DNS-over-TLS server on this port.",
"encryption_certificates": "Certificates", "encryption_certificates": "Certificates",
"encryption_certificates_desc": "In order to use encryption, you need to provide a valid SSL certificates chain for your domain. You can get a free certificate on <0>{{link}}</0> or you can buy it from one of the trusted Certificate Authorities.", "encryption_certificates_desc": "In order to use encryption, you need to provide a valid SSL certificates chain for your domain. You can get a free certificate on <0>{{link}}<\/0> or you can buy it from one of the trusted Certificate Authorities.",
"encryption_certificates_input": "Copy/paste your PEM-encoded cerificates here.", "encryption_certificates_input": "Copy\/paste your PEM-encoded certificates here.",
"encryption_status": "Status", "encryption_status": "Status",
"encryption_expire": "Expires", "encryption_expire": "Expires",
"encryption_key": "Private key", "encryption_key": "Private key",
"encryption_key_input": "Copy/paste your PEM-encoded private key for your cerficate here.", "encryption_key_input": "Copy\/paste your PEM-encoded private key for your certificate here.",
"encryption_enable": "Enable Encryption (HTTPS, DNS-over-HTTPS, and DNS-over-TLS)", "encryption_enable": "Enable Encryption (HTTPS, DNS-over-HTTPS, and DNS-over-TLS)",
"encryption_enable_desc": "If encryption is enabled, AdGuard Home admin interface will work over HTTPS, and the DNS server will listen for requests over DNS-over-HTTPS and DNS-over-TLS.", "encryption_enable_desc": "If encryption is enabled, AdGuard Home admin interface will work over HTTPS, and the DNS server will listen for requests over DNS-over-HTTPS and DNS-over-TLS.",
"encryption_chain_valid": "Certificate chain is valid", "encryption_chain_valid": "Certificate chain is valid",
@ -239,12 +243,14 @@
"encryption_issuer": "Issuer", "encryption_issuer": "Issuer",
"encryption_hostnames": "Hostnames", "encryption_hostnames": "Hostnames",
"encryption_reset": "Are you sure you want to reset encryption settings?", "encryption_reset": "Are you sure you want to reset encryption settings?",
"topline_expiring_certificate": "Your SSL certificate is about to expire. Update <0>Encryption settings</0>.", "topline_expiring_certificate": "Your SSL certificate is about to expire. Update <0>Encryption settings<\/0>.",
"topline_expired_certificate": "Your SSL certificate is expired. Update <0>Encryption settings</0>.", "topline_expired_certificate": "Your SSL certificate is expired. Update <0>Encryption settings<\/0>.",
"form_error_port_range": "Enter port value in the range of 80-65535", "form_error_port_range": "Enter port value in the range of 80-65535",
"form_error_port_unsafe": "This is an unsafe port", "form_error_port_unsafe": "This is an unsafe port",
"form_error_equal": "Shouldn't be equal", "form_error_equal": "Shouldn't be equal",
"form_error_password": "Password mismatched", "form_error_password": "Password mismatched",
"reset_settings": "Reset settings", "reset_settings": "Reset settings",
"update_announcement": "AdGuard Home {{version}} is now available! <0>Click here</0> for more info." "update_announcement": "AdGuard Home {{version}} is now available! <0>Click here<\/0> for more info.",
"setup_guide": "Setup guide",
"dns_addresses": "DNS addresses"
} }

View File

@ -115,9 +115,9 @@
"example_comment_meaning": "solo un comentario", "example_comment_meaning": "solo un comentario",
"example_comment_hash": "# Tambi\u00e9n un comentario", "example_comment_hash": "# Tambi\u00e9n un comentario",
"example_upstream_regular": "DNS regular (a trav\u00e9s de UDP)", "example_upstream_regular": "DNS regular (a trav\u00e9s de UDP)",
"example_upstream_dot": "encriptado <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-a-trav\u00e9s-de-TLS<\/a>", "example_upstream_dot": "encriptado <0>DNS-a-trav\u00e9s-de-TLS<\/0>",
"example_upstream_doh": "encriptado <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-a-trav\u00e9s-de-TLS<\/a>", "example_upstream_doh": "encriptado <0>DNS-a-trav\u00e9s-de-TLS<\/0>",
"example_upstream_sdns": "puedes usar <a href='https:\/\/dnscrypt.info\/stamps\/' target='_blank'>DNS Stamps<\/a> para <a href='https:\/\/dnscrypt.info\/' target='_blank'>DNSCrypt<\/a> o <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a> resolutores", "example_upstream_sdns": "puedes usar <0>DNS Stamps<\/0> para <1>DNSCrypt<\/1> o <2>DNS-over-HTTPS<\/2> resolutores",
"example_upstream_tcp": "DNS regular (a trav\u00e9s de TCP)", "example_upstream_tcp": "DNS regular (a trav\u00e9s de TCP)",
"all_filters_up_to_date_toast": "Todos los filtros son actualizados", "all_filters_up_to_date_toast": "Todos los filtros son actualizados",
"updated_upstream_dns_toast": "Servidores DNS upstream actualizados", "updated_upstream_dns_toast": "Servidores DNS upstream actualizados",

View File

@ -116,9 +116,9 @@
"example_comment_meaning": "commentaire", "example_comment_meaning": "commentaire",
"example_comment_hash": "# Et comme \u00e7a aussi on peut laisser des commentaires", "example_comment_hash": "# Et comme \u00e7a aussi on peut laisser des commentaires",
"example_upstream_regular": "DNS classique (au-dessus de UDP)", "example_upstream_regular": "DNS classique (au-dessus de UDP)",
"example_upstream_dot": "<a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-au-dessus-de-TLS<\/a> chiffr\u00e9", "example_upstream_dot": "<0>DNS-au-dessus-de-TLS<\/0> chiffr\u00e9",
"example_upstream_doh": "<a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-au-dessus-de-HTTPS<\/a> chiffr\u00e9", "example_upstream_doh": "<0>DNS-au-dessus-de-HTTPS<\/0> chiffr\u00e9",
"example_upstream_sdns": "vous pouvez utiliser <a href='https:\/\/dnscrypt.info\/stamps\/' target='_blank'>DNS Stamps<\/a> pour <a href='https:\/\/dnscrypt.info\/' target='_blank'>DNSCrypt<\/a> ou les resolveurs <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-au-dessus-de-HTTPS<\/a>", "example_upstream_sdns": "vous pouvez utiliser <0>DNS Stamps<\/0> pour <1>DNSCrypt<\/1> ou les resolveurs <2>DNS-au-dessus-de-HTTPS<\/2>",
"example_upstream_tcp": "DNS classique (au-dessus de TCP)", "example_upstream_tcp": "DNS classique (au-dessus de TCP)",
"all_filters_up_to_date_toast": "Tous les filtres sont mis \u00e0 jour", "all_filters_up_to_date_toast": "Tous les filtres sont mis \u00e0 jour",
"updated_upstream_dns_toast": "Les serveurs DNS upstream sont mis \u00e0 jour", "updated_upstream_dns_toast": "Les serveurs DNS upstream sont mis \u00e0 jour",

View File

@ -115,9 +115,9 @@
"example_comment_meaning": "\u305f\u3060\u306e\u30b3\u30e1\u30f3\u30c8\u3067\u3059", "example_comment_meaning": "\u305f\u3060\u306e\u30b3\u30e1\u30f3\u30c8\u3067\u3059",
"example_comment_hash": "# \u3053\u3053\u3082\u30b3\u30e1\u30f3\u30c8\u3067\u3059", "example_comment_hash": "# \u3053\u3053\u3082\u30b3\u30e1\u30f3\u30c8\u3067\u3059",
"example_upstream_regular": "\u901a\u5e38\u306eDNS\uff08UDP\u3067\u306e\u554f\u3044\u5408\u308f\u305b\uff09", "example_upstream_regular": "\u901a\u5e38\u306eDNS\uff08UDP\u3067\u306e\u554f\u3044\u5408\u308f\u305b\uff09",
"example_upstream_dot": "\u6697\u53f7\u5316\u3055\u308c\u3066\u3044\u308b <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-over-TLS<\/a>", "example_upstream_dot": "\u6697\u53f7\u5316\u3055\u308c\u3066\u3044\u308b <0>DNS-over-TLS<\/0>",
"example_upstream_doh": "\u6697\u53f7\u5316\u3055\u308c\u3066\u3044\u308b <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a>", "example_upstream_doh": "\u6697\u53f7\u5316\u3055\u308c\u3066\u3044\u308b <0>DNS-over-HTTPS<\/0>",
"example_upstream_sdns": "<a href='https:\/\/dnscrypt.info\/' target='_blank'>DNSCrypt<\/a> \u307e\u305f\u306f <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a> \u30ea\u30be\u30eb\u30d0\u306e\u305f\u3081\u306b <a href='https:\/\/dnscrypt.info\/stamps\/' target='_blank'>DNS Stamps<\/a> \u3092\u4f7f\u3048\u307e\u3059", "example_upstream_sdns": "<0>DNSCrypt<\/0> \u307e\u305f\u306f <1>DNS-over-HTTPS<\/1> \u30ea\u30be\u30eb\u30d0\u306e\u305f\u3081\u306b <2>DNS Stamps<\/2> \u3092\u4f7f\u3048\u307e\u3059",
"example_upstream_tcp": "\u901a\u5e38\u306eDNS\uff08TCP\u3067\u306e\u554f\u3044\u5408\u308f\u305b\uff09", "example_upstream_tcp": "\u901a\u5e38\u306eDNS\uff08TCP\u3067\u306e\u554f\u3044\u5408\u308f\u305b\uff09",
"all_filters_up_to_date_toast": "\u3059\u3079\u3066\u306e\u30d5\u30a3\u30eb\u30bf\u306f\u65e2\u306b\u6700\u65b0\u3067\u3059", "all_filters_up_to_date_toast": "\u3059\u3079\u3066\u306e\u30d5\u30a3\u30eb\u30bf\u306f\u65e2\u306b\u6700\u65b0\u3067\u3059",
"updated_upstream_dns_toast": "\u4e0a\u6d41DNS\u30b5\u30fc\u30d0\u3092\u66f4\u65b0\u3057\u307e\u3057\u305f", "updated_upstream_dns_toast": "\u4e0a\u6d41DNS\u30b5\u30fc\u30d0\u3092\u66f4\u65b0\u3057\u307e\u3057\u305f",

View File

@ -1,6 +1,6 @@
{ {
"url_added_successfully": "Url adicionada com sucesso", "url_added_successfully": "URL adicionada com sucesso",
"check_dhcp_servers": "Verifique se h\u00e1 servidores DHCP", "check_dhcp_servers": "Verificar por servidores DHCP",
"save_config": "Salvar configura\u00e7\u00e3o", "save_config": "Salvar configura\u00e7\u00e3o",
"enabled_dhcp": "Servidor DHCP ativado", "enabled_dhcp": "Servidor DHCP ativado",
"disabled_dhcp": "Servidor DHCP desativado", "disabled_dhcp": "Servidor DHCP desativado",
@ -28,6 +28,7 @@
"dhcp_ip_addresses": "Endere\u00e7o de IP", "dhcp_ip_addresses": "Endere\u00e7o de IP",
"dhcp_table_hostname": "Hostname", "dhcp_table_hostname": "Hostname",
"dhcp_table_expires": "Expira", "dhcp_table_expires": "Expira",
"dhcp_warning": "Se voc\u00ea quiser ativar o servidor DHCP interno, certifique-se que n\u00e3o h\u00e1 outro servidor DHCP ativo. Caso contr\u00e1rio, isso pode impedir que outros dispositivos conectem \u00e1 internet!",
"back": "Voltar", "back": "Voltar",
"dashboard": "Painel", "dashboard": "Painel",
"settings": "Configura\u00e7\u00f5es", "settings": "Configura\u00e7\u00f5es",
@ -115,10 +116,11 @@
"example_comment": "! Aqui vai um coment\u00e1rio", "example_comment": "! Aqui vai um coment\u00e1rio",
"example_comment_meaning": "apenas um coment\u00e1rio", "example_comment_meaning": "apenas um coment\u00e1rio",
"example_comment_hash": "# Tamb\u00e9m um coment\u00e1rio", "example_comment_hash": "# Tamb\u00e9m um coment\u00e1rio",
"example_regex_meaning": "bloqueia o acesso aos dom\u00ednios correspondentes \u00e0 express\u00e3o regular especificada",
"example_upstream_regular": "DNS regular (atrav\u00e9s do UDP)", "example_upstream_regular": "DNS regular (atrav\u00e9s do UDP)",
"example_upstream_dot": "DNS criptografado <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>atrav\u00e9s do TLS<\/a>", "example_upstream_dot": "DNS criptografado <0>atrav\u00e9s do TLS<\/0>",
"example_upstream_doh": "DNS criptografado <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>atrav\u00e9s do HTTPS<\/a>", "example_upstream_doh": "DNS criptografado <0>atrav\u00e9s do HTTPS<\/0>",
"example_upstream_sdns": "Voc\u00ea pode usar <a href='https:\/\/dnscrypt.info\/stamps\/' target='_blank'>DNS Stamps<\/a>para o<a href='https:\/\/dnscrypt.info\/' target='_blank'>DNSCrypt<\/a>ou usar resolvedores<a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-sobre-HTTPS<\/a>", "example_upstream_sdns": "Voc\u00ea pode usar <0>DNS Stamps<\/0> para o <1>DNSCrypt<\/1> ou usar resolvedores <2>DNS-sobre-HTTPS<\/2>",
"example_upstream_tcp": "DNS regular (atrav\u00e9s do TCP)", "example_upstream_tcp": "DNS regular (atrav\u00e9s do TCP)",
"all_filters_up_to_date_toast": "Todos os filtros j\u00e1 est\u00e3o atualizados", "all_filters_up_to_date_toast": "Todos os filtros j\u00e1 est\u00e3o atualizados",
"updated_upstream_dns_toast": "Atualizado os servidores DNS upstream", "updated_upstream_dns_toast": "Atualizado os servidores DNS upstream",
@ -156,5 +158,93 @@
"category_label": "Categoria", "category_label": "Categoria",
"rule_label": "Regra", "rule_label": "Regra",
"filter_label": "Filtro", "filter_label": "Filtro",
"unknown_filter": "Filtro desconhecido {{filterId}}" "unknown_filter": "Filtro desconhecido {{filterId}}",
"install_welcome_title": "Bem-vindo(a) ao AdGuard Home!",
"install_welcome_desc": "O AdGuard Home \u00e9 um servidor de DNS para bloqueio de an\u00fancios e rastreamento em toda a rede. Sua finalidade \u00e9 permitir que voc\u00ea controle toda a sua rede e seus dispositivos sem precisar ter um programa instalado.",
"install_settings_title": "Interface web de administrador",
"install_settings_listen": "Interface de escuta",
"install_settings_port": "Porta",
"install_settings_interface_link": "A interface web de administrador do AdGuard estar\u00e1 dispon\u00edvel nos seguintes endere\u00e7os:",
"form_error_port": "Digite uma porta v\u00e1lida",
"install_settings_dns": "Servidor DNS",
"install_settings_dns_desc": "Voc\u00ea precisa configurar seu dispositivo ou roteador para usar o servidor DNS nos seguintes endere\u00e7os:",
"install_settings_all_interfaces": "Todas interfaces",
"install_auth_title": "Autentica\u00e7\u00e3o",
"install_auth_desc": "\u00c9 altamente recomend\u00e1vel configurar uma senha de autentica\u00e7\u00e3o na interface web de administrador do AdGuard Home. Mesmo que seja acess\u00edvel apenas em sua rede local, ainda \u00e9 importante proteg\u00ea-lo contra o acesso irrestrito.",
"install_auth_username": "Nome de usu\u00e1rio",
"install_auth_password": "Senha",
"install_auth_confirm": "Confirmar senha",
"install_auth_username_enter": "Digite o nome de usu\u00e1rio",
"install_auth_password_enter": "Digite a senha",
"install_step": "Passo",
"install_devices_title": "Configure seus dispositivos",
"install_devices_desc": "Para que o AdGuard Home comece a funcionar, voc\u00ea precisa configurar seus dispositivos para us\u00e1-lo.",
"install_submit_title": "Parab\u00e9ns!",
"install_submit_desc": "O procedimento de configura\u00e7\u00e3o est\u00e1 conclu\u00eddo e voc\u00ea est\u00e1 pronto para come\u00e7ar a usar o AdGuard Home.",
"install_devices_router": "Roteador",
"install_devices_router_desc": "Esta configura\u00e7\u00e3o cobrir\u00e1 automaticamente todos os dispositivos conectados ao seu roteador dom\u00e9stico e voc\u00ea n\u00e3o ir\u00e1 precisar configurar cada um deles manualmente.",
"install_devices_address": "O servidor de DNS do AdGuard Home est\u00e1 capturando os seguintes endere\u00e7os",
"install_devices_router_list_1": "Abra as configura\u00e7\u00f5es do seu roteador\nNo navegador digite o IP do roteador, o padr\u00e3o \u00e9 (http:\/\/192.168.0.1\/ ou http:\/\/192.168.1.1\/), e o login e senha \u00e9 admin\/admin; Se voc\u00ea n\u00e3o se lembra da senha, voc\u00ea pode redefinir a senha rapidamente pressionando um bot\u00e3o no pr\u00f3prio roteador. Alguns roteadores t\u00eam um aplicativo espec\u00edfico que j\u00e1 deve estar instalado em seu computador\/telefone.",
"install_devices_router_list_2": "Encontre as Configura\u00e7\u00f5es de DNS. Procure as letras DNS ao lado de um campo que permite dois ou tr\u00eas conjuntos de n\u00fameros, cada um dividido em quatro grupos de um a tr\u00eas n\u00fameros.",
"install_devices_router_list_3": "Digite aqui seu servidor do AdGuard Home.",
"install_devices_windows_list_1": "Abra o Painel de Controle pelo Menu Iniciar ou pela Pesquisa do Windows.",
"install_devices_windows_list_2": "Entre na categoria Rede e Internet e depois clique em Central de Rede e Compartilhamento.",
"install_devices_windows_list_3": "No lado esquerdo da janela clique em Alterar as configura\u00e7\u00f5es do adaptador.",
"install_devices_windows_list_4": "Selecione sua atual conex\u00e3o, clique nela com o bot\u00e3o direito do mouse e depois clique em Propriedades.",
"install_devices_windows_list_5": "Procure na lista por Internet Protocol Version 4 (TCP\/IP), selecione e clique em Propriedades novamente.",
"install_devices_windows_list_6": "Marque usar os seguintes endere\u00e7os de servidor DNS e digite os endere\u00e7os do servidores do AdGuard Home.",
"install_devices_macos_list_1": "Clique na \u00edcone da Apple e depois em Prefer\u00eancias do Sistema.",
"install_devices_macos_list_2": "Clique em Rede.",
"install_devices_macos_list_3": "Selecione a primeira conex\u00e3o da lista e clique em Avan\u00e7ado.",
"install_devices_macos_list_4": "Selecione a guia DNS e digite os endere\u00e7os dos servidores do AdGuard Home.",
"install_devices_android_list_1": "Na tela inicial do menu Android, toque em Configura\u00e7\u00f5es.",
"install_devices_android_list_2": "Toque em Wi-Fi. A tela listando todas as redes ser\u00e1 exibida (n\u00e3o \u00e9 poss\u00edvel configurar DNS personalizado para uma conex\u00e3o de dados m\u00f3veis)",
"install_devices_android_list_3": "Pressione prolongadamente a rede para a qual voc\u00ea est\u00e1 conectado e toque em Modificar rede",
"install_devices_android_list_4": "Em alguns dispositivos, talvez seja necess\u00e1rio marcar a caixa Avan\u00e7ado para ver as outras configura\u00e7\u00f5es. Para ajustar suas configura\u00e7\u00f5es de DNS do Android, voc\u00ea precisar\u00e1 alternar as configura\u00e7\u00f5es de IP de DHCP para Est\u00e1tico.",
"install_devices_android_list_5": "Altere o conjunto dos valores DNS 1 e DNS 2 para os endere\u00e7os de servidores do AdGuard Home.",
"install_devices_ios_list_1": "Na tela incial, toque em Ajustes.",
"install_devices_ios_list_2": "Selecione Wi-Fi no menu esquerdo (n\u00e3o \u00e9 poss\u00edvel configurar o DNS em conex\u00f5es de dados m\u00f3veis).",
"install_devices_ios_list_3": "Toque no nome da rede atualmente ativa.",
"install_devices_ios_list_4": "No campo DNS, digite os endere\u00e7os dos servidores do AdGuard Home.",
"get_started": "Come\u00e7ar",
"next": "Pr\u00f3ximo",
"open_dashboard": "Abrir painel",
"install_saved": "Salvo com sucesso",
"encryption_title": "Criptografia",
"encryption_desc": "Encryption (HTTPS\/TLS) support for both DNS and admin web interface",
"encryption_config_saved": "Configura\u00e7\u00e3o de criptografia salva",
"encryption_server": "Nome do servidor",
"encryption_server_enter": "Digite seu nome de dom\u00ednio",
"encryption_server_desc": "In order to use HTTPS, you need to enter the server name that matches your SSL certificate.",
"encryption_redirect": "Redirecionar automaticamente para HTTPS",
"encryption_redirect_desc": "If checked, AdGuard Home will automatically redirect you from HTTP to HTTPS addresses.",
"encryption_https": "Porta HTTPS",
"encryption_https_desc": "If HTTPS port is configured, AdGuard Home admin interface will be accessible via HTTPS, and it will also provide DNS-over-HTTPS on '\/dns-query' location.",
"encryption_dot": "Porta DNS-sobre-TLS",
"encryption_dot_desc": "Se essa porta estiver configurada, o AdGuard Home ir\u00e1 executar o servidor DNS-sobre- TSL nesta porta.",
"encryption_certificates": "Certificados",
"encryption_certificates_desc": "In order to use encryption, you need to provide a valid SSL certificates chain for your domain. You can get a free certificate on <0>{{link}}<\/0> or you can buy it from one of the trusted Certificate Authorities.",
"encryption_certificates_input": "Copie\/cole aqui seu certificado codificado em PEM.",
"encryption_status": "Status",
"encryption_expire": "Expira",
"encryption_key": "Chave privada",
"encryption_key_input": "Copie\/cole aqui a chave privada codificada em PEM para seu certificado.",
"encryption_enable": "Ativar criptografia (HTTPS, DNS-sobre-HTTPS e DNS-sobre-TLS)",
"encryption_enable_desc": "If encryption is enabled, AdGuard Home admin interface will work over HTTPS, and the DNS server will listen for requests over DNS-over-HTTPS and DNS-over-TLS.",
"encryption_chain_valid": "Cadeia de chave v\u00e1lida.",
"encryption_chain_invalid": "A cadeia de certificado \u00e9 inv\u00e1lida",
"encryption_key_valid": "Esta \u00e9 uma chave privada {{type}} v\u00e1lida",
"encryption_key_invalid": "Esta \u00e9 uma chave privada {{type}} inv\u00e1lida",
"encryption_subject": "Assunto",
"encryption_issuer": "Emissor",
"encryption_hostnames": "Hostnames",
"encryption_reset": "Voc\u00ea tem certeza de que deseja redefinir a configura\u00e7\u00e3o de criptografia?",
"topline_expiring_certificate": "Seu certificado SSL est\u00e1 prestes a expirar. Atualize suas <0>configura\u00e7\u00f5es de criptografia<\/]0>",
"topline_expired_certificate": "Seu certificado SSL est\u00e1 expirado. Atualize suas <0>configura\u00e7\u00f5es de criptografia<\/0>",
"form_error_port_range": "Digite um porta entre 80 e 65535",
"form_error_port_unsafe": "Esta porta n\u00e3o \u00e9 segura",
"form_error_equal": "N\u00e3o deve ser igual",
"form_error_password": "Senhas n\u00e3o coincidem",
"reset_settings": "Redefinir configura\u00e7\u00f5es",
"update_announcement": "AdGuard Home {{version}} est\u00e1 dispon\u00edvel!<0>Clique aqui<\/0> para mais informa\u00e7\u00f5es."
} }

View File

@ -115,9 +115,9 @@
"example_comment_meaning": "\u043a\u043e\u043c\u043c\u0435\u043d\u0442\u0430\u0440\u0438\u0439", "example_comment_meaning": "\u043a\u043e\u043c\u043c\u0435\u043d\u0442\u0430\u0440\u0438\u0439",
"example_comment_hash": "# \u0418 \u0432\u043e\u0442 \u0442\u0430\u043a \u0442\u043e\u0436\u0435", "example_comment_hash": "# \u0418 \u0432\u043e\u0442 \u0442\u0430\u043a \u0442\u043e\u0436\u0435",
"example_upstream_regular": "\u043e\u0431\u044b\u0447\u043d\u044b\u0439 DNS (\u043f\u043e\u0432\u0435\u0440\u0445 UDP)", "example_upstream_regular": "\u043e\u0431\u044b\u0447\u043d\u044b\u0439 DNS (\u043f\u043e\u0432\u0435\u0440\u0445 UDP)",
"example_upstream_dot": "\u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-\u043f\u043e\u0432\u0435\u0440\u0445-TLS<\/a>", "example_upstream_dot": "\u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 <0>DNS-\u043f\u043e\u0432\u0435\u0440\u0445-TLS<\/0>",
"example_upstream_doh": "\u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-\u043f\u043e\u0432\u0435\u0440\u0445-HTTPS<\/a>", "example_upstream_doh": "\u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 <0>DNS-\u043f\u043e\u0432\u0435\u0440\u0445-HTTPS<\/0>",
"example_upstream_sdns": "\u0432\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c <a href='https:\/\/dnscrypt.info\/stamps\/' target='_blank'>DNS Stamps<\/a> \u0434\u043b\u044f <a href='https:\/\/dnscrypt.info\/' target='_blank'>DNSCrypt<\/a> \u0438\u043b\u0438 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a> \u0440\u0435\u0437\u043e\u043b\u0432\u0435\u0440\u043e\u0432", "example_upstream_sdns": "\u0432\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c <0>DNS Stamps<\/0> \u0434\u043b\u044f <1>DNSCrypt<\/1> \u0438\u043b\u0438 <2>DNS-over-HTTPS<\/2> \u0440\u0435\u0437\u043e\u043b\u0432\u0435\u0440\u043e\u0432",
"example_upstream_tcp": "\u043e\u0431\u044b\u0447\u043d\u044b\u0439 DNS (\u043f\u043e\u0432\u0435\u0440\u0445 TCP)", "example_upstream_tcp": "\u043e\u0431\u044b\u0447\u043d\u044b\u0439 DNS (\u043f\u043e\u0432\u0435\u0440\u0445 TCP)",
"all_filters_up_to_date_toast": "\u0412\u0441\u0435 \u0444\u0438\u043b\u044c\u0442\u0440\u044b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u044b", "all_filters_up_to_date_toast": "\u0412\u0441\u0435 \u0444\u0438\u043b\u044c\u0442\u0440\u044b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u044b",
"updated_upstream_dns_toast": "Upstream DNS-\u0441\u0435\u0440\u0432\u0435\u0440\u044b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u044b", "updated_upstream_dns_toast": "Upstream DNS-\u0441\u0435\u0440\u0432\u0435\u0440\u044b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u044b",

View File

@ -28,6 +28,7 @@
"dhcp_ip_addresses": "IP-adresser", "dhcp_ip_addresses": "IP-adresser",
"dhcp_table_hostname": "V\u00e4rdnamn", "dhcp_table_hostname": "V\u00e4rdnamn",
"dhcp_table_expires": "Utg\u00e5r", "dhcp_table_expires": "Utg\u00e5r",
"dhcp_warning": "Om du vill koppla in den inbyggda DHCP-servern m\u00e5ste du f\u00f6rs\u00e4kra dig om att det finns n\u00e5gra andra DHCP-servar som i s\u00e5 fall riskerar att orsaka avbrott p\u00e5 internet!",
"back": "Tiilbaka", "back": "Tiilbaka",
"dashboard": "Kontrollpanel", "dashboard": "Kontrollpanel",
"settings": "Inst\u00e4llningar", "settings": "Inst\u00e4llningar",
@ -115,10 +116,11 @@
"example_comment": "! H\u00e4r kommer en kommentar", "example_comment": "! H\u00e4r kommer en kommentar",
"example_comment_meaning": "Endast en kommentar", "example_comment_meaning": "Endast en kommentar",
"example_comment_hash": "# Ocks\u00e5 en kommentar", "example_comment_hash": "# Ocks\u00e5 en kommentar",
"example_regex_meaning": "blockera \u00e5tkomst till en dom\u00e4n som matchar det angivna uttrycket",
"example_upstream_regular": "vanlig DNS (\u00f6ver UDP)", "example_upstream_regular": "vanlig DNS (\u00f6ver UDP)",
"example_upstream_dot": "krypterat <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-over-TLS<\/a>", "example_upstream_dot": "krypterat <0>DNS-over-TLS<\/0>",
"example_upstream_doh": "krypterat <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a>", "example_upstream_doh": "krypterat <0>DNS-over-HTTPS<\/0>",
"example_upstream_sdns": "Du kan anv\u00e4nda <a href='https:\/\/dnscrypt.info\/stamps\/' target='_blank'>DNS-stamps<\/a> f\u00f6r <a href='https:\/\/dnscrypt.info\/' target='_blank'>DNSCrypt<\/a> eller <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-\u00f6ver-HTTPS<\/a>\n-resolvers", "example_upstream_sdns": "Du kan anv\u00e4nda <0>DNS-stamps<\/0> f\u00f6r <1>DNSCrypt<\/1> eller <2>DNS-\u00f6ver-HTTPS<\/2>\n-resolvers",
"example_upstream_tcp": "vanlig DNS (\u00f6ver UDP)", "example_upstream_tcp": "vanlig DNS (\u00f6ver UDP)",
"all_filters_up_to_date_toast": "Alla filter \u00e4r redan aktuella", "all_filters_up_to_date_toast": "Alla filter \u00e4r redan aktuella",
"updated_upstream_dns_toast": "Uppdaterade uppstr\u00f6ms-dns-servrar", "updated_upstream_dns_toast": "Uppdaterade uppstr\u00f6ms-dns-servrar",
@ -156,5 +158,93 @@
"category_label": "Kategori", "category_label": "Kategori",
"rule_label": "Regel", "rule_label": "Regel",
"filter_label": "Filter", "filter_label": "Filter",
"unknown_filter": "Ok\u00e4nt filter {{filterId}}" "unknown_filter": "Ok\u00e4nt filter {{filterId}}",
"install_welcome_title": "V\u00e4lkommen till AdGuard Home!",
"install_welcome_desc": "AdGuard Home \u00e4r en DNS-server f\u00f6r n\u00e4tverkst\u00e4ckande annons- och sp\u00e5rningsblockering. Dess syfte \u00e4r att de dig kontroll \u00f6ver hela n\u00e4tverket och alla dina enheter, utan behov av att anv\u00e4nda klientbaserade program.",
"install_settings_title": "Administrat\u00f6rens webbgr\u00e4nssnitt",
"install_settings_listen": "\u00d6vervakningsgr\u00e4nssnitt",
"install_settings_port": "Port",
"install_settings_interface_link": "Din administrat\u00f6rssida f\u00f6r AdGuard Home finns p\u00e5 f\u00f6ljande adresser:",
"form_error_port": "Skriv in ett giltigt portnummer",
"install_settings_dns": "DNS-server",
"install_settings_dns_desc": "Du beh\u00f6ver st\u00e4lla in dina enheter eller din router f\u00f6r att anv\u00e4nda DNS-server p\u00e5 f\u00f6ljande adresser.",
"install_settings_all_interfaces": "Alla gr\u00e4nssnitt",
"install_auth_title": "Autentisering",
"install_auth_desc": "Det rekommenderas starkt att st\u00e4lla in l\u00f6senordsskydd till webbgr\u00e4nssnittets administrativa del i ditt AdGuard Home. \u00c4ven om den endast \u00e4r \u00e5tkomlig p\u00e5 ditt lokala n\u00e4tverk rekommenderas det \u00e4nd\u00e5 att skydda det mot o\u00f6nskad \u00e5tkomst.",
"install_auth_username": "Anv\u00e4ndarnamn",
"install_auth_password": "L\u00f6senord",
"install_auth_confirm": "Bekr\u00e4fta l\u00f6senord",
"install_auth_username_enter": "Skriv in anv\u00e4ndarnamn",
"install_auth_password_enter": "Skriv in l\u00f6senord",
"install_step": "Steg",
"install_devices_title": "St\u00e4ll in dina enheter",
"install_devices_desc": "F\u00f6r att kunna anv\u00e4nda AdGuard Home m\u00e5ste du s\u00e4lla in dina enheter f\u00f6r att utnyttja den.",
"install_submit_title": "Grattis!",
"install_submit_desc": "Inst\u00e4llningsproceduren \u00e4r klar och du kan b\u00f6rja anv\u00e4nda AdGuard Home.",
"install_devices_router": "Router",
"install_devices_router_desc": "Den h\u00e4r anpassningen kommer att automatiskt t\u00e4cka in alla de enheter som \u00e4r anslutna till din hemmarouter och du beh\u00f6ver d\u00e4rf\u00f6r inte konfigurera var och en individuellt.",
"install_devices_address": "AdGuard Home DNS-server t\u00e4cker f\u00f6ljande adresser",
"install_devices_router_list_1": "\u00d6ppna routern Inst\u00e4llningar. Vanligtvis f\u00e5r man \u00e5tkomst via en URL (http:\/\/192.168.0.1 eller 192.168.1.1)- Du kommer att bli ombes att ange ett l\u00f6senord. L\u00f6senordet kan st\u00e5 angivet p\u00e5 routerns bak- eller undersida. Om l\u00f6senordet \u00e4ndrats och du inte k\u00e4nner till det kan du \u00e5terst\u00e4lla med Reset-knappen. En del routrar kr\u00e4ver en s\u00e4rskild applikation som skall finnas p\u00e5 antingen din dator eller i din mobil.",
"install_devices_router_list_2": "Leta upp DHCP\/DNS-inst\u00e4llningarna. Titta efter DNS-tecken intill ett f\u00e4lt med tv\u00e5 eller tre upps\u00e4ttningar siffror, var och en uppdelade i grupper om fyra med en eller tre siffror.",
"install_devices_router_list_3": "Ange serveradressen till ditt AdGuard Home.",
"install_devices_windows_list_1": "\u00d6ppna Kontrollpanelen via Start eller Windows S\u00f6k.",
"install_devices_windows_list_2": "V\u00e4lj N\u00e4tverks och delningscenter, N\u00e4tverk och Internet.",
"install_devices_windows_list_3": "Leta upp \u00c4ndra n\u00e4tverkskortsalternativ",
"install_devices_windows_list_4": "Markera din aktiva anslutning. H\u00f6gerklicka p\u00e5 den och v\u00e4lj Egenskaper.",
"install_devices_windows_list_5": "Markera Internet Protocol Version 4 (TCP\/IP) och klicka p\u00e5 knappen Egenskaper.",
"install_devices_windows_list_6": "Markera Anv\u00e4nd f\u00f6ljande DNS-serveradresser och skriv in adresserna till ditt AdGuard Home.",
"install_devices_macos_list_1": "Klicka p\u00e5 Apple-ikonen och v\u00e4lj Systemalternativ.",
"install_devices_macos_list_2": "Klicka p\u00e5 N\u00e4tverk.",
"install_devices_macos_list_3": "V\u00e4lj den f\u00f6rsta anslutningen i listan och klicka p\u00e5 Avancerat.",
"install_devices_macos_list_4": "Klicka p\u00e5 DNS-fliken och skriv in AdGuard Homes serveradresser",
"install_devices_android_list_1": "V\u00e4lj Inst\u00e4llningar fr\u00e5n Androids hemknapp",
"install_devices_android_list_2": "Tryck p\u00e5 N\u00e4tverk och Internet, Wi-Fi. Alla tillg\u00e4ngliga n\u00e4tverk visas i en lista (det g\u00e5r inte all v\u00e4lja egen DNS p\u00e5 mobiln\u00e4tverk.",
"install_devices_android_list_3": "H\u00e5ll ner p\u00e5 n\u00e4tverksnamnet som du \u00e4r ansluten till och v\u00e4lj \u00c4ndra n\u00e4tverk.",
"install_devices_android_list_4": "P\u00e5 en del enheter kan du beh\u00f6va v\u00e4lja Avancerat f\u00f6r att komma \u00e5t ytterligare inst\u00e4llningar. F\u00f6r att \u00e4ndra p\u00e5 DNS-inst\u00e4llningar m\u00e5ste du byta IP-inst\u00e4llning fr\u00e5n DHCP till Statisk. P\u00e5 Android Pie v\u00e4ljs Privat DNS p\u00e5 N\u00e4tverk och internet.",
"install_devices_android_list_5": "\u00c4ndra DNS 1 och DNS 2 till serveradresserna f\u00f6r AdGuard Home.",
"install_devices_ios_list_1": "Tryck Inst\u00e4llningar fr\u00e5n hemsk\u00e4rmen.",
"install_devices_ios_list_2": "V\u00e4lj Wi_Fi p\u00e5 den v\u00e4nstra menyn (det g\u00e5r inte att st\u00e4lla in egen DNS f\u00f6r mobila n\u00e4tverk).",
"install_devices_ios_list_3": "Tryck p\u00e5 namnet p\u00e5 den aktiva anslutningen.",
"install_devices_ios_list_4": "Skriv in AdGuard Homes serveradresser i DNS-f\u00e4lten.",
"get_started": "Kom ig\u00e5ng",
"next": "N\u00e4sta",
"open_dashboard": "\u00d6ppna Kontrollbordet",
"install_saved": "Sparat utan fel",
"encryption_title": "Encryption",
"encryption_desc": "Encryption (HTTPS\/TLS) support for both DNS and admin web interface",
"encryption_config_saved": "Encryption config saved",
"encryption_server": "Server name",
"encryption_server_enter": "Enter your domain name",
"encryption_server_desc": "In order to use HTTPS, you need to enter the server name that matches your SSL certificate.",
"encryption_redirect": "Redirect to HTTPS automatically",
"encryption_redirect_desc": "If checked, AdGuard Home will automatically redirect you from HTTP to HTTPS addresses.",
"encryption_https": "HTTPS port",
"encryption_https_desc": "If HTTPS port is configured, AdGuard Home admin interface will be accessible via HTTPS, and it will also provide DNS-over-HTTPS on '\/dns-query' location.",
"encryption_dot": "DNS-over-TLS port",
"encryption_dot_desc": "If this port is configured, AdGuard Home will run a DNS-over-TLS server on this port.",
"encryption_certificates": "Certificates",
"encryption_certificates_desc": "In order to use encryption, you need to provide a valid SSL certificates chain for your domain. You can get a free certificate on <0>{{link}}<\/0> or you can buy it from one of the trusted Certificate Authorities.",
"encryption_certificates_input": "Copy\/paste your PEM-encoded certificates here.",
"encryption_status": "Status",
"encryption_expire": "Expires",
"encryption_key": "Private key",
"encryption_key_input": "Copy\/paste your PEM-encoded private key for your certificate here.",
"encryption_enable": "Enable Encryption (HTTPS, DNS-over-HTTPS, and DNS-over-TLS)",
"encryption_enable_desc": "If encryption is enabled, AdGuard Home admin interface will work over HTTPS, and the DNS server will listen for requests over DNS-over-HTTPS and DNS-over-TLS.",
"encryption_chain_valid": "Certificate chain is valid",
"encryption_chain_invalid": "Certificate chain is invalid",
"encryption_key_valid": "This is a valid {{type}} private key",
"encryption_key_invalid": "This is an invalid {{type}} private key",
"encryption_subject": "Subject",
"encryption_issuer": "Issuer",
"encryption_hostnames": "Hostnames",
"encryption_reset": "Are you sure you want to reset encryption settings?",
"topline_expiring_certificate": "Your SSL certificate is about to expire. Update <0>Encryption settings<\/0>.",
"topline_expired_certificate": "Your SSL certificate is expired. Update <0>Encryption settings<\/0>.",
"form_error_port_range": "Enter port value in the range of 80-65535",
"form_error_port_unsafe": "This is an unsafe port",
"form_error_equal": "Shouldn't be equal",
"form_error_password": "L\u00f6senorden \u00f6verensst\u00e4mmer inte",
"reset_settings": "Reset settings",
"update_announcement": "AdGuard Home {{version}} is now available! <0>Click here<\/0> for more info."
} }

View File

@ -113,9 +113,9 @@
"example_comment_meaning": "Ch\u1ec9 l\u00e0 m\u1ed9t ch\u00fa th\u00edch", "example_comment_meaning": "Ch\u1ec9 l\u00e0 m\u1ed9t ch\u00fa th\u00edch",
"example_comment_hash": "# C\u0169ng l\u00e0 m\u1ed9t ch\u00fa th\u00edch", "example_comment_hash": "# C\u0169ng l\u00e0 m\u1ed9t ch\u00fa th\u00edch",
"example_upstream_regular": "DNS th\u00f4ng th\u01b0\u1eddng (d\u00f9ng UDP)", "example_upstream_regular": "DNS th\u00f4ng th\u01b0\u1eddng (d\u00f9ng UDP)",
"example_upstream_dot": "\u0111\u01b0\u1ee3c m\u00e3 ho\u00e1 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-over-TLS<\/a>", "example_upstream_dot": "\u0111\u01b0\u1ee3c m\u00e3 ho\u00e1 <0>DNS-over-TLS<\/0>",
"example_upstream_doh": "\u0111\u01b0\u1ee3c m\u00e3 ho\u00e1 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a>", "example_upstream_doh": "\u0111\u01b0\u1ee3c m\u00e3 ho\u00e1 <0>DNS-over-HTTPS<\/0>",
"example_upstream_sdns": "b\u1ea1n c\u00f3 th\u1ec3 s\u1eed d\u1ee5ng <a href='https:\/\/dnscrypt.info\/stamps\/' target='_blank'>DNS Stamps<\/a> for <a href='https:\/\/dnscrypt.info\/' target='_blank'>DNSCrypt<\/a> ho\u1eb7c<a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a> ", "example_upstream_sdns": "b\u1ea1n c\u00f3 th\u1ec3 s\u1eed d\u1ee5ng <0>DNS Stamps<\/0> for <1>DNSCrypt<\/1> ho\u1eb7c <2>DNS-over-HTTPS<\/2> ",
"example_upstream_tcp": "DNS th\u00f4ng th\u01b0\u1eddng(d\u00f9ng TCP)", "example_upstream_tcp": "DNS th\u00f4ng th\u01b0\u1eddng(d\u00f9ng TCP)",
"all_filters_up_to_date_toast": "T\u1ea5t c\u1ea3 b\u1ed9 l\u1ecdc \u0111\u00e3 \u0111\u01b0\u1ee3c c\u1eadp nh\u1eadt", "all_filters_up_to_date_toast": "T\u1ea5t c\u1ea3 b\u1ed9 l\u1ecdc \u0111\u00e3 \u0111\u01b0\u1ee3c c\u1eadp nh\u1eadt",
"updated_upstream_dns_toast": "\u0110\u00e3 c\u1eadp nh\u1eadt m\u00e1y ch\u1ee7 DNS t\u00ecm ki\u1ebfm", "updated_upstream_dns_toast": "\u0110\u00e3 c\u1eadp nh\u1eadt m\u00e1y ch\u1ee7 DNS t\u00ecm ki\u1ebfm",

View File

@ -0,0 +1,253 @@
{
"upstream_parallel": "\u901a\u8fc7\u540c\u65f6\u67e5\u8be2\u6240\u6709\u4e0a\u6d41\u670d\u52a1\u5668\u4ee5\u4f7f\u7528\u5e76\u884c\u67e5\u8be2\u52a0\u901f\u89e3\u6790",
"bootstrap_dns": "Bootstrap DNS \u670d\u52a1\u5668",
"bootstrap_dns_desc": "Bootstrap DNS servers are used to resolve IP addresses of the DoH\/DoT resolvers you specify as upstreams.",
"url_added_successfully": "\u7f51\u5740\u6dfb\u52a0\u6210\u529f",
"check_dhcp_servers": "\u68c0\u67e5 DHCP \u670d\u52a1\u5668",
"save_config": "\u4fdd\u5b58\u914d\u7f6e",
"enabled_dhcp": "DHCP \u670d\u52a1\u5668\u5df2\u542f\u7528",
"disabled_dhcp": "DHCP \u670d\u52a1\u5668\u5df2\u7981\u7528",
"dhcp_title": "DHCP \u670d\u52a1\u5668\uff08\u5b9e\u9a8c\u6027\uff09",
"dhcp_description": "\u5982\u679c\u4f60\u7684\u8def\u7531\u5668\u6ca1\u6709\u63d0\u4f9b\u52a8\u6001\u4e3b\u673a\u8bbe\u7f6e\u534f\u8bae\uff08DHCP\uff09\u8bbe\u7f6e\uff0c\u4f60\u53ef\u4ee5\u4f7f\u7528 AdGuard \u5185\u7f6e\u7684 DHCP \u670d\u52a1\u5668\u3002",
"dhcp_enable": "\u542f\u7528 DHCP \u670d\u52a1\u5668",
"dhcp_disable": "\u7981\u7528 DHCP \u670d\u52a1\u5668",
"dhcp_not_found": "\u5728\u5f53\u524d\u7f51\u7edc\u4e2d\u672a\u68c0\u6d4b\u5230 DHCP \u670d\u52a1\u5668\u3002\u60a8\u53ef\u4ee5\u5b89\u5168\u5730\u542f\u7528\u5185\u7f6e DHCP \u670d\u52a1\u5668\u3002",
"dhcp_found": "\u5728\u5f53\u524d\u7f51\u7edc\u4e2d\u68c0\u6d4b\u5230 DHCP \u670d\u52a1\u5668\u3002\u5982\u679c\u542f\u7528\u5185\u7f6e\u7684 DHCP \u670d\u52a1\u5668\u53ef\u80fd\u4e0d\u5b89\u5168\u3002",
"dhcp_leases": "DHCP \u79df\u7ea6",
"dhcp_leases_not_found": "\u672a\u68c0\u6d4b\u5230 DHCP \u79df\u7ea6",
"dhcp_config_saved": "\u4fdd\u5b58 DHCP \u670d\u52a1\u5668\u914d\u7f6e",
"form_error_required": "\u5fc5\u586b\u5b57\u6bb5",
"form_error_ip_format": "\u65e0\u6548\u7684 IPv4 \u683c\u5f0f",
"form_error_positive": "\u5fc5\u987b\u5927\u4e8e 0",
"dhcp_form_gateway_input": "\u7f51\u5173 IP",
"dhcp_form_subnet_input": "\u5b50\u7f51\u63a9\u7801",
"dhcp_form_range_title": "IP \u5730\u5740\u8303\u56f4",
"dhcp_form_range_start": "\u8d77\u59cb IP \u5730\u5740",
"dhcp_form_range_end": "\u672b\u5c3e IP \u5730\u5740",
"dhcp_form_lease_title": "DHCP \u79df\u7ea6\u65f6\u95f4\uff08\u79d2\uff09",
"dhcp_form_lease_input": "\u79df\u671f",
"dhcp_interface_select": "\u9009\u62e9 DHCP \u63a5\u53e3",
"dhcp_hardware_address": "\u786c\u4ef6\u5730\u5740",
"dhcp_ip_addresses": "IP \u5730\u5740",
"dhcp_table_hostname": "\u4e3b\u673a\u540d",
"dhcp_table_expires": "\u5230\u671f",
"dhcp_warning": "\u5982\u679c\u4f60\u60f3\u8981\u542f\u7528\u5185\u7f6e\u7684 DHCP \u670d\u52a1\u5668\uff0c\u8bf7\u786e\u4fdd\u5728\u5f53\u524d\u7f51\u7edc\u4e2d\u6ca1\u6709\u5176\u5b83\u6d3b\u52a8\u7684 DHCP \u670d\u52a1\u5668\u3002\u5426\u5219\uff0c\u6b64\u64cd\u4f5c\u53ef\u80fd\u4f1a\u7834\u574f\u5df2\u8fde\u63a5\u8bbe\u5907\u7684\u7f51\u7edc\u8fde\u63a5\uff01",
"back": "\u8fd4\u56de",
"dashboard": "\u4eea\u8868\u76d8",
"settings": "\u8bbe\u7f6e",
"filters": "\u8fc7\u6ee4\u5668",
"query_log": "\u67e5\u8be2\u65e5\u5fd7",
"faq": "\u5e38\u89c1\u95ee\u9898",
"version": "\u7248\u672c",
"address": "\u5730\u5740",
"on": "\u542f\u7528\u4e2d",
"off": "\u7981\u7528\u4e2d",
"copyright": "\u7248\u6743",
"homepage": "\u4e3b\u9875",
"report_an_issue": "\u95ee\u9898\u53cd\u9988",
"enable_protection": "\u542f\u7528\u4fdd\u62a4",
"enabled_protection": "\u4fdd\u62a4\u5df2\u542f\u7528",
"disable_protection": "\u7981\u7528\u4fdd\u62a4",
"disabled_protection": "\u4fdd\u62a4\u5df2\u7981\u7528",
"refresh_statics": "\u5237\u65b0\u72b6\u6001",
"dns_query": "DNS\u67e5\u8be2",
"blocked_by": "\u5df2\u88ab\u8fc7\u6ee4\u5668\u62e6\u622a",
"stats_malware_phishing": "\u88ab\u62e6\u622a\u7684\u6076\u610f\/\u9493\u9c7c\u7f51\u7ad9",
"stats_adult": "\u88ab\u62e6\u622a\u7684\u6210\u4eba\u7f51\u7ad9",
"stats_query_domain": "\u8bf7\u6c42\u57df\u540d\u6392\u884c",
"for_last_24_hours": "\u5728\u8fc7\u53bb 24 \u5c0f\u65f6",
"no_domains_found": "\u672a\u627e\u5230\u57df\u540d",
"requests_count": "\u8bf7\u6c42\u6570",
"top_blocked_domains": "\u88ab\u62e6\u622a\u57df\u540d\u6392\u884c",
"top_clients": "\u5ba2\u6237\u7aef\u6392\u884c",
"no_clients_found": "\u672a\u627e\u5230\u5ba2\u6237\u7aef",
"general_statistics": "\u6982\u51b5\u7edf\u8ba1",
"number_of_dns_query_24_hours": "\u8fc7\u53bb 24 \u5c0f\u65f6\u5185\u5904\u7406\u7684 DNS \u8bf7\u6c42\u603b\u6570",
"number_of_dns_query_blocked_24_hours": "\u88ab\u5e7f\u544a\u8fc7\u6ee4\u5668\u548c Hosts \u62e6\u622a\u6e05\u5355\u62e6\u622a\u7684 DNS \u8bf7\u6c42\u603b\u6570",
"number_of_dns_query_blocked_24_hours_by_sec": "\u88ab AdGuard \u5b89\u5168\u6d4f\u89c8\u6a21\u5757\u62e6\u622a\u7684 DNS \u8bf7\u6c42\u603b\u6570",
"number_of_dns_query_blocked_24_hours_adult": "\u88ab\u62e6\u622a\u7684\u6210\u4eba\u7f51\u7ad9\u603b\u6570",
"enforced_save_search": "\u5f3a\u5236\u5b89\u5168\u641c\u7d22",
"number_of_dns_query_to_safe_search": "\u542f\u7528\u5f3a\u5236\u5b89\u5168\u641c\u7d22\u540e\u5bf9\u641c\u7d22\u5f15\u64ce\u7684 DNS \u8bf7\u6c42\u603b\u6570",
"average_processing_time": "\u5e73\u5747\u5904\u7406\u65f6\u95f4",
"average_processing_time_hint": "\u5904\u7406 DNS \u8bf7\u6c42\u7684\u5e73\u5747\u65f6\u95f4\uff08\u6beb\u79d2\uff09",
"block_domain_use_filters_and_hosts": "\u4f7f\u7528\u8fc7\u6ee4\u5668\u548c Hosts \u6587\u4ef6\u4ee5\u62e6\u622a\u6307\u5b9a\u57df\u540d",
"filters_block_toggle_hint": "\u4f60\u53ef\u4ee5\u5728 <a href='#filters'>\u8fc7\u6ee4\u5668<\/a> \u8bbe\u7f6e\u4e2d\u6dfb\u52a0\u8fc7\u6ee4\u89c4\u5219\u3002",
"use_adguard_browsing_sec": "\u4f7f\u7528 AdGuard\u3010\u6d4f\u89c8\u5b89\u5168\u3011\u7f51\u9875\u670d\u52a1",
"use_adguard_browsing_sec_hint": "AdGuard Home \u5c06\u68c0\u67e5\u57df\u540d\u662f\u5426\u88ab\u6d4f\u89c8\u5b89\u5168\u670d\u52a1\u5217\u5165\u9ed1\u540d\u5355\u3002\u5b83\u5c06\u4f7f\u7528\u9690\u79c1\u6027\u5f3a\u7684\u68c0\u7d22 API \u6765\u6267\u884c\u68c0\u67e5\uff0c\u53ea\u6709\u57df\u540d\u7684 SHA256 \u7684\u77ed\u524d\u7f00\u4f1a\u88ab\u53d1\u9001\u5230\u670d\u52a1\u5668\u3002",
"use_adguard_parental": "\u4f7f\u7528 AdGuard \u3010\u5bb6\u957f\u63a7\u5236\u3011\u670d\u52a1",
"use_adguard_parental_hint": "AdGuard Home \u5c06\u4f7f\u7528\u4e0e\u6d4f\u89c8\u5b89\u5168\u670d\u52a1\u76f8\u540c\u7684\u9690\u79c1\u6027\u5f3a\u7684 API \u6765\u68c0\u67e5\u57df\u540d\u6307\u5411\u7684\u7f51\u7ad9\u662f\u5426\u5305\u542b\u6210\u4eba\u5185\u5bb9\u3002",
"enforce_safe_search": "\u5f3a\u5236\u5b89\u5168\u641c\u7d22",
"enforce_save_search_hint": "AdGuard Home \u5c06\u5bf9\u4ee5\u4e0b\u641c\u7d22\u5f15\u64ce\u5f3a\u5236\u542f\u7528\u5b89\u5168\u641c\u7d22\uff1aGoogle\u3001YouTube\u3001Bing \u548c Yandex\u3002",
"no_servers_specified": "\u672a\u627e\u5230\u6307\u5b9a\u7684\u670d\u52a1\u5668",
"no_settings": "\u672a\u627e\u5230\u8bbe\u7f6e",
"general_settings": "\u5e38\u89c4\u8bbe\u7f6e",
"upstream_dns": "\u4e0a\u6e38 DNS \u670d\u52a1\u5668",
"upstream_dns_hint": "\u5982\u679c\u6b64\u5904\u7559\u7a7a\uff0cAdGuard Home \u5c06\u4f1a\u4f7f\u7528 <a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a> \u4f5c\u4e3a\u4e0a\u6e38 DNS\u3002\u5982\u679c\u60f3\u8981\u4f7f\u7528\u4f7f\u7528 DNS over TLS\uff0c\u8bf7\u4ee5 tls:\/\/ \u4e3a\u5f00\u5934\u3002",
"test_upstream_btn": "\u6d4b\u8bd5\u4e0a\u6e38 DNS",
"apply_btn": "\u5e94\u7528",
"disabled_filtering_toast": "\u8fc7\u6ee4\u5668\u5df2\u7981\u7528",
"enabled_filtering_toast": "\u8fc7\u6ee4\u5668\u5df2\u542f\u7528",
"disabled_safe_browsing_toast": "\u5b89\u5168\u6d4f\u89c8\u5df2\u7981\u7528",
"enabled_safe_browsing_toast": "\u5b89\u5168\u6d4f\u89c8\u5df2\u542f\u7528",
"disabled_parental_toast": "\u5bb6\u957f\u63a7\u5236\u5df2\u7981\u7528",
"enabled_parental_toast": "\u5bb6\u957f\u63a7\u5236\u5df2\u542f\u7528",
"disabled_safe_search_toast": "\u5b89\u5168\u641c\u7d22\u5df2\u7981\u7528",
"enabled_save_search_toast": "\u5b89\u5168\u641c\u7d22\u5df2\u542f\u7528",
"enabled_table_header": "\u5df2\u542f\u7528",
"name_table_header": "\u540d\u79f0",
"filter_url_table_header": "\u8fc7\u6ee4\u5668\u5730\u5740",
"rules_count_table_header": "\u89c4\u5219\u6570",
"last_time_updated_table_header": "\u4e0a\u6b21\u66f4\u65b0\u65f6\u95f4",
"actions_table_header": "\u6d3b\u8dc3\u72b6\u6001",
"delete_table_action": "\u5220\u9664",
"filters_and_hosts": "\u8fc7\u6ee4\u5668\u548c Hosts \u62e6\u622a\u6e05\u5355",
"filters_and_hosts_hint": "AdGuard Home \u53ef\u4ee5\u89e3\u6790\u57fa\u7840\u7684 adblock \u89c4\u5219\u548c Hosts \u8bed\u6cd5\u3002",
"no_filters_added": "\u672a\u6dfb\u52a0\u4efb\u4f55\u8fc7\u6ee4\u5668",
"add_filter_btn": "\u6dfb\u52a0\u8fc7\u6ee4\u5668",
"cancel_btn": "\u53d6\u6d88",
"enter_name_hint": "\u8f93\u5165\u540d\u79f0",
"enter_url_hint": "\u8f93\u5165 URL",
"check_updates_btn": "\u68c0\u67e5\u66f4\u65b0",
"new_filter_btn": "\u8ba2\u9605\u65b0\u7684\u8fc7\u6ee4\u5668",
"enter_valid_filter_url": "\u8f93\u5165\u4e00\u4e2a\u8fc7\u6ee4\u5668\u8ba2\u9605\u6216 Hosts \u6587\u4ef6\u7684\u6709\u6548 URL",
"custom_filter_rules": "\u81ea\u5b9a\u4e49\u8fc7\u6ee4\u5668\u89c4\u5219",
"custom_filter_rules_hint": "\u8bf7\u786e\u4fdd\u6bcf\u884c\u53ea\u8f93\u5165\u4e00\u6761\u89c4\u5219\u3002\u4f60\u53ef\u4ee5\u8f93\u5165\u7b26\u5408 adblock \u8bed\u6cd5\u6216 Hosts \u8bed\u6cd5\u7684\u89c4\u5219\u3002",
"examples_title": "\u8303\u4f8b",
"example_meaning_filter_block": "\u62e6\u622a example.org \u57df\u540d\u53ca\u5176\u6240\u6709\u5b50\u57df\u540d",
"example_meaning_filter_whitelist": "\u653e\u884c example.org \u53ca\u5176\u6240\u6709\u5b50\u57df\u540d",
"example_meaning_host_block": "AdGuard Home \u73b0\u5728\u5c06\u4f1a\u628a example.org\uff08\u4f46\u4e0d\u5305\u62ec\u5b83\u7684\u5b50\u57df\u540d\uff09\u89e3\u6790\u5230 127.0.0.1\u3002",
"example_comment": "! \u8fd9\u662f\u4e00\u884c\u6ce8\u91ca",
"example_comment_meaning": "\u53ea\u662f\u4e00\u6761\u6ce8\u91ca",
"example_comment_hash": "# \u8fd9\u4e5f\u662f\u4e00\u884c\u6ce8\u91ca",
"example_regex_meaning": "\u963b\u6b62\u8bbf\u95ee\u4e0e\u6307\u5b9a\u7684\u6b63\u5219\u8868\u8fbe\u5f0f\u5339\u914d\u7684\u57df\u540d",
"example_upstream_regular": "\u5e38\u89c4 DNS\uff08\u57fa\u4e8e UDP\uff09",
"example_upstream_dot": "\u52a0\u5bc6 <0>DNS-over-TLS<\/0>",
"example_upstream_doh": "\u52a0\u5bc6 <0>DNS-over-HTTPS<\/0>",
"example_upstream_sdns": "\u4f60\u53ef\u4ee5\u4f7f\u7528 <0>DNSCrypt<\/0> \u7684 <1>DNS Stamps<\/1> \u6216\u8005 <2>DNS-over-HTTPS<\/2> \u89e3\u6790\u5668",
"example_upstream_tcp": "\u5e38\u89c4 DNS\uff08\u57fa\u4e8e TCP \uff09",
"all_filters_up_to_date_toast": "\u6240\u6709\u8fc7\u6ee4\u5668\u5df2\u66f4\u65b0\u81f3\u6700\u65b0",
"updated_upstream_dns_toast": "\u4e0a\u6e38 DNS \u5df2\u66f4\u65b0",
"dns_test_ok_toast": "\u6307\u5b9a\u7684 DNS \u670d\u52a1\u5668\u73b0\u5df2\u6b63\u5e38\u8fd0\u884c",
"dns_test_not_ok_toast": "\u670d\u52a1\u5668 \"{{key}}\"\uff1a\u65e0\u6cd5\u4f7f\u7528\uff0c\u8bf7\u68c0\u67e5\u4f60\u8f93\u5165\u7684\u662f\u5426\u6b63\u786e",
"unblock_btn": "\u653e\u884c",
"block_btn": "\u62e6\u622a",
"time_table_header": "\u65f6\u95f4",
"domain_name_table_header": "\u57df\u540d",
"type_table_header": "\u7c7b\u578b",
"response_table_header": "\u54cd\u5e94",
"client_table_header": "\u5ba2\u6237\u7aef",
"empty_response_status": "\u7a7a",
"show_all_filter_type": "\u663e\u793a\u6240\u6709",
"show_filtered_type": "\u663e\u793a\u88ab\u62e6\u622a\u7684",
"no_logs_found": "\u672a\u627e\u5230\u65e5\u5fd7",
"disabled_log_btn": "\u7981\u7528\u65e5\u5fd7",
"download_log_file_btn": "\u4e0b\u8f7d\u65e5\u5fd7\u6587\u4ef6",
"refresh_btn": "\u5237\u65b0",
"enabled_log_btn": "\u542f\u7528\u65e5\u5fd7",
"last_dns_queries": "\u6700\u8fd1\u7684 5000 \u4e2a DNS \u8bf7\u6c42",
"previous_btn": "\u4e0a\u4e00\u9875",
"next_btn": "\u4e0b\u4e00\u9875",
"loading_table_status": "\u52a0\u8f7d\u4e2d\u2026\u2026",
"page_table_footer_text": "\u9875",
"of_table_footer_text": "\u5728",
"rows_table_footer_text": "\u884c",
"updated_custom_filtering_toast": "\u81ea\u5b9a\u4e49\u8fc7\u6ee4\u89c4\u5219\u5df2\u66f4\u65b0",
"rule_removed_from_custom_filtering_toast": "\u89c4\u5219\u5df2\u4ece\u81ea\u5b9a\u4e49\u8fc7\u6ee4\u89c4\u5219\u5217\u8868\u4e2d\u79fb\u9664",
"rule_added_to_custom_filtering_toast": "\u89c4\u5219\u5df2\u6dfb\u52a0\u5230\u81ea\u5b9a\u4e49\u8fc7\u6ee4\u89c4\u5219\u5217\u8868\u4e2d",
"query_log_disabled_toast": "\u67e5\u8be2\u65e5\u5fd7\u5df2\u7981\u7528",
"query_log_enabled_toast": "\u67e5\u8be2\u65e5\u5fd7\u5df2\u542f\u7528",
"source_label": "\u6e90",
"found_in_known_domain_db": "\u6210\u529f\u5728\u5df2\u77e5\u57df\u540d\u6570\u636e\u5e93\u4e2d\u67e5\u8be2\u5230",
"category_label": "\u7c7b\u522b",
"rule_label": "\u89c4\u5219",
"filter_label": "\u8fc7\u6ee4\u5668",
"unknown_filter": "\u672a\u77e5\u8fc7\u6ee4\u5668 {{filterId}}",
"install_welcome_title": "\u6b22\u8fce\u4f7f\u7528 AdGuard Home\uff01",
"install_welcome_desc": "AdGuard Home \u662f\u4e00\u4e2a\u53ef\u5728\u7279\u5b9a\u7f51\u7edc\u8303\u56f4\u5185\u62e6\u622a\u6240\u6709\u5e7f\u544a\u548c\u8ddf\u8e2a\u5668\u7684 DNS \u670d\u52a1\u5668\u3002\u5b83\u7684\u76ee\u7684\u662f\u8ba9\u60a8\u63a7\u5236\u6574\u4e2a\u7f51\u7edc\u548c\u60a8\u7684\u6240\u6709\u8bbe\u5907\uff0c\u4e14\u4e0d\u9700\u8981\u4f7f\u7528\u4efb\u4f55\u5ba2\u6237\u7aef\u7a0b\u5e8f\u3002",
"install_settings_title": "\u7f51\u9875\u7ba1\u7406\u754c\u9762",
"install_settings_listen": "\u76d1\u542c\u63a5\u53e3",
"install_settings_port": "\u7aef\u53e3",
"install_settings_interface_link": "\u60a8\u53ef\u4ee5\u901a\u8fc7\u4ee5\u4e0b\u5730\u5740\u8bbf\u95ee\u60a8\u7684 AdGuard Home \u7f51\u9875\u7ba1\u7406\u754c\u9762\uff1a",
"form_error_port": "\u8f93\u5165\u6709\u6548\u7684\u7aef\u53e3\u503c",
"install_settings_dns": "DNS \u670d\u52a1\u5668",
"install_settings_dns_desc": "\u60a8\u5c06\u9700\u8981\u4f7f\u7528\u4ee5\u4e0b\u5730\u5740\u6765\u8bbe\u7f6e\u60a8\u7684\u8bbe\u5907\u6216\u8def\u7531\u5668\u7684 DNS \u670d\u52a1\u5668\uff1a",
"install_settings_all_interfaces": "\u6240\u6709\u63a5\u53e3",
"install_auth_title": "\u8eab\u4efd\u8ba4\u8bc1",
"install_auth_desc": "\u6211\u4eec\u5f3a\u70c8\u5efa\u8bae\u60a8\u4e3a AdGuard Home \u7684\u7f51\u9875\u7ba1\u7406\u754c\u9762\u914d\u7f6e\u5bc6\u7801\u3002\u5c3d\u7ba1\u8be5\u9875\u9762\u53ea\u80fd\u901a\u8fc7\u60a8\u7684\u672c\u5730\u7f51\u7edc\u8bbf\u95ee\uff0c\u4f46\u907f\u514d\u5b83\u88ab\u4e0d\u52a0\u9650\u5236\u5730\u8bbf\u95ee\u4ecd\u5341\u5206\u91cd\u8981\u3002",
"install_auth_username": "\u7528\u6237\u540d",
"install_auth_password": "\u5bc6\u7801",
"install_auth_confirm": "\u786e\u8ba4\u5bc6\u7801",
"install_auth_username_enter": "\u8f93\u5165\u7528\u6237\u540d",
"install_auth_password_enter": "\u8f93\u5165\u5bc6\u7801",
"install_step": "\u6b65\u9aa4",
"install_devices_title": "\u914d\u7f6e\u60a8\u7684\u8bbe\u5907",
"install_devices_desc": "\u4e3a\u4fdd\u8bc1 AdGuard Home \u53ef\u4ee5\u5f00\u59cb\u6b63\u5e38\u5de5\u4f5c\uff0c\u60a8\u9700\u8981\u5728\u8bbe\u5907\u4e0a\u5bf9\u5176\u8fdb\u884c\u914d\u7f6e\u3002",
"install_submit_title": "\u606d\u559c\u60a8\uff01",
"install_submit_desc": "\u5b89\u88c5\u8fc7\u7a0b\u5df2\u7ecf\u5b8c\u6210\uff0c\u60a8\u53ef\u4ee5\u5f00\u59cb\u4f7f\u7528 AdGuard Home \u4e86\u3002",
"install_devices_router": "\u8def\u7531\u5668",
"install_devices_router_desc": "\u6b64\u8bbe\u7f6e\u5c06\u81ea\u52a8\u8986\u76d6\u8fde\u63a5\u5230\u60a8\u7684\u5bb6\u5ead\u8def\u7531\u5668\u7684\u6240\u6709\u8bbe\u5907\uff0c\u60a8\u4e0d\u9700\u8981\u624b\u52a8\u914d\u7f6e\u5b83\u4eec\u3002",
"install_devices_address": "AdGuard Home DNS \u670d\u52a1\u5668\u6b63\u5728\u76d1\u542c\u4ee5\u4e0b\u5730\u5740",
"install_devices_router_list_1": "\u6253\u5f00\u60a8\u7684\u8def\u7531\u5668\u914d\u7f6e\u754c\u9762\u3002\u901a\u5e38\u60c5\u51b5\u4e0b\uff0c\u60a8\u53ef\u4ee5\u901a\u8fc7\u6d4f\u89c8\u5668\u8bbf\u95ee\u5730\u5740\uff08\u5982 http:\/\/192.168.0.1\/ \u6216 http:\/\/192.168.1.1 \uff09\u3002\u6253\u5f00\u540e\u60a8\u53ef\u80fd\u9700\u8981\u8f93\u5165\u5bc6\u7801\u4ee5\u8fdb\u5165\u914d\u7f6e\u754c\u9762\u3002\u5982\u679c\u60a8\u4e0d\u8bb0\u5f97\u5bc6\u7801\uff0c\u901a\u5e38\u53ef\u4ee5\u901a\u8fc7\u6309\u4e0b\u8def\u7531\u5668\u4e0a\u7684\u91cd\u7f6e\u6309\u94ae\u6765\u91cd\u8bbe\u5bc6\u7801\u3002\u4e00\u4e9b\u8def\u7531\u5668\u53ef\u80fd\u9700\u8981\u901a\u8fc7\u7279\u5b9a\u7684\u5e94\u7528\u6765\u8fdb\u884c\u8fd9\u4e00\u64cd\u4f5c\uff0c\u8bf7\u786e\u4fdd\u60a8\u5df2\u7ecf\u5728\u8ba1\u7b97\u673a\u6216\u624b\u673a\u4e0a\u5b89\u88c5\u4e86\u76f8\u5173\u5e94\u7528\u3002",
"install_devices_router_list_2": "\u627e\u5230\u8def\u7531\u5668\u7684 DHCP\/DNS \u8bbe\u7f6e\u9875\u9762\u3002\u60a8\u4f1a\u5728 DNS \u8fd9\u4e00\u5355\u8bcd\u65c1\u8fb9\u627e\u5230\u4e24\u5230\u4e09\u884c\u5141\u8bb8\u8f93\u5165\u7684\u8f93\u5165\u6846\uff0c\u6bcf\u4e00\u884c\u8f93\u5165\u6846\u5206\u4e3a\u56db\u7ec4\uff0c\u6bcf\u7ec4\u5141\u8bb8\u8f93\u5165\u4e00\u5230\u4e09\u4e2a\u6570\u5b57\u3002",
"install_devices_router_list_3": "\u8bf7\u5728\u6b64\u5904\u8f93\u5165\u60a8\u7684 AdGuard Home \u670d\u52a1\u5668\u5730\u5740\u3002",
"install_devices_windows_list_1": "\u901a\u8fc7\u5f00\u59cb\u83dc\u5355\u6216 Windows \u641c\u7d22\u529f\u80fd\u6253\u5f00\u63a7\u5236\u9762\u677f\u3002",
"install_devices_windows_list_2": "\u70b9\u51fb\u8fdb\u5165 \u201d\u7f51\u7edc\u548c Internet\u201c \u540e\uff0c\u518d\u6b21\u70b9\u51fb\u8fdb\u5165 \u201c\u7f51\u7edc\u548c\u5171\u4eab\u4e2d\u5fc3\u201d",
"install_devices_windows_list_3": "\u5728\u7a97\u53e3\u7684\u5de6\u4fa7\u627e\u5230 \u201d\u66f4\u6539\u9002\u914d\u5668\u8bbe\u7f6e\u201c \u5e76\u70b9\u51fb\u8fdb\u5165\u3002",
"install_devices_windows_list_4": "\u9009\u62e9\u60a8\u6b63\u5728\u8fde\u63a5\u7684\u7f51\u7edc\u8bbe\u5907\uff0c\u53f3\u51fb\u5b83\u5e76\u9009\u62e9 \u201d\u5c5e\u6027\u201c \u3002",
"install_devices_windows_list_5": "\u5728\u5217\u8868\u4e2d\u627e\u5230 \u201dInternet \u534f\u8bae\u7248\u672c 4 (TCP\/IPv4)\u201c \uff0c\u9009\u62e9\u5e76\u518d\u6b21\u70b9\u51fb \u201d\u5c5e\u6027\u201c \u3002",
"install_devices_windows_list_6": "\u9009\u62e9 \u201d\u4f7f\u7528\u4e0b\u9762\u7684 DNS \u670d\u52a1\u5668\u5730\u5740\u201c \uff0c\u5e76\u8f93\u5165\u60a8\u7684 AdGuard Home \u670d\u52a1\u5668\u5730\u5740\u3002",
"install_devices_macos_list_1": "\u70b9\u51fb\u82f9\u679c\u56fe\u6807\uff0c\u8fdb\u5165 \u201d\u7cfb\u7edf\u9996\u9009\u9879\u201c\u3002",
"install_devices_macos_list_2": "\u70b9\u51fb \u201d\u7f51\u7edc\u201c \u3002",
"install_devices_macos_list_3": "\u9009\u62e9\u5728\u5217\u8868\u4e2d\u7684\u7b2c\u4e00\u4e2a\u8fde\u63a5\uff0c\u5e76\u70b9\u51fb \u201d\u9ad8\u7ea7\u201c \u3002",
"install_devices_macos_list_4": "\u9009\u62e9 \u201dDNS\u201c \u9009\u9879\u5361\uff0c\u5e76\u8f93\u5165\u60a8\u7684 AdGuard Home \u670d\u52a1\u5668\u5730\u5740\u3002",
"install_devices_android_list_1": "\u5728\u5b89\u5353\u4e3b\u5c4f\u5e55\u83dc\u5355\u4e2d\u70b9\u51fb\u8bbe\u7f6e\u3002",
"install_devices_android_list_2": "\u70b9\u51fb\u83dc\u5355\u4e0a\u7684 \u201d\u65e0\u7ebf\u5c40\u57df\u7f51\u201c \u9009\u9879\u3002\u5728\u5c4f\u5e55\u4e0a\u5c06\u5217\u51fa\u6240\u6709\u53ef\u7528\u7684\u7f51\u7edc\uff08\u8702\u7a9d\u79fb\u52a8\u7f51\u7edc\u4e0d\u652f\u6301\u4fee\u6539 DNS \uff09\u3002",
"install_devices_android_list_3": "\u957f\u6309\u5f53\u524d\u5df2\u8fde\u63a5\u7684\u7f51\u7edc\uff0c\u7136\u540e\u70b9\u51fb \u201d\u4fee\u6539\u7f51\u7edc\u8bbe\u7f6e\u201c \u3002",
"install_devices_android_list_4": "\u5728\u67d0\u4e9b\u8bbe\u5907\u4e0a\uff0c\u60a8\u53ef\u80fd\u9700\u8981\u9009\u4e2d \u201d\u9ad8\u7ea7\u201c \u590d\u9009\u6846\u4ee5\u67e5\u770b\u8fdb\u4e00\u6b65\u7684\u8bbe\u7f6e\u3002\u60a8\u53ef\u80fd\u9700\u8981\u8c03\u6574\u60a8\u5b89\u5353\u8bbe\u5907\u7684 DNS \u8bbe\u7f6e\uff0c\u6216\u662f\u9700\u8981\u5c06 IP \u8bbe\u7f6e\u4ece DHCP \u5207\u6362\u5230\u9759\u6001\u3002",
"install_devices_android_list_5": "\u5c06 \"DNS 1 \/ \u4e3b DNS\" \u548c \u201dDNS 2 \/ \u526f DNS\u201c \u7684\u503c\u6539\u4e3a\u60a8\u7684 AdGuard Home \u670d\u52a1\u5668\u5730\u5740\u3002",
"install_devices_ios_list_1": "\u4ece\u4e3b\u5c4f\u5e55\u4e2d\u70b9\u51fb \u201d\u8bbe\u7f6e\u201c \u3002",
"install_devices_ios_list_2": "\u4ece\u5de6\u4fa7\u76ee\u5f55\u4e2d\u9009\u62e9 \u201d\u65e0\u7ebf\u5c40\u57df\u7f51\u201c\uff08\u79fb\u52a8\u6570\u636e\u7f51\u7edc\u73af\u5883\u4e0b\u4e0d\u652f\u6301\u4fee\u6539 DNS \uff09\u3002",
"install_devices_ios_list_3": "\u70b9\u51fb\u5f53\u524d\u5df2\u8fde\u63a5\u7f51\u7edc\u7684\u540d\u79f0\u3002",
"install_devices_ios_list_4": "\u5728 DNS \u5b57\u6bb5\u4e2d\u8f93\u5165\u60a8\u7684 AdGuard Home \u670d\u52a1\u5668\u5730\u5740\u3002",
"get_started": "\u5f00\u59cb\u914d\u7f6e",
"next": "\u4e0b\u4e00\u6b65",
"open_dashboard": "\u6253\u5f00\u4eea\u8868\u76d8",
"install_saved": "\u4fdd\u5b58\u6210\u529f",
"encryption_title": "\u52a0\u5bc6",
"encryption_desc": "\u4e3a DNS \u4e0e\u7f51\u9875\u7ba1\u7406\u754c\u9762\u542f\u7528\u52a0\u5bc6\uff08HTTPS\/TLS\uff09",
"encryption_config_saved": "\u52a0\u5bc6\u914d\u7f6e\u5df2\u4fdd\u5b58",
"encryption_server": "\u670d\u52a1\u5668\u540d\u79f0",
"encryption_server_enter": "\u8f93\u5165\u60a8\u7684\u57df\u540d",
"encryption_server_desc": "\u82e5\u8981\u4f7f\u7528 HTTPS \uff0c\u60a8\u9700\u8981\u8f93\u5165\u4e0e SSL \u8bc1\u4e66\u76f8\u5339\u914d\u7684\u670d\u52a1\u5668\u540d\u79f0\u3002",
"encryption_redirect": "HTTPS \u81ea\u52a8\u91cd\u5b9a\u5411",
"encryption_redirect_desc": "\u5982\u679c\u52fe\u9009\u6b64\u9009\u9879\uff0cAdGuard Home \u5c06\u81ea\u52a8\u5c06\u60a8\u4ece HTTP \u91cd\u5b9a\u5411\u5230 HTTPS \u5730\u5740\u3002",
"encryption_https": "HTTPS \u7aef\u53e3",
"encryption_https_desc": "\u5982\u679c\u914d\u7f6e\u4e86 HTTPS \u7aef\u53e3\uff0cAdGuard Home \u7ba1\u7406\u754c\u9762\u5c06\u53ef\u4ee5\u901a\u8fc7 HTTPS \u8bbf\u95ee\uff0c\u5b83\u8fd8\u5c06\u5728\u5728 '\/dns-query' \u4f4d\u7f6e\u63d0\u4f9b DNS-over-HTTPS \u3002",
"encryption_dot": "DNS-over-TLS \u7aef\u53e3",
"encryption_dot_desc": "\u5982\u679c\u914d\u7f6e\u4e86\u6b64\u7aef\u53e3\uff0cAdGuard Home \u5c06\u5728\u6b64\u7aef\u53e3\u4e0a\u8fd0\u884c\u4e00\u4e2a DNS-over-TLS \u670d\u52a1\u5668\u3002",
"encryption_certificates": "\u8bc1\u4e66",
"encryption_certificates_desc": "\u4e3a\u4e86\u4f7f\u7528\u52a0\u5bc6\uff0c\u60a8\u9700\u8981\u4e3a\u57df\u63d0\u4f9b\u6709\u6548\u7684 SSL \u8bc1\u4e66\u94fe\u3002\u60a8\u53ef\u4ee5\u5728 <0>{{link}}<\/0> \u4e0a\u83b7\u5f97\u514d\u8d39\u8bc1\u4e66\uff0c\u4e5f\u53ef\u4ee5\u4ece\u53d7\u4fe1\u4efb\u7684\u8bc1\u4e66\u9881\u53d1\u673a\u6784\u8d2d\u4e70\u8bc1\u4e66\u3002",
"encryption_certificates_input": "\u5c06\u60a8\u4ee5 PEM \u683c\u5f0f\u7f16\u7801\u7684\u8bc1\u4e66\u590d\u5236\u7c98\u8d34\u5230\u6b64\u5904\u3002",
"encryption_status": "\u72b6\u6001",
"encryption_expire": "\u6709\u6548\u671f",
"encryption_key": "\u79c1\u94a5",
"encryption_key_input": "\u5c06\u60a8\u4ee5 PEM \u683c\u5f0f\u7f16\u7801\u7684\u8bc1\u4e66\u79c1\u94a5\u590d\u5236\u7c98\u8d34\u5230\u6b64\u5904\u3002",
"encryption_enable": "\u542f\u7528\u52a0\u5bc6\uff08HTTPS\u3001DNS-over-HTTPS\u3001DNS-over-TLS\uff09",
"encryption_enable_desc": "\u5982\u679c\u542f\u7528\u52a0\u5bc6\u9009\u9879\uff0cAdGuard Home \u7684\u7f51\u9875\u7ba1\u7406\u754c\u9762\u5c06\u901a\u8fc7 HTTPS \u8fde\u63a5\u8bbf\u95ee\uff0c\u540c\u65f6 DNS \u670d\u52a1\u5668\u5c06\u76d1\u542c\u901a\u8fc7 DNS-over-HTTPS \u4e0e DNS-over-TLS \u53d1\u9001\u7684\u8bf7\u6c42\u3002",
"encryption_chain_valid": "\u8bc1\u4e66\u94fe\u9a8c\u8bc1\u6709\u6548",
"encryption_chain_invalid": "\u8bc1\u4e66\u94fe\u9a8c\u8bc1\u65e0\u6548",
"encryption_key_valid": "\u8be5 {{type}} \u79c1\u94a5\u9a8c\u8bc1\u6709\u6548",
"encryption_key_invalid": "\u8be5 {{type}} \u79c1\u94a5\u9a8c\u8bc1\u65e0\u6548",
"encryption_subject": "\u4f7f\u7528\u8005",
"encryption_issuer": "\u9881\u53d1\u8005",
"encryption_hostnames": "\u4e3b\u673a\u540d",
"encryption_reset": "\u60a8\u786e\u5b9a\u60f3\u8981\u91cd\u7f6e\u52a0\u5bc6\u8bbe\u7f6e\uff1f",
"topline_expiring_certificate": "\u60a8\u7684 SSL \u8bc1\u4e66\u5373\u5c06\u8fc7\u671f\u3002\u8bf7\u66f4\u65b0 <0>\u52a0\u5bc6\u8bbe\u7f6e<\/0> \u3002",
"topline_expired_certificate": "\u60a8\u7684 SSL \u8bc1\u4e66\u5df2\u8fc7\u671f\u3002\u8bf7\u66f4\u65b0 <0>\u52a0\u5bc6\u8bbe\u7f6e<\/0> \u3002",
"form_error_port_range": "\u8f93\u5165 80 - 65535 \u8303\u56f4\u5185\u7684\u7aef\u53e3\u503c",
"form_error_port_unsafe": "\u8fd9\u662f\u4e00\u4e2a\u4e0d\u5b89\u5168\u7684\u7aef\u53e3",
"form_error_equal": "\u4e0d\u5e94\u8be5\u76f8\u540c",
"form_error_password": "\u5bc6\u7801\u4e0d\u5339\u914d",
"reset_settings": "\u91cd\u7f6e\u8bbe\u7f6e",
"update_announcement": "AdGuard Home {{version}} \u73b0\u5df2\u53d1\u5e03\uff01 <0>\u70b9\u51fb\u6b64\u5904<\/0> \u4ee5\u83b7\u53d6\u8be6\u7ec6\u4fe1\u606f\u3002"
}

View File

@ -1,4 +1,7 @@
{ {
"upstream_parallel": "\u900f\u904e\u540c\u6642\u5730\u67e5\u8a62\u6240\u6709\u4e0a\u6e38\u7684\u4f3a\u670d\u5668\uff0c\u4f7f\u7528\u4e26\u884c\u7684\u67e5\u8a62\u4ee5\u52a0\u901f\u89e3\u6790",
"bootstrap_dns": "\u81ea\u6211\u555f\u52d5\uff08Bootstrap\uff09DNS \u4f3a\u670d\u5668",
"bootstrap_dns_desc": "\u81ea\u6211\u555f\u52d5\uff08Bootstrap\uff09DNS\u4f3a\u670d\u5668\u88ab\u7528\u65bc\u89e3\u6790\u60a8\u660e\u78ba\u6307\u5b9a\u4f5c\u70ba\u4e0a\u6e38\u7684DoH\/DoT\u89e3\u6790\u5668\u4e4bIP\u4f4d\u5740\u3002",
"url_added_successfully": "\u7db2\u5740\u88ab\u6210\u529f\u5730\u52a0\u5165", "url_added_successfully": "\u7db2\u5740\u88ab\u6210\u529f\u5730\u52a0\u5165",
"check_dhcp_servers": "\u6aa2\u67e5\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668", "check_dhcp_servers": "\u6aa2\u67e5\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668",
"save_config": "\u5132\u5b58\u914d\u7f6e", "save_config": "\u5132\u5b58\u914d\u7f6e",
@ -79,7 +82,7 @@
"no_settings": "\u7121\u8a2d\u5b9a", "no_settings": "\u7121\u8a2d\u5b9a",
"general_settings": "\u4e00\u822c\u7684\u8a2d\u5b9a", "general_settings": "\u4e00\u822c\u7684\u8a2d\u5b9a",
"upstream_dns": "\u4e0a\u6e38\u7684DNS\u4f3a\u670d\u5668", "upstream_dns": "\u4e0a\u6e38\u7684DNS\u4f3a\u670d\u5668",
"upstream_dns_hint": "\u5982\u679c\u60a8\u4fdd\u7559\u8a72\u6b04\u4f4d\u7a7a\u767d\u7684\uff0cAdGuard Home\u5c07\u4f7f\u7528<a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a>\u4f5c\u70ba\u4e0a\u6e38\u3002\u5c0d\u65bcDNS over TLS\u4f3a\u670d\u5668\u4f7f\u7528 tls:\/\/ \u524d\u7db4\u3002", "upstream_dns_hint": "\u5982\u679c\u60a8\u5c07\u8a72\u6b04\u4f4d\u7559\u7a7a\uff0cAdGuard Home\u5c07\u4f7f\u7528<a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a>\u4f5c\u70ba\u4e0a\u6e38\u3002",
"test_upstream_btn": "\u6e2c\u8a66\u4e0a\u884c\u8cc7\u6599\u6d41", "test_upstream_btn": "\u6e2c\u8a66\u4e0a\u884c\u8cc7\u6599\u6d41",
"apply_btn": "\u5957\u7528", "apply_btn": "\u5957\u7528",
"disabled_filtering_toast": "\u5df2\u7981\u7528\u904e\u6ffe", "disabled_filtering_toast": "\u5df2\u7981\u7528\u904e\u6ffe",
@ -116,11 +119,11 @@
"example_comment": "! \u770b\uff0c\u4e00\u500b\u8a3b\u89e3", "example_comment": "! \u770b\uff0c\u4e00\u500b\u8a3b\u89e3",
"example_comment_meaning": "\u53ea\u662f\u4e00\u500b\u8a3b\u89e3", "example_comment_meaning": "\u53ea\u662f\u4e00\u500b\u8a3b\u89e3",
"example_comment_hash": "# \u4e5f\u662f\u4e00\u500b\u8a3b\u89e3", "example_comment_hash": "# \u4e5f\u662f\u4e00\u500b\u8a3b\u89e3",
"example_regex_meaning": "\u5c01\u9396\u81f3\u8207\u5df2\u660e\u78ba\u6307\u5b9a\u7684\u898f\u5247\u904b\u7b97\u5f0f\uff08Regular Expression\uff09\u76f8\u914d\u7684\u7db2\u57df\u4e4b\u5b58\u53d6", "example_regex_meaning": "\u5c01\u9396\u81f3\u8207\u5df2\u660e\u78ba\u6307\u5b9a\u7684\u898f\u5247\u904b\u7b97\u5f0f\uff08Regular Expression\uff09\u76f8\u7b26\u7684\u7db2\u57df\u4e4b\u5b58\u53d6",
"example_upstream_regular": "\u4e00\u822c\u7684 DNS\uff08\u900f\u904eUDP\uff09", "example_upstream_regular": "\u4e00\u822c\u7684 DNS\uff08\u900f\u904eUDP\uff09",
"example_upstream_dot": "\u52a0\u5bc6\u7684 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-over-TLS<\/a>", "example_upstream_dot": "\u52a0\u5bc6\u7684 <0>DNS-over-TLS<\/0>",
"example_upstream_doh": "\u52a0\u5bc6\u7684 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS <\/a>", "example_upstream_doh": "\u52a0\u5bc6\u7684 <0>DNS-over-HTTPS <\/0>",
"example_upstream_sdns": "\u60a8\u53ef\u4f7f\u7528\u95dc\u65bc <a href='https:\/\/dnscrypt.info\/' target='_blank'>DNSCrypt<\/a> \u6216 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a> \u89e3\u6790\u5668\u4e4b <a href='https:\/\/dnscrypt.info\/stamps\/' target='_blank'>DNS \u6233\u8a18<\/a>", "example_upstream_sdns": "\u60a8\u53ef\u4f7f\u7528\u95dc\u65bc <0>DNSCrypt<\/0> \u6216 <1>DNS-over-HTTPS<\/1> \u89e3\u6790\u5668\u4e4b <2>DNS \u6233\u8a18<\/2>",
"example_upstream_tcp": "\u4e00\u822c\u7684 DNS\uff08\u900f\u904eTCP\uff09", "example_upstream_tcp": "\u4e00\u822c\u7684 DNS\uff08\u900f\u904eTCP\uff09",
"all_filters_up_to_date_toast": "\u6240\u6709\u7684\u904e\u6ffe\u5668\u5df2\u662f\u6700\u65b0\u7684", "all_filters_up_to_date_toast": "\u6240\u6709\u7684\u904e\u6ffe\u5668\u5df2\u662f\u6700\u65b0\u7684",
"updated_upstream_dns_toast": "\u5df2\u66f4\u65b0\u4e0a\u6e38\u7684DNS\u4f3a\u670d\u5668", "updated_upstream_dns_toast": "\u5df2\u66f4\u65b0\u4e0a\u6e38\u7684DNS\u4f3a\u670d\u5668",
@ -210,5 +213,43 @@
"next": "\u4e0b\u4e00\u6b65", "next": "\u4e0b\u4e00\u6b65",
"open_dashboard": "\u958b\u555f\u5100\u8868\u677f", "open_dashboard": "\u958b\u555f\u5100\u8868\u677f",
"install_saved": "\u5df2\u6210\u529f\u5730\u5132\u5b58", "install_saved": "\u5df2\u6210\u529f\u5730\u5132\u5b58",
"form_error_password": "\u4e0d\u76f8\u7b26\u7684\u5bc6\u78bc" "encryption_title": "\u52a0\u5bc6",
"encryption_desc": "\u52a0\u5bc6\uff08HTTPS\/TLS\uff09\u652f\u63f4\u4f9bDNS\u548c\u7ba1\u7406\u54e1\u7db2\u9801\u4ecb\u9762\u5169\u8005",
"encryption_config_saved": "\u52a0\u5bc6\u914d\u7f6e\u5df2\u88ab\u5132\u5b58",
"encryption_server": "\u4f3a\u670d\u5668\u540d\u7a31",
"encryption_server_enter": "\u8f38\u5165\u60a8\u7684\u57df\u540d",
"encryption_server_desc": "\u70ba\u4e86\u4f7f\u7528HTTPS\uff0c\u60a8\u9700\u8981\u8f38\u5165\u8207\u60a8\u7684\u5b89\u5168\u901a\u8a0a\u7aef\u5c64\uff08SSL\uff09\u6191\u8b49\u76f8\u7b26\u7684\u4f3a\u670d\u5668\u540d\u7a31\u3002",
"encryption_redirect": "\u81ea\u52d5\u5730\u91cd\u5b9a\u5411\u5230HTTPS",
"encryption_redirect_desc": "\u5982\u679c\u88ab\u52fe\u9078\uff0cAdGuard Home\u5c07\u81ea\u52d5\u5730\u91cd\u5b9a\u5411\u60a8\u5f9eHTTP\u5230HTTPS\u4f4d\u5740\u3002",
"encryption_https": "HTTPS \u9023\u63a5\u57e0",
"encryption_https_desc": "\u5982\u679cHTTPS\u9023\u63a5\u57e0\u88ab\u914d\u7f6e\uff0cAdGuard Home\u7ba1\u7406\u54e1\u4ecb\u9762\u900f\u904eHTTPS\u5c07\u70ba\u53ef\u5b58\u53d6\u7684\uff0c\u4e14\u5b83\u4e5f\u5c07\u65bc '\/dns-query' \u4f4d\u7f6e\u4e0a\u63d0\u4f9bDNS-over-HTTPS\u3002",
"encryption_dot": "DNS-over-TLS \u9023\u63a5\u57e0",
"encryption_dot_desc": "\u5982\u679c\u8a72\u9023\u63a5\u57e0\u88ab\u914d\u7f6e\uff0cAdGuard Home\u5c07\u65bc\u6b64\u9023\u63a5\u57e0\u4e0a\u904b\u884cDNS-over-TLS\u4f3a\u670d\u5668\u3002",
"encryption_certificates": "\u6191\u8b49",
"encryption_certificates_desc": "\u70ba\u4e86\u4f7f\u7528\u52a0\u5bc6\uff0c\u60a8\u9700\u8981\u63d0\u4f9b\u6709\u6548\u7684\u5b89\u5168\u901a\u8a0a\u7aef\u5c64\uff08SSL\uff09\u6191\u8b49\u93c8\u7d50\u4f9b\u60a8\u7684\u7db2\u57df\u3002\u65bc <0>{{link}}<\/0> \u4e0a\u60a8\u53ef\u53d6\u5f97\u514d\u8cbb\u7684\u6191\u8b49\u6216\u60a8\u53ef\u5f9e\u53d7\u4fe1\u4efb\u7684\u6191\u8b49\u6388\u6b0a\u55ae\u4f4d\u4e4b\u4e00\u8cfc\u8cb7\u5b83\u3002",
"encryption_certificates_input": "\u65bc\u6b64\u8907\u88fd\/\u8cbc\u4e0a\u60a8\u7684\u96b1\u79c1\u589e\u5f37\u90f5\u4ef6\u7de8\u78bc\u4e4b\uff08PEM-encoded\uff09\u6191\u8b49\u3002",
"encryption_status": "\u72c0\u614b",
"encryption_expire": "\u5230\u671f",
"encryption_key": "\u79c1\u5bc6\u91d1\u9470",
"encryption_key_input": "\u65bc\u6b64\u8907\u88fd\/\u8cbc\u4e0a\u60a8\u7684\u96b1\u79c1\u589e\u5f37\u90f5\u4ef6\u7de8\u78bc\u4e4b\uff08PEM-encoded\uff09\u79c1\u5bc6\u91d1\u9470\u4f9b\u60a8\u7684\u6191\u8b49\u3002",
"encryption_enable": "\u555f\u7528\u52a0\u5bc6\uff08HTTPS\u3001DNS-over-HTTPS\u548cDNS-over-TLS\uff09",
"encryption_enable_desc": "\u5982\u679c\u52a0\u5bc6\u88ab\u555f\u7528\uff0cAdGuard Home\u7ba1\u7406\u54e1\u4ecb\u9762\u900f\u904eHTTPS\u5c07\u904b\u4f5c\uff0c\u4e14\u8a72DNS\u4f3a\u670d\u5668\u5c07\u7559\u5fc3\u76e3\u807d\u900f\u904eDNS-over-HTTPS\u548cDNS-over-TLS\u4e4b\u8acb\u6c42\u3002",
"encryption_chain_valid": "\u6191\u8b49\u93c8\u7d50\u70ba\u6709\u6548\u7684",
"encryption_chain_invalid": "\u6191\u8b49\u93c8\u7d50\u70ba\u7121\u6548\u7684",
"encryption_key_valid": "\u6b64\u70ba\u6709\u6548\u7684 {{type}} \u79c1\u5bc6\u91d1\u9470",
"encryption_key_invalid": "\u6b64\u70ba\u7121\u6548\u7684 {{type}} \u79c1\u5bc6\u91d1\u9470",
"encryption_subject": "\u7269\u4ef6",
"encryption_issuer": "\u7c3d\u767c\u8005",
"encryption_hostnames": "\u4e3b\u6a5f\u540d\u7a31",
"encryption_reset": "\u60a8\u78ba\u5b9a\u60a8\u60f3\u8981\u91cd\u7f6e\u52a0\u5bc6\u8a2d\u5b9a\u55ce\uff1f",
"topline_expiring_certificate": "\u60a8\u7684\u5b89\u5168\u901a\u8a0a\u7aef\u5c64\uff08SSL\uff09\u6191\u8b49\u5373\u5c07\u5230\u671f\u3002\u66f4\u65b0<0>\u52a0\u5bc6\u8a2d\u5b9a<\/0>\u3002",
"topline_expired_certificate": "\u60a8\u7684\u5b89\u5168\u901a\u8a0a\u7aef\u5c64\uff08SSL\uff09\u6191\u8b49\u70ba\u5df2\u5230\u671f\u7684\u3002\u66f4\u65b0<0>\u52a0\u5bc6\u8a2d\u5b9a<\/0>\u3002",
"form_error_port_range": "\u572880-65535\u4e4b\u7bc4\u570d\u5167\u8f38\u5165\u9023\u63a5\u57e0\u503c",
"form_error_port_unsafe": "\u6b64\u70ba\u4e0d\u5b89\u5168\u7684\u9023\u63a5\u57e0",
"form_error_equal": "\u4e0d\u61c9\u70ba\u76f8\u7b49\u7684",
"form_error_password": "\u4e0d\u76f8\u7b26\u7684\u5bc6\u78bc",
"reset_settings": "\u91cd\u7f6e\u8a2d\u5b9a",
"update_announcement": "AdGuard Home {{version}} \u73fe\u70ba\u53ef\u7528\u7684\uff01\u95dc\u65bc\u66f4\u591a\u7684\u8cc7\u8a0a\uff0c<0>\u9ede\u64ca\u9019\u88e1<\/0>\u3002",
"setup_guide": "\u5b89\u88dd\u6307\u5357",
"dns_addresses": "DNS \u4f4d\u5740"
} }

View File

@ -3,7 +3,7 @@ import round from 'lodash/round';
import { t } from 'i18next'; import { t } from 'i18next';
import { showLoading, hideLoading } from 'react-redux-loading-bar'; import { showLoading, hideLoading } from 'react-redux-loading-bar';
import { normalizeHistory, normalizeFilteringStatus, normalizeLogs } from '../helpers/helpers'; import { normalizeHistory, normalizeFilteringStatus, normalizeLogs, normalizeTextarea } from '../helpers/helpers';
import { SETTINGS_NAMES } from '../helpers/constants'; import { SETTINGS_NAMES } from '../helpers/constants';
import Api from '../api/Api'; import Api from '../api/Api';
@ -452,10 +452,18 @@ export const setUpstreamRequest = createAction('SET_UPSTREAM_REQUEST');
export const setUpstreamFailure = createAction('SET_UPSTREAM_FAILURE'); export const setUpstreamFailure = createAction('SET_UPSTREAM_FAILURE');
export const setUpstreamSuccess = createAction('SET_UPSTREAM_SUCCESS'); export const setUpstreamSuccess = createAction('SET_UPSTREAM_SUCCESS');
export const setUpstream = url => async (dispatch) => { export const setUpstream = config => async (dispatch) => {
dispatch(setUpstreamRequest()); dispatch(setUpstreamRequest());
try { try {
await apiClient.setUpstream(url); const values = { ...config };
values.bootstrap_dns = (
values.bootstrap_dns && normalizeTextarea(values.bootstrap_dns)
) || [];
values.upstream_dns = (
values.upstream_dns && normalizeTextarea(values.upstream_dns)
) || [];
await apiClient.setUpstream(values);
dispatch(addSuccessToast('updated_upstream_dns_toast')); dispatch(addSuccessToast('updated_upstream_dns_toast'));
dispatch(setUpstreamSuccess()); dispatch(setUpstreamSuccess());
} catch (error) { } catch (error) {
@ -468,11 +476,18 @@ export const testUpstreamRequest = createAction('TEST_UPSTREAM_REQUEST');
export const testUpstreamFailure = createAction('TEST_UPSTREAM_FAILURE'); export const testUpstreamFailure = createAction('TEST_UPSTREAM_FAILURE');
export const testUpstreamSuccess = createAction('TEST_UPSTREAM_SUCCESS'); export const testUpstreamSuccess = createAction('TEST_UPSTREAM_SUCCESS');
export const testUpstream = servers => async (dispatch) => { export const testUpstream = config => async (dispatch) => {
dispatch(testUpstreamRequest()); dispatch(testUpstreamRequest());
try { try {
const upstreamResponse = await apiClient.testUpstream(servers); const values = { ...config };
values.bootstrap_dns = (
values.bootstrap_dns && normalizeTextarea(values.bootstrap_dns)
) || [];
values.upstream_dns = (
values.upstream_dns && normalizeTextarea(values.upstream_dns)
) || [];
const upstreamResponse = await apiClient.testUpstream(values);
const testMessages = Object.keys(upstreamResponse).map((key) => { const testMessages = Object.keys(upstreamResponse).map((key) => {
const message = upstreamResponse[key]; const message = upstreamResponse[key];
if (message !== 'OK') { if (message !== 'OK') {

View File

@ -34,7 +34,7 @@ export default class Api {
GLOBAL_QUERY_LOG = { path: 'querylog', method: 'GET' }; GLOBAL_QUERY_LOG = { path: 'querylog', method: 'GET' };
GLOBAL_QUERY_LOG_ENABLE = { path: 'querylog_enable', method: 'POST' }; GLOBAL_QUERY_LOG_ENABLE = { path: 'querylog_enable', method: 'POST' };
GLOBAL_QUERY_LOG_DISABLE = { path: 'querylog_disable', method: 'POST' }; GLOBAL_QUERY_LOG_DISABLE = { path: 'querylog_disable', method: 'POST' };
GLOBAL_SET_UPSTREAM_DNS = { path: 'set_upstream_dns', method: 'POST' }; GLOBAL_SET_UPSTREAM_DNS = { path: 'set_upstreams_config', method: 'POST' };
GLOBAL_TEST_UPSTREAM_DNS = { path: 'test_upstream_dns', method: 'POST' }; GLOBAL_TEST_UPSTREAM_DNS = { path: 'test_upstream_dns', method: 'POST' };
GLOBAL_VERSION = { path: 'version.json', method: 'GET' }; GLOBAL_VERSION = { path: 'version.json', method: 'GET' };
GLOBAL_ENABLE_PROTECTION = { path: 'enable_protection', method: 'POST' }; GLOBAL_ENABLE_PROTECTION = { path: 'enable_protection', method: 'POST' };
@ -110,7 +110,7 @@ export default class Api {
const { path, method } = this.GLOBAL_SET_UPSTREAM_DNS; const { path, method } = this.GLOBAL_SET_UPSTREAM_DNS;
const config = { const config = {
data: url, data: url,
header: { 'Content-Type': 'text/plain' }, headers: { 'Content-Type': 'application/json' },
}; };
return this.makeRequest(path, method, config); return this.makeRequest(path, method, config);
} }
@ -119,7 +119,7 @@ export default class Api {
const { path, method } = this.GLOBAL_TEST_UPSTREAM_DNS; const { path, method } = this.GLOBAL_TEST_UPSTREAM_DNS;
const config = { const config = {
data: servers, data: servers,
header: { 'Content-Type': 'text/plain' }, headers: { 'Content-Type': 'application/json' },
}; };
return this.makeRequest(path, method, config); return this.makeRequest(path, method, config);
} }

View File

@ -14,8 +14,9 @@ import Dashboard from '../../containers/Dashboard';
import Settings from '../../containers/Settings'; import Settings from '../../containers/Settings';
import Filters from '../../containers/Filters'; import Filters from '../../containers/Filters';
import Logs from '../../containers/Logs'; import Logs from '../../containers/Logs';
import Footer from '../ui/Footer'; import SetupGuide from '../../containers/SetupGuide';
import Toasts from '../Toasts'; import Toasts from '../Toasts';
import Footer from '../ui/Footer';
import Status from '../ui/Status'; import Status from '../ui/Status';
import UpdateTopline from '../ui/UpdateTopline'; import UpdateTopline from '../ui/UpdateTopline';
import EncryptionTopline from '../ui/EncryptionTopline'; import EncryptionTopline from '../ui/EncryptionTopline';
@ -86,6 +87,7 @@ class App extends Component {
<Route path="/settings" component={Settings} /> <Route path="/settings" component={Settings} />
<Route path="/filters" component={Filters} /> <Route path="/filters" component={Filters} />
<Route path="/logs" component={Logs} /> <Route path="/logs" component={Logs} />
<Route path="/guide" component={SetupGuide} />
</Fragment> </Fragment>
} }
</div> </div>

View File

@ -76,6 +76,13 @@
font-weight: 600; font-weight: 600;
} }
.nav-version__link {
position: relative;
display: inline-block;
border-bottom: 1px dashed #495057;
cursor: pointer;
}
.header-brand-img { .header-brand-img {
height: 32px; height: 32px;
} }

View File

@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
import enhanceWithClickOutside from 'react-click-outside'; import enhanceWithClickOutside from 'react-click-outside';
import classnames from 'classnames'; import classnames from 'classnames';
import { Trans, withNamespaces } from 'react-i18next'; import { Trans, withNamespaces } from 'react-i18next';
import { REPOSITORY } from '../../helpers/constants';
class Menu extends Component { class Menu extends Component {
handleClickOutside = () => { handleClickOutside = () => {
@ -56,10 +55,10 @@ class Menu extends Component {
</NavLink> </NavLink>
</li> </li>
<li className="nav-item"> <li className="nav-item">
<a href={`${REPOSITORY.URL}/wiki`} className="nav-link" target="_blank" rel="noopener noreferrer"> <NavLink to="/guide" href="/guide" className="nav-link">
<svg className="nav-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#66b574" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12" y2="17"></line></svg> <svg className="nav-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#66b574" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12" y2="17"></line></svg>
<Trans>faq</Trans> <Trans>setup_guide</Trans>
</a> </NavLink>
</li> </li>
</ul> </ul>
</div> </div>

View File

@ -2,24 +2,35 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Trans, withNamespaces } from 'react-i18next'; import { Trans, withNamespaces } from 'react-i18next';
import { getDnsAddress } from '../../helpers/helpers';
function Version(props) { function Version(props) {
const { dnsVersion, dnsAddress, dnsPort } = props; const { dnsVersion, dnsAddresses, dnsPort } = props;
return ( return (
<div className="nav-version"> <div className="nav-version">
<div className="nav-version__text"> <div className="nav-version__text">
<Trans>version</Trans>: <span className="nav-version__value">{dnsVersion}</span> <Trans>version</Trans>: <span className="nav-version__value">{dnsVersion}</span>
</div> </div>
<div className="nav-version__text"> <div className="nav-version__link">
<Trans>address</Trans>: <span className="nav-version__value">{dnsAddress}:{dnsPort}</span> <div className="popover__trigger popover__trigger--address">
<Trans>dns_addresses</Trans>
</div>
<div className="popover__body popover__body--address">
<div className="popover__list">
{dnsAddresses
.map(ip => <li key={ip}>{getDnsAddress(ip, dnsPort)}</li>)
}
</div>
</div>
</div> </div>
</div> </div>
); );
} }
Version.propTypes = { Version.propTypes = {
dnsVersion: PropTypes.string, dnsVersion: PropTypes.string.isRequired,
dnsAddress: PropTypes.string, dnsAddresses: PropTypes.array.isRequired,
dnsPort: PropTypes.number, dnsPort: PropTypes.number.isRequired,
}; };
export default withNamespaces()(Version); export default withNamespaces()(Version);

View File

@ -56,11 +56,13 @@ class Header extends Component {
toggleMenuOpen={this.toggleMenuOpen} toggleMenuOpen={this.toggleMenuOpen}
closeMenu={this.closeMenu} closeMenu={this.closeMenu}
/> />
{!dashboard.processing &&
<div className="col col-sm-6 col-lg-3"> <div className="col col-sm-6 col-lg-3">
<Version <Version
{ ...this.props.dashboard } { ...this.props.dashboard }
/> />
</div> </div>
}
</div> </div>
</div> </div>
</div> </div>

View File

@ -57,6 +57,7 @@ let Form = (props) => {
valid_chain, valid_chain,
valid_key, valid_key,
valid_cert, valid_cert,
valid_pair,
dns_names, dns_names,
key_type, key_type,
issuer, issuer,
@ -65,6 +66,15 @@ let Form = (props) => {
setTlsConfig, setTlsConfig,
} = props; } = props;
const isSavingDisabled = invalid
|| submitting
|| processingConfig
|| processingValidate
|| (isEnabled && (!privateKey || !certificateChain))
|| (privateKey && !valid_key)
|| (certificateChain && !valid_cert)
|| (privateKey && certificateChain && !valid_pair);
return ( return (
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="row"> <div className="row">
@ -291,15 +301,7 @@ let Form = (props) => {
<button <button
type="submit" type="submit"
className="btn btn-success btn-standart" className="btn btn-success btn-standart"
disabled={ disabled={isSavingDisabled}
invalid
|| submitting
|| processingConfig
|| processingValidate
|| (isEnabled && (!privateKey || !certificateChain))
|| (privateKey && !valid_key)
|| (certificateChain && !valid_cert)
}
> >
<Trans>save_config</Trans> <Trans>save_config</Trans>
</button> </button>
@ -334,6 +336,7 @@ Form.propTypes = {
valid_chain: PropTypes.bool, valid_chain: PropTypes.bool,
valid_key: PropTypes.bool, valid_key: PropTypes.bool,
valid_cert: PropTypes.bool, valid_cert: PropTypes.bool,
valid_pair: PropTypes.bool,
dns_names: PropTypes.string, dns_names: PropTypes.string,
key_type: PropTypes.string, key_type: PropTypes.string,
issuer: PropTypes.string, issuer: PropTypes.string,

View File

@ -1,97 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Trans, withNamespaces } from 'react-i18next';
import Card from '../ui/Card';
class Upstream extends Component {
handleChange = (e) => {
const { value } = e.currentTarget;
this.props.handleUpstreamChange(value);
};
handleSubmit = (e) => {
e.preventDefault();
this.props.handleUpstreamSubmit();
};
handleTest = () => {
this.props.handleUpstreamTest();
}
render() {
const testButtonClass = classnames({
'btn btn-primary btn-standard mr-2': true,
'btn btn-primary btn-standard mr-2 btn-loading': this.props.processingTestUpstream,
});
const { t } = this.props;
return (
<Card
title={ t('upstream_dns') }
subtitle={ t('upstream_dns_hint') }
bodyType="card-body box-body--settings"
>
<div className="row">
<div className="col">
<form>
<textarea
className="form-control form-control--textarea"
value={this.props.upstreamDns}
onChange={this.handleChange}
/>
<div className="card-actions">
<button
className={testButtonClass}
type="button"
onClick={this.handleTest}
>
<Trans>test_upstream_btn</Trans>
</button>
<button
className="btn btn-success btn-standard"
type="submit"
onClick={this.handleSubmit}
>
<Trans>apply_btn</Trans>
</button>
</div>
</form>
<hr/>
<div className="list leading-loose">
<Trans>examples_title</Trans>:
<ol className="leading-loose">
<li>
<code>1.1.1.1</code> - { t('example_upstream_regular') }
</li>
<li>
<code>tls://1dot1dot1dot1.cloudflare-dns.com</code> - <span dangerouslySetInnerHTML={{ __html: t('example_upstream_dot') }} />
</li>
<li>
<code>https://cloudflare-dns.com/dns-query</code> - <span dangerouslySetInnerHTML={{ __html: t('example_upstream_doh') }} />
</li>
<li>
<code>tcp://1.1.1.1</code> - { t('example_upstream_tcp') }
</li>
<li>
<code>sdns://...</code> - <span dangerouslySetInnerHTML={{ __html: t('example_upstream_sdns') }} />
</li>
</ol>
</div>
</div>
</div>
</Card>
);
}
}
Upstream.propTypes = {
upstreamDns: PropTypes.string,
processingTestUpstream: PropTypes.bool,
handleUpstreamChange: PropTypes.func,
handleUpstreamSubmit: PropTypes.func,
handleUpstreamTest: PropTypes.func,
t: PropTypes.func,
};
export default withNamespaces()(Upstream);

View File

@ -0,0 +1,85 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Trans, withNamespaces } from 'react-i18next';
const Examples = props => (
<div className="list leading-loose">
<Trans>examples_title</Trans>:
<ol className="leading-loose">
<li>
<code>1.1.1.1</code> - { props.t('example_upstream_regular') }
</li>
<li>
<code>tls://1dot1dot1dot1.cloudflare-dns.com</code> &nbsp;
<span>
<Trans
components={[
<a href="https://en.wikipedia.org/wiki/DNS_over_TLS" target="_blank" rel="noopener noreferrer" key="0">
DNS-over-TLS
</a>,
]}
>
example_upstream_dot
</Trans>
</span>
</li>
<li>
<code>https://cloudflare-dns.com/dns-query</code> &nbsp;
<span>
<Trans
components={[
<a href="https://en.wikipedia.org/wiki/DNS_over_HTTPS" target="_blank" rel="noopener noreferrer" key="0">
DNS-over-HTTPS
</a>,
]}
>
example_upstream_doh
</Trans>
</span>
</li>
<li>
<code>tcp://1.1.1.1</code> <Trans>example_upstream_tcp</Trans>
</li>
<li>
<code>sdns://...</code> &nbsp;
<span>
<Trans
components={[
<a href="https://dnscrypt.info/stamps/" target="_blank" rel="noopener noreferrer" key="0">
DNS Stamps
</a>,
<a href="https://dnscrypt.info/" target="_blank" rel="noopener noreferrer" key="1">
DNSCrypt
</a>,
<a href="https://en.wikipedia.org/wiki/DNS_over_HTTPS" target="_blank" rel="noopener noreferrer" key="2">
DNS-over-HTTPS
</a>,
]}
>
example_upstream_sdns
</Trans>
</span>
</li>
<li>
<code>[/example.local/]1.1.1.1</code> &nbsp;
<span>
<Trans
components={[
<a href="https://github.com/AdguardTeam/dnsproxy#specifying-upstreams-for-domains" target="_blank" rel="noopener noreferrer" key="0">
Link
</a>,
]}
>
example_upstream_reserved
</Trans>
</span>
</li>
</ol>
</div>
);
Examples.propTypes = {
t: PropTypes.func.isRequired,
};
export default withNamespaces()(Examples);

View File

@ -0,0 +1,144 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { Field, reduxForm, formValueSelector } from 'redux-form';
import { Trans, withNamespaces } from 'react-i18next';
import flow from 'lodash/flow';
import classnames from 'classnames';
import { renderSelectField } from '../../../helpers/form';
import Examples from './Examples';
let Form = (props) => {
const {
t,
handleSubmit,
testUpstream,
upstreamDns,
bootstrapDns,
allServers,
submitting,
invalid,
processingSetUpstream,
processingTestUpstream,
} = props;
const testButtonClass = classnames({
'btn btn-primary btn-standard mr-2': true,
'btn btn-primary btn-standard mr-2 btn-loading': processingTestUpstream,
});
return (
<form onSubmit={handleSubmit}>
<div className="row">
<div className="col-12">
<div className="form__group form__group--settings">
<label className="form__label" htmlFor="upstream_dns">
<Trans>upstream_dns</Trans>
</label>
<Field
id="upstream_dns"
name="upstream_dns"
component="textarea"
type="text"
className="form-control form-control--textarea"
placeholder={t('upstream_dns')}
/>
</div>
</div>
<div className="col-12">
<div className="form__group form__group--settings">
<Field
name="all_servers"
type="checkbox"
component={renderSelectField}
placeholder={t('upstream_parallel')}
/>
</div>
</div>
<div className="col-12">
<Examples />
<hr/>
</div>
<div className="col-12">
<div className="form__group">
<label className="form__label" htmlFor="bootstrap_dns">
<Trans>bootstrap_dns</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>bootstrap_dns_desc</Trans>
</div>
<Field
id="bootstrap_dns"
name="bootstrap_dns"
component="textarea"
type="text"
className="form-control"
placeholder={t('bootstrap_dns')}
/>
</div>
</div>
</div>
<div className="card-actions">
<div className="btn-list">
<button
type="button"
className={testButtonClass}
onClick={() => testUpstream({
upstream_dns: upstreamDns,
bootstrap_dns: bootstrapDns,
all_servers: allServers,
})}
disabled={!upstreamDns || processingTestUpstream}
>
<Trans>test_upstream_btn</Trans>
</button>
<button
type="submit"
className="btn btn-success btn-standard"
disabled={
submitting
|| invalid
|| processingSetUpstream
|| processingTestUpstream
}
>
<Trans>apply_btn</Trans>
</button>
</div>
</div>
</form>
);
};
Form.propTypes = {
handleSubmit: PropTypes.func,
testUpstream: PropTypes.func,
submitting: PropTypes.bool,
invalid: PropTypes.bool,
initialValues: PropTypes.object,
upstreamDns: PropTypes.string,
bootstrapDns: PropTypes.string,
allServers: PropTypes.bool,
processingTestUpstream: PropTypes.bool,
processingSetUpstream: PropTypes.bool,
t: PropTypes.func,
};
const selector = formValueSelector('upstreamForm');
Form = connect((state) => {
const upstreamDns = selector(state, 'upstream_dns');
const bootstrapDns = selector(state, 'bootstrap_dns');
const allServers = selector(state, 'all_servers');
return {
upstreamDns,
bootstrapDns,
allServers,
};
})(Form);
export default flow([
withNamespaces(),
reduxForm({ form: 'upstreamForm' }),
])(Form);

View File

@ -0,0 +1,64 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withNamespaces } from 'react-i18next';
import Form from './Form';
import Card from '../../ui/Card';
class Upstream extends Component {
handleSubmit = (values) => {
this.props.setUpstream(values);
};
handleTest = (values) => {
this.props.testUpstream(values);
}
render() {
const {
t,
upstreamDns: upstream_dns,
bootstrapDns: bootstrap_dns,
allServers: all_servers,
processingSetUpstream,
processingTestUpstream,
} = this.props;
return (
<Card
title={ t('upstream_dns') }
subtitle={ t('upstream_dns_hint') }
bodyType="card-body box-body--settings"
>
<div className="row">
<div className="col">
<Form
initialValues={{
upstream_dns,
bootstrap_dns,
all_servers,
}}
testUpstream={this.handleTest}
onSubmit={this.handleSubmit}
processingTestUpstream={processingTestUpstream}
processingSetUpstream={processingSetUpstream}
/>
</div>
</div>
</Card>
);
}
}
Upstream.propTypes = {
upstreamDns: PropTypes.string,
bootstrapDns: PropTypes.string,
allServers: PropTypes.bool,
setUpstream: PropTypes.func.isRequired,
testUpstream: PropTypes.func.isRequired,
processingSetUpstream: PropTypes.bool.isRequired,
processingTestUpstream: PropTypes.bool.isRequired,
t: PropTypes.func.isRequired,
};
export default withNamespaces()(Upstream);

View File

@ -41,22 +41,6 @@ class Settings extends Component {
this.props.getTlsStatus(); this.props.getTlsStatus();
} }
handleUpstreamChange = (value) => {
this.props.handleUpstreamChange({ upstreamDns: value });
};
handleUpstreamSubmit = () => {
this.props.setUpstream(this.props.dashboard.upstreamDns);
};
handleUpstreamTest = () => {
if (this.props.dashboard.upstreamDns.length > 0) {
this.props.testUpstream(this.props.dashboard.upstreamDns);
} else {
this.props.addErrorToast({ error: this.props.t('no_servers_specified') });
}
};
renderSettings = (settings) => { renderSettings = (settings) => {
if (Object.keys(settings).length > 0) { if (Object.keys(settings).length > 0) {
return Object.keys(settings).map((key) => { return Object.keys(settings).map((key) => {
@ -75,8 +59,7 @@ class Settings extends Component {
} }
render() { render() {
const { settings, t } = this.props; const { settings, dashboard, t } = this.props;
const { upstreamDns } = this.props.dashboard;
return ( return (
<Fragment> <Fragment>
<PageTitle title={ t('settings') } /> <PageTitle title={ t('settings') } />
@ -91,11 +74,13 @@ class Settings extends Component {
</div> </div>
</Card> </Card>
<Upstream <Upstream
upstreamDns={upstreamDns} upstreamDns={dashboard.upstreamDns}
bootstrapDns={dashboard.bootstrapDns}
allServers={dashboard.allServers}
setUpstream={this.props.setUpstream}
testUpstream={this.props.testUpstream}
processingTestUpstream={settings.processingTestUpstream} processingTestUpstream={settings.processingTestUpstream}
handleUpstreamChange={this.handleUpstreamChange} processingSetUpstream={settings.processingSetUpstream}
handleUpstreamSubmit={this.handleUpstreamSubmit}
handleUpstreamTest={this.handleUpstreamTest}
/> />
<Encryption <Encryption
encryption={this.props.encryption} encryption={this.props.encryption}
@ -125,7 +110,6 @@ Settings.propTypes = {
toggleSetting: PropTypes.func, toggleSetting: PropTypes.func,
handleUpstreamChange: PropTypes.func, handleUpstreamChange: PropTypes.func,
setUpstream: PropTypes.func, setUpstream: PropTypes.func,
upstream: PropTypes.string,
t: PropTypes.func, t: PropTypes.func,
}; };

View File

@ -0,0 +1,15 @@
.guide {
max-width: 768px;
margin: 0 auto;
}
.guide__title {
margin-bottom: 10px;
font-size: 17px;
font-weight: 700;
}
.guide__desc {
margin-bottom: 20px;
font-size: 15px;
}

View File

@ -0,0 +1,46 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Trans, withNamespaces } from 'react-i18next';
import { getDnsAddress } from '../../helpers/helpers';
import Guide from '../ui/Guide';
import Card from '../ui/Card';
import PageTitle from '../ui/PageTitle';
import './Guide.css';
const SetupGuide = ({
t,
dashboard: {
dnsAddresses,
dnsPort,
},
}) => (
<div className="guide">
<PageTitle title={t('setup_guide')} />
<Card>
<div className="guide__title">
<Trans>install_devices_title</Trans>
</div>
<div className="guide__desc">
<Trans>install_devices_desc</Trans>
<div className="mt-1">
<Trans>install_devices_address</Trans>:
</div>
<div className="mt-2 font-weight-bold">
{dnsAddresses
.map(ip => <li key={ip}>{getDnsAddress(ip, dnsPort)}</li>)
}
</div>
</div>
<Guide />
</Card>
</div>
);
SetupGuide.propTypes = {
dashboard: PropTypes.object.isRequired,
t: PropTypes.func.isRequired,
};
export default withNamespaces()(SetupGuide);

View File

@ -91,6 +91,10 @@
line-height: 1.5; line-height: 1.5;
} }
.checkbox__label-text--long {
max-width: initial;
}
.checkbox__label-title { .checkbox__label-title {
display: block; display: block;
line-height: 1.5; line-height: 1.5;

View File

@ -0,0 +1,83 @@
import React from 'react';
import { Trans, withNamespaces } from 'react-i18next';
import Tabs from '../ui/Tabs';
import Icons from '../ui/Icons';
const Guide = () => (
<div>
<Icons />
<Tabs>
<div label="Router">
<div className="tab__title">
<Trans>install_devices_router</Trans>
</div>
<div className="tab__text">
<p><Trans>install_devices_router_desc</Trans></p>
<ol>
<li><Trans>install_devices_router_list_1</Trans></li>
<li><Trans>install_devices_router_list_2</Trans></li>
<li><Trans>install_devices_router_list_3</Trans></li>
</ol>
</div>
</div>
<div label="Windows">
<div className="tab__title">
Windows
</div>
<div className="tab__text">
<ol>
<li><Trans>install_devices_windows_list_1</Trans></li>
<li><Trans>install_devices_windows_list_2</Trans></li>
<li><Trans>install_devices_windows_list_3</Trans></li>
<li><Trans>install_devices_windows_list_4</Trans></li>
<li><Trans>install_devices_windows_list_5</Trans></li>
<li><Trans>install_devices_windows_list_6</Trans></li>
</ol>
</div>
</div>
<div label="macOS">
<div className="tab__title">
macOS
</div>
<div className="tab__text">
<ol>
<li><Trans>install_devices_macos_list_1</Trans></li>
<li><Trans>install_devices_macos_list_2</Trans></li>
<li><Trans>install_devices_macos_list_3</Trans></li>
<li><Trans>install_devices_macos_list_4</Trans></li>
</ol>
</div>
</div>
<div label="Android">
<div className="tab__title">
Android
</div>
<div className="tab__text">
<ol>
<li><Trans>install_devices_android_list_1</Trans></li>
<li><Trans>install_devices_android_list_2</Trans></li>
<li><Trans>install_devices_android_list_3</Trans></li>
<li><Trans>install_devices_android_list_4</Trans></li>
<li><Trans>install_devices_android_list_5</Trans></li>
</ol>
</div>
</div>
<div label="iOS">
<div className="tab__title">
iOS
</div>
<div className="tab__text">
<ol>
<li><Trans>install_devices_ios_list_1</Trans></li>
<li><Trans>install_devices_ios_list_2</Trans></li>
<li><Trans>install_devices_ios_list_3</Trans></li>
<li><Trans>install_devices_ios_list_4</Trans></li>
</ol>
</div>
</div>
</Tabs>
</div>
);
export default withNamespaces()(Guide);

View File

@ -22,6 +22,16 @@
height: 24px; height: 24px;
} }
.popover__trigger--address {
top: 0;
margin: 0;
line-height: 1.2;
}
.popover__trigger--address:after {
display: none;
}
.popover__body { .popover__body {
content: ""; content: "";
display: flex; display: flex;
@ -57,6 +67,38 @@
border-top: 6px solid #585965; border-top: 6px solid #585965;
} }
.popover__body--address {
top: calc(100% + 10px);
right: 0;
left: initial;
bottom: initial;
z-index: 1;
min-width: 100px;
padding: 12px 18px;
font-weight: 700;
text-align: left;
white-space: nowrap;
transform: none;
cursor: default;
}
.popover__body--address:after {
top: -11px;
left: initial;
right: 40px;
border-top: 6px solid transparent;
border-bottom: 6px solid #585965;
}
.popover__body--address:before {
content: "";
position: absolute;
top: -7px;
left: 0;
width: 100%;
height: 10px;
}
.popover__trigger:hover + .popover__body, .popover__trigger:hover + .popover__body,
.popover__body:hover { .popover__body:hover {
visibility: visible; visibility: visible;
@ -73,6 +115,10 @@
stroke: #66b574; stroke: #66b574;
} }
.popover__list--bold {
font-weight: 700;
}
.popover__list-title { .popover__list-title {
margin-bottom: 3px; margin-bottom: 3px;
} }

View File

@ -0,0 +1,14 @@
import { connect } from 'react-redux';
import * as actionCreators from '../actions';
import SetupGuide from '../components/SetupGuide';
const mapStateToProps = (state) => {
const { dashboard } = state;
const props = { dashboard };
return props;
};
export default connect(
mapStateToProps,
actionCreators,
)(SetupGuide);

View File

@ -47,6 +47,10 @@ export const LANGUAGES = [
key: 'vi', key: 'vi',
name: 'Tiếng Việt', name: 'Tiếng Việt',
}, },
{
key: 'bg',
name: 'Български',
},
{ {
key: 'ru', key: 'ru',
name: 'Русский', name: 'Русский',
@ -59,6 +63,10 @@ export const LANGUAGES = [
key: 'zh-tw', key: 'zh-tw',
name: '正體中文', name: '正體中文',
}, },
{
key: 'zh-cn',
name: '简体中文',
},
]; ];
export const INSTALL_FIRST_STEP = 1; export const INSTALL_FIRST_STEP = 1;

View File

@ -32,7 +32,7 @@ export const renderSelectField = ({
disabled={disabled} disabled={disabled}
/> />
<span className="checkbox__label"> <span className="checkbox__label">
<span className="checkbox__label-text"> <span className="checkbox__label-text checkbox__label-text--long">
<span className="checkbox__label-title">{placeholder}</span> <span className="checkbox__label-title">{placeholder}</span>
</span> </span>
</span> </span>

View File

@ -201,3 +201,5 @@ export const redirectToCurrentProtocol = (values, httpPort = 80) => {
window.location.replace(`http://${hostname}:${httpPort}/${hash}`); window.location.replace(`http://${hostname}:${httpPort}/${hash}`);
} }
}; };
export const normalizeTextarea = text => text && text.replace(/[;, ]/g, '\n').split('\n').filter(n => n);

View File

@ -12,6 +12,8 @@ import ja from './__locales/ja.json';
import sv from './__locales/sv.json'; import sv from './__locales/sv.json';
import ptBR from './__locales/pt-br.json'; import ptBR from './__locales/pt-br.json';
import zhTW from './__locales/zh-tw.json'; import zhTW from './__locales/zh-tw.json';
import bg from './__locales/bg.json';
import zhCN from './__locales/zh-cn.json';
const resources = { const resources = {
en: { en: {
@ -41,6 +43,12 @@ const resources = {
'zh-TW': { 'zh-TW': {
translation: zhTW, translation: zhTW,
}, },
bg: {
translation: bg,
},
'zh-CN': {
translation: zhCN,
},
}; };
i18n i18n

View File

@ -5,8 +5,7 @@ import { reduxForm, formValueSelector } from 'redux-form';
import { Trans, withNamespaces } from 'react-i18next'; import { Trans, withNamespaces } from 'react-i18next';
import flow from 'lodash/flow'; import flow from 'lodash/flow';
import Tabs from '../../components/ui/Tabs'; import Guide from '../../components/ui/Guide';
import Icons from '../../components/ui/Icons';
import Controls from './Controls'; import Controls from './Controls';
import AddressList from './AddressList'; import AddressList from './AddressList';
@ -30,77 +29,7 @@ let Devices = props => (
/> />
</div> </div>
</div> </div>
<Icons /> <Guide />
<Tabs>
<div label="Router">
<div className="tab__title">
<Trans>install_devices_router</Trans>
</div>
<div className="tab__text">
<p><Trans>install_devices_router_desc</Trans></p>
<ol>
<li><Trans>install_devices_router_list_1</Trans></li>
<li><Trans>install_devices_router_list_2</Trans></li>
<li><Trans>install_devices_router_list_3</Trans></li>
</ol>
</div>
</div>
<div label="Windows">
<div className="tab__title">
Windows
</div>
<div className="tab__text">
<ol>
<li><Trans>install_devices_windows_list_1</Trans></li>
<li><Trans>install_devices_windows_list_2</Trans></li>
<li><Trans>install_devices_windows_list_3</Trans></li>
<li><Trans>install_devices_windows_list_4</Trans></li>
<li><Trans>install_devices_windows_list_5</Trans></li>
<li><Trans>install_devices_windows_list_6</Trans></li>
</ol>
</div>
</div>
<div label="macOS">
<div className="tab__title">
macOS
</div>
<div className="tab__text">
<ol>
<li><Trans>install_devices_macos_list_1</Trans></li>
<li><Trans>install_devices_macos_list_2</Trans></li>
<li><Trans>install_devices_macos_list_3</Trans></li>
<li><Trans>install_devices_macos_list_4</Trans></li>
</ol>
</div>
</div>
<div label="Android">
<div className="tab__title">
Android
</div>
<div className="tab__text">
<ol>
<li><Trans>install_devices_android_list_1</Trans></li>
<li><Trans>install_devices_android_list_2</Trans></li>
<li><Trans>install_devices_android_list_3</Trans></li>
<li><Trans>install_devices_android_list_4</Trans></li>
<li><Trans>install_devices_android_list_5</Trans></li>
</ol>
</div>
</div>
<div label="iOS">
<div className="tab__title">
iOS
</div>
<div className="tab__text">
<ol>
<li><Trans>install_devices_ios_list_1</Trans></li>
<li><Trans>install_devices_ios_list_2</Trans></li>
<li><Trans>install_devices_ios_list_3</Trans></li>
<li><Trans>install_devices_ios_list_4</Trans></li>
</ol>
</div>
</div>
</Tabs>
</div> </div>
<Controls /> <Controls />
</div> </div>

View File

@ -70,6 +70,7 @@ const encryption = handleActions({
valid_chain: false, valid_chain: false,
valid_key: false, valid_key: false,
valid_cert: false, valid_cert: false,
valid_pair: false,
status_cert: '', status_cert: '',
status_key: '', status_key: '',
certificate_chain: '', certificate_chain: '',

View File

@ -48,9 +48,11 @@ const dashboard = handleActions({
version, version,
running, running,
dns_port: dnsPort, dns_port: dnsPort,
dns_address: dnsAddress, dns_addresses: dnsAddresses,
querylog_enabled: queryLogEnabled, querylog_enabled: queryLogEnabled,
upstream_dns: upstreamDns, upstream_dns: upstreamDns,
bootstrap_dns: bootstrapDns,
all_servers: allServers,
protection_enabled: protectionEnabled, protection_enabled: protectionEnabled,
language, language,
http_port: httpPort, http_port: httpPort,
@ -61,9 +63,11 @@ const dashboard = handleActions({
processing: false, processing: false,
dnsVersion: version, dnsVersion: version,
dnsPort, dnsPort,
dnsAddress, dnsAddresses,
queryLogEnabled, queryLogEnabled,
upstreamDns: upstreamDns.join('\n'), upstreamDns: upstreamDns.join('\n'),
bootstrapDns: bootstrapDns.join('\n'),
allServers,
protectionEnabled, protectionEnabled,
language, language,
httpPort, httpPort,
@ -171,10 +175,15 @@ const dashboard = handleActions({
logStatusProcessing: false, logStatusProcessing: false,
processingVersion: true, processingVersion: true,
processingFiltering: true, processingFiltering: true,
upstreamDns: [], upstreamDns: '',
bootstrapDns: '',
allServers: false,
protectionEnabled: false, protectionEnabled: false,
processingProtection: false, processingProtection: false,
httpPort: 80, httpPort: 80,
dnsPort: 53,
dnsAddresses: [],
dnsVersion: '',
}); });
const queryLogs = handleActions({ const queryLogs = handleActions({

View File

@ -5,12 +5,14 @@ const ExtractTextPlugin = require('extract-text-webpack-plugin');
const webpack = require('webpack'); const webpack = require('webpack');
const flexBugsFixes = require('postcss-flexbugs-fixes'); const flexBugsFixes = require('postcss-flexbugs-fixes');
const CleanWebpackPlugin = require('clean-webpack-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');
const RESOURCES_PATH = path.resolve(__dirname); const RESOURCES_PATH = path.resolve(__dirname);
const ENTRY_REACT = path.resolve(RESOURCES_PATH, 'src/index.js'); const ENTRY_REACT = path.resolve(RESOURCES_PATH, 'src/index.js');
const ENTRY_INSTALL = path.resolve(RESOURCES_PATH, 'src/install/index.js'); const ENTRY_INSTALL = path.resolve(RESOURCES_PATH, 'src/install/index.js');
const HTML_PATH = path.resolve(RESOURCES_PATH, 'public/index.html'); const HTML_PATH = path.resolve(RESOURCES_PATH, 'public/index.html');
const HTML_INSTALL_PATH = path.resolve(RESOURCES_PATH, 'public/install.html'); const HTML_INSTALL_PATH = path.resolve(RESOURCES_PATH, 'public/install.html');
const FAVICON_PATH = path.resolve(RESOURCES_PATH, 'public/favicon.ico');
const PUBLIC_PATH = path.resolve(__dirname, '../build/static'); const PUBLIC_PATH = path.resolve(__dirname, '../build/static');
@ -117,6 +119,9 @@ const config = {
new ExtractTextPlugin({ new ExtractTextPlugin({
filename: '[name].[contenthash].css', filename: '[name].[contenthash].css',
}), }),
new CopyPlugin([
{ from: FAVICON_PATH, to: PUBLIC_PATH },
]),
], ],
}; };

View File

@ -11,7 +11,8 @@ import (
"github.com/AdguardTeam/AdGuardHome/dhcpd" "github.com/AdguardTeam/AdGuardHome/dhcpd"
"github.com/AdguardTeam/AdGuardHome/dnsfilter" "github.com/AdguardTeam/AdGuardHome/dnsfilter"
"github.com/AdguardTeam/AdGuardHome/dnsforward" "github.com/AdguardTeam/AdGuardHome/dnsforward"
"github.com/hmage/golibs/log" "github.com/AdguardTeam/golibs/file"
"github.com/AdguardTeam/golibs/log"
yaml "gopkg.in/yaml.v2" yaml "gopkg.in/yaml.v2"
) )
@ -61,6 +62,9 @@ type dnsConfig struct {
UpstreamDNS []string `yaml:"upstream_dns"` UpstreamDNS []string `yaml:"upstream_dns"`
} }
var defaultDNS = []string{"https://dns.cloudflare.com/dns-query"}
var defaultBootstrap = []string{"1.1.1.1"}
type tlsConfigSettings struct { type tlsConfigSettings struct {
Enabled bool `yaml:"enabled" json:"enabled"` // Enabled is the encryption (DOT/DOH/HTTPS) status Enabled bool `yaml:"enabled" json:"enabled"` // Enabled is the encryption (DOT/DOH/HTTPS) status
ServerName string `yaml:"server_name" json:"server_name,omitempty"` // ServerName is the hostname of your HTTPS/TLS server ServerName string `yaml:"server_name" json:"server_name,omitempty"` // ServerName is the hostname of your HTTPS/TLS server
@ -86,7 +90,7 @@ type tlsConfigStatus struct {
KeyType string `yaml:"-" json:"key_type,omitempty"` // KeyType is one of RSA or ECDSA KeyType string `yaml:"-" json:"key_type,omitempty"` // KeyType is one of RSA or ECDSA
// is usable? set by validator // is usable? set by validator
usable bool ValidPair bool `yaml:"-" json:"valid_pair"` // ValidPair is true if both certificate and private key are correct
// warnings // warnings
WarningValidation string `yaml:"-" json:"warning_validation,omitempty"` // WarningValidation is a validation warning message with the issue description WarningValidation string `yaml:"-" json:"warning_validation,omitempty"` // WarningValidation is a validation warning message with the issue description
@ -98,8 +102,6 @@ type tlsConfig struct {
tlsConfigStatus `yaml:"-" json:",inline"` tlsConfigStatus `yaml:"-" json:",inline"`
} }
var defaultDNS = []string{"tls://1.1.1.1", "tls://1.0.0.1"}
// initialize to default values, will be changed later when reading config or parsing command line // initialize to default values, will be changed later when reading config or parsing command line
var config = configuration{ var config = configuration{
ourConfigFilename: "AdGuardHome.yaml", ourConfigFilename: "AdGuardHome.yaml",
@ -115,7 +117,8 @@ var config = configuration{
QueryLogEnabled: true, QueryLogEnabled: true,
Ratelimit: 20, Ratelimit: 20,
RefuseAny: true, RefuseAny: true,
BootstrapDNS: "8.8.8.8:53", BootstrapDNS: defaultBootstrap,
AllServers: false,
}, },
UpstreamDNS: defaultDNS, UpstreamDNS: defaultDNS,
}, },
@ -129,7 +132,7 @@ var config = configuration{
{Filter: dnsfilter.Filter{ID: 1}, Enabled: true, URL: "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt", Name: "AdGuard Simplified Domain Names filter"}, {Filter: dnsfilter.Filter{ID: 1}, Enabled: true, URL: "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt", Name: "AdGuard Simplified Domain Names filter"},
{Filter: dnsfilter.Filter{ID: 2}, Enabled: false, URL: "https://adaway.org/hosts.txt", Name: "AdAway"}, {Filter: dnsfilter.Filter{ID: 2}, Enabled: false, URL: "https://adaway.org/hosts.txt", Name: "AdAway"},
{Filter: dnsfilter.Filter{ID: 3}, Enabled: false, URL: "https://hosts-file.net/ad_servers.txt", Name: "hpHosts - Ad and Tracking servers only"}, {Filter: dnsfilter.Filter{ID: 3}, Enabled: false, URL: "https://hosts-file.net/ad_servers.txt", Name: "hpHosts - Ad and Tracking servers only"},
{Filter: dnsfilter.Filter{ID: 4}, Enabled: false, URL: "http://www.malwaredomainlist.com/hostslist/hosts.txt", Name: "MalwareDomainList.com Hosts List"}, {Filter: dnsfilter.Filter{ID: 4}, Enabled: false, URL: "https://www.malwaredomainlist.com/hostslist/hosts.txt", Name: "MalwareDomainList.com Hosts List"},
}, },
SchemaVersion: currentSchemaVersion, SchemaVersion: currentSchemaVersion,
} }
@ -146,9 +149,15 @@ func init() {
// getConfigFilename returns path to the current config file // getConfigFilename returns path to the current config file
func (c *configuration) getConfigFilename() string { func (c *configuration) getConfigFilename() string {
configFile := config.ourConfigFilename configFile, err := filepath.EvalSymlinks(config.ourConfigFilename)
if err != nil {
if !os.IsNotExist(err) {
log.Error("unexpected error while config file path evaluation: %s", err)
}
configFile = config.ourConfigFilename
}
if !filepath.IsAbs(configFile) { if !filepath.IsAbs(configFile) {
configFile = filepath.Join(config.ourWorkingDir, config.ourConfigFilename) configFile = filepath.Join(config.ourWorkingDir, configFile)
} }
return configFile return configFile
} }
@ -163,7 +172,7 @@ func getLogSettings() logSettings {
} }
err = yaml.Unmarshal(yamlFile, &l) err = yaml.Unmarshal(yamlFile, &l)
if err != nil { if err != nil {
log.Printf("Couldn't get logging settings from the configuration: %s", err) log.Error("Couldn't get logging settings from the configuration: %s", err)
} }
return l return l
} }
@ -171,19 +180,19 @@ func getLogSettings() logSettings {
// parseConfig loads configuration from the YAML file // parseConfig loads configuration from the YAML file
func parseConfig() error { func parseConfig() error {
configFile := config.getConfigFilename() configFile := config.getConfigFilename()
log.Printf("Reading config file: %s", configFile) log.Debug("Reading config file: %s", configFile)
yamlFile, err := readConfigFile() yamlFile, err := readConfigFile()
if err != nil { if err != nil {
log.Printf("Couldn't read config file: %s", err) log.Error("Couldn't read config file: %s", err)
return err return err
} }
if yamlFile == nil { if yamlFile == nil {
log.Printf("YAML file doesn't exist, skipping it") log.Error("YAML file doesn't exist, skipping it")
return nil return nil
} }
err = yaml.Unmarshal(yamlFile, &config) err = yaml.Unmarshal(yamlFile, &config)
if err != nil { if err != nil {
log.Printf("Couldn't parse config file: %s", err) log.Error("Couldn't parse config file: %s", err)
return err return err
} }
@ -210,19 +219,19 @@ func (c *configuration) write() error {
c.Lock() c.Lock()
defer c.Unlock() defer c.Unlock()
if config.firstRun { if config.firstRun {
log.Tracef("Silently refusing to write config because first run and not configured yet") log.Debug("Silently refusing to write config because first run and not configured yet")
return nil return nil
} }
configFile := config.getConfigFilename() configFile := config.getConfigFilename()
log.Tracef("Writing YAML file: %s", configFile) log.Debug("Writing YAML file: %s", configFile)
yamlText, err := yaml.Marshal(&config) yamlText, err := yaml.Marshal(&config)
if err != nil { if err != nil {
log.Printf("Couldn't generate YAML file: %s", err) log.Error("Couldn't generate YAML file: %s", err)
return err return err
} }
err = safeWriteFile(configFile, yamlText) err = file.SafeWrite(configFile, yamlText)
if err != nil { if err != nil {
log.Printf("Couldn't save YAML config: %s", err) log.Error("Couldn't save YAML config: %s", err)
return err return err
} }
@ -232,14 +241,14 @@ func (c *configuration) write() error {
func writeAllConfigs() error { func writeAllConfigs() error {
err := config.write() err := config.write()
if err != nil { if err != nil {
log.Printf("Couldn't write config: %s", err) log.Error("Couldn't write config: %s", err)
return err return err
} }
userFilter := userFilter() userFilter := userFilter()
err = userFilter.save() err = userFilter.save()
if err != nil { if err != nil {
log.Printf("Couldn't save the user filter: %s", err) log.Error("Couldn't save the user filter: %s", err)
return err return err
} }

File diff suppressed because it is too large Load Diff

153
control_test.go Normal file
View File

@ -0,0 +1,153 @@
package main
import (
"testing"
"time"
)
/* Tests performed:
. Bad certificate
. Bad private key
. Valid certificate & private key */
func TestValidateCertificates(t *testing.T) {
var data tlsConfigStatus
// bad cert
data = validateCertificates("bad cert", "", "")
if !(data.WarningValidation != "" &&
!data.ValidCert &&
!data.ValidChain) {
t.Fatalf("bad cert: validateCertificates(): %v", data)
}
// bad priv key
data = validateCertificates("", "bad priv key", "")
if !(data.WarningValidation != "" &&
!data.ValidKey) {
t.Fatalf("bad priv key: validateCertificates(): %v", data)
}
// valid cert & priv key
CertificateChain := `-----BEGIN CERTIFICATE-----
MIICKzCCAZSgAwIBAgIJAMT9kPVJdM7LMA0GCSqGSIb3DQEBCwUAMC0xFDASBgNV
BAoMC0FkR3VhcmQgTHRkMRUwEwYDVQQDDAxBZEd1YXJkIEhvbWUwHhcNMTkwMjI3
MDkyNDIzWhcNNDYwNzE0MDkyNDIzWjAtMRQwEgYDVQQKDAtBZEd1YXJkIEx0ZDEV
MBMGA1UEAwwMQWRHdWFyZCBIb21lMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB
gQCwvwUnPJiOvLcOaWmGu6Y68ksFr13nrXBcsDlhxlXy8PaohVi3XxEmt2OrVjKW
QFw/bdV4fZ9tdWFAVRRkgeGbIZzP7YBD1Ore/O5SQ+DbCCEafvjJCcXQIrTeKFE6
i9G3aSMHs0Pwq2LgV8U5mYotLrvyFiE8QPInJbDDMpaFYwIDAQABo1MwUTAdBgNV
HQ4EFgQUdLUmQpEqrhn4eKO029jYd2AAZEQwHwYDVR0jBBgwFoAUdLUmQpEqrhn4
eKO029jYd2AAZEQwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOBgQB8
LwlXfbakf7qkVTlCNXgoY7RaJ8rJdPgOZPoCTVToEhT6u/cb1c2qp8QB0dNExDna
b0Z+dnODTZqQOJo6z/wIXlcUrnR4cQVvytXt8lFn+26l6Y6EMI26twC/xWr+1swq
Muj4FeWHVDerquH4yMr1jsYLD3ci+kc5sbIX6TfVxQ==
-----END CERTIFICATE-----`
PrivateKey := `-----BEGIN PRIVATE KEY-----
MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBALC/BSc8mI68tw5p
aYa7pjrySwWvXeetcFywOWHGVfLw9qiFWLdfESa3Y6tWMpZAXD9t1Xh9n211YUBV
FGSB4ZshnM/tgEPU6t787lJD4NsIIRp++MkJxdAitN4oUTqL0bdpIwezQ/CrYuBX
xTmZii0uu/IWITxA8iclsMMyloVjAgMBAAECgYEAmjzoG1h27UDkIlB9BVWl95TP
QVPLB81D267xNFDnWk1Lgr5zL/pnNjkdYjyjgpkBp1yKyE4gHV4skv5sAFWTcOCU
QCgfPfUn/rDFcxVzAdJVWAa/CpJNaZgjTPR8NTGU+Ztod+wfBESNCP5tbnuw0GbL
MuwdLQJGbzeJYpsNysECQQDfFHYoRNfgxHwMbX24GCoNZIgk12uDmGTA9CS5E+72
9t3V1y4CfXxSkfhqNbd5RWrUBRLEw9BKofBS7L9NMDKDAkEAytQoIueE1vqEAaRg
a3A1YDUekKesU5wKfKfKlXvNgB7Hwh4HuvoQS9RCvVhf/60Dvq8KSu6hSjkFRquj
FQ5roQJBAMwKwyiCD5MfJPeZDmzcbVpiocRQ5Z4wPbffl9dRTDnIA5AciZDthlFg
An/jMjZSMCxNl6UyFcqt5Et1EGVhuFECQQCZLXxaT+qcyHjlHJTMzuMgkz1QFbEp
O5EX70gpeGQMPDK0QSWpaazg956njJSDbNCFM4BccrdQbJu1cW4qOsfBAkAMgZuG
O88slmgTRHX4JGFmy3rrLiHNI2BbJSuJ++Yllz8beVzh6NfvuY+HKRCmPqoBPATU
kXS9jgARhhiWXJrk
-----END PRIVATE KEY-----`
data = validateCertificates(CertificateChain, PrivateKey, "")
notBefore, _ := time.Parse(time.RFC3339, "2019-02-27T09:24:23Z")
notAfter, _ := time.Parse(time.RFC3339, "2046-07-14T09:24:23Z")
if !(data.WarningValidation != "" /* self signed */ &&
data.ValidCert &&
!data.ValidChain &&
data.ValidKey &&
data.KeyType == "RSA" &&
data.Subject == "CN=AdGuard Home,O=AdGuard Ltd" &&
data.Issuer == "CN=AdGuard Home,O=AdGuard Ltd" &&
data.NotBefore == notBefore &&
data.NotAfter == notAfter &&
// data.DNSNames[0] == &&
data.ValidPair) {
t.Fatalf("valid cert & priv key: validateCertificates(): %v", data)
}
}
func TestValidateUpstream(t *testing.T) {
invalidUpstreams := []string{"1.2.3.4.5",
"123.3.7m",
"htttps://google.com/dns-query",
"[/host.com]tls://dns.adguard.com",
"[host.ru]#",
}
validDefaultUpstreams := []string{"1.1.1.1",
"tls://1.1.1.1",
"https://dns.adguard.com/dns-query",
"sdns://AQMAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20",
}
validUpstreams := []string{"[/host.com/]1.1.1.1",
"[//]tls://1.1.1.1",
"[/www.host.com/]#",
"[/host.com/google.com/]8.8.8.8",
"[/host/]sdns://AQMAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20",
}
for _, u := range invalidUpstreams {
_, err := validateUpstream(u)
if err == nil {
t.Fatalf("upstream %s is invalid but it pass through validation", u)
}
}
for _, u := range validDefaultUpstreams {
defaultUpstream, err := validateUpstream(u)
if err != nil {
t.Fatalf("upstream %s is valid but it doen't pass through validation cause: %s", u, err)
}
if !defaultUpstream {
t.Fatalf("upstream %s is default one!", u)
}
}
for _, u := range validUpstreams {
defaultUpstream, err := validateUpstream(u)
if err != nil {
t.Fatalf("upstream %s is valid but it doen't pass through validation cause: %s", u, err)
}
if defaultUpstream {
t.Fatalf("upstream %s is default one!", u)
}
}
}
func TestValidateUpstreamsSet(t *testing.T) {
// Set of valid upstreams. There is no default upstream specified
upstreamsSet := []string{"[/host.com/]1.1.1.1",
"[//]tls://1.1.1.1",
"[/www.host.com/]#",
"[/host.com/google.com/]8.8.8.8",
"[/host/]sdns://AQMAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20",
}
err := validateUpstreams(upstreamsSet)
if err == nil {
t.Fatalf("there is no default upstream")
}
// Let's add default upstream
upstreamsSet = append(upstreamsSet, "8.8.8.8")
err = validateUpstreams(upstreamsSet)
if err != nil {
t.Fatalf("upstreams set is valid, but doesn't pass through validation cause: %s", err)
}
// Let's add invalid upstream
upstreamsSet = append(upstreamsSet, "dhcp://fake.dns")
err = validateUpstreams(upstreamsSet)
if err == nil {
t.Fatalf("there is an invalid upstream in set, but it pass through validation")
}
}

338
control_tls.go Normal file
View File

@ -0,0 +1,338 @@
// Control: TLS configuring handlers
package main
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"net/http"
"reflect"
"strings"
"time"
"github.com/AdguardTeam/golibs/log"
"github.com/joomcode/errorx"
)
// RegisterTLSHandlers registers HTTP handlers for TLS configuration
func RegisterTLSHandlers() {
http.HandleFunc("/control/tls/status", postInstall(optionalAuth(ensureGET(handleTLSStatus))))
http.HandleFunc("/control/tls/configure", postInstall(optionalAuth(ensurePOST(handleTLSConfigure))))
http.HandleFunc("/control/tls/validate", postInstall(optionalAuth(ensurePOST(handleTLSValidate))))
}
func handleTLSStatus(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
marshalTLS(w, config.TLS)
}
func handleTLSValidate(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
data, err := unmarshalTLS(r)
if err != nil {
httpError(w, http.StatusBadRequest, "Failed to unmarshal TLS config: %s", err)
return
}
// check if port is available
// BUT: if we are already using this port, no need
alreadyRunning := false
if httpsServer.server != nil {
alreadyRunning = true
}
if !alreadyRunning {
err = checkPortAvailable(config.BindHost, data.PortHTTPS)
if err != nil {
httpError(w, http.StatusBadRequest, "port %d is not available, cannot enable HTTPS on it", data.PortHTTPS)
return
}
}
data.tlsConfigStatus = validateCertificates(data.CertificateChain, data.PrivateKey, data.ServerName)
marshalTLS(w, data)
}
func handleTLSConfigure(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
data, err := unmarshalTLS(r)
if err != nil {
httpError(w, http.StatusBadRequest, "Failed to unmarshal TLS config: %s", err)
return
}
// check if port is available
// BUT: if we are already using this port, no need
alreadyRunning := false
if httpsServer.server != nil {
alreadyRunning = true
}
if !alreadyRunning {
err = checkPortAvailable(config.BindHost, data.PortHTTPS)
if err != nil {
httpError(w, http.StatusBadRequest, "port %d is not available, cannot enable HTTPS on it", data.PortHTTPS)
return
}
}
restartHTTPS := false
data.tlsConfigStatus = validateCertificates(data.CertificateChain, data.PrivateKey, data.ServerName)
if !reflect.DeepEqual(config.TLS.tlsConfigSettings, data.tlsConfigSettings) {
log.Printf("tls config settings have changed, will restart HTTPS server")
restartHTTPS = true
}
config.TLS = data
err = writeAllConfigsAndReloadDNS()
if err != nil {
httpError(w, http.StatusInternalServerError, "Couldn't write config file: %s", err)
return
}
marshalTLS(w, data)
// this needs to be done in a goroutine because Shutdown() is a blocking call, and it will block
// until all requests are finished, and _we_ are inside a request right now, so it will block indefinitely
if restartHTTPS {
go func() {
time.Sleep(time.Second) // TODO: could not find a way to reliably know that data was fully sent to client by https server, so we wait a bit to let response through before closing the server
httpsServer.cond.L.Lock()
httpsServer.cond.Broadcast()
if httpsServer.server != nil {
httpsServer.server.Shutdown(context.TODO())
}
httpsServer.cond.L.Unlock()
}()
}
}
func verifyCertChain(data *tlsConfigStatus, certChain string, serverName string) error {
log.Tracef("got certificate: %s", certChain)
// now do a more extended validation
var certs []*pem.Block // PEM-encoded certificates
var skippedBytes []string // skipped bytes
pemblock := []byte(certChain)
for {
var decoded *pem.Block
decoded, pemblock = pem.Decode(pemblock)
if decoded == nil {
break
}
if decoded.Type == "CERTIFICATE" {
certs = append(certs, decoded)
} else {
skippedBytes = append(skippedBytes, decoded.Type)
}
}
var parsedCerts []*x509.Certificate
for _, cert := range certs {
parsed, err := x509.ParseCertificate(cert.Bytes)
if err != nil {
data.WarningValidation = fmt.Sprintf("Failed to parse certificate: %s", err)
return errors.New(data.WarningValidation)
}
parsedCerts = append(parsedCerts, parsed)
}
if len(parsedCerts) == 0 {
data.WarningValidation = fmt.Sprintf("You have specified an empty certificate")
return errors.New(data.WarningValidation)
}
data.ValidCert = true
// spew.Dump(parsedCerts)
opts := x509.VerifyOptions{
DNSName: serverName,
}
log.Printf("number of certs - %d", len(parsedCerts))
if len(parsedCerts) > 1 {
// set up an intermediate
pool := x509.NewCertPool()
for _, cert := range parsedCerts[1:] {
log.Printf("got an intermediate cert")
pool.AddCert(cert)
}
opts.Intermediates = pool
}
// TODO: save it as a warning rather than error it out -- shouldn't be a big problem
mainCert := parsedCerts[0]
_, err := mainCert.Verify(opts)
if err != nil {
// let self-signed certs through
data.WarningValidation = fmt.Sprintf("Your certificate does not verify: %s", err)
} else {
data.ValidChain = true
}
// spew.Dump(chains)
// update status
if mainCert != nil {
notAfter := mainCert.NotAfter
data.Subject = mainCert.Subject.String()
data.Issuer = mainCert.Issuer.String()
data.NotAfter = notAfter
data.NotBefore = mainCert.NotBefore
data.DNSNames = mainCert.DNSNames
}
return nil
}
func validatePkey(data *tlsConfigStatus, pkey string) error {
// now do a more extended validation
var key *pem.Block // PEM-encoded certificates
var skippedBytes []string // skipped bytes
// go through all pem blocks, but take first valid pem block and drop the rest
pemblock := []byte(pkey)
for {
var decoded *pem.Block
decoded, pemblock = pem.Decode(pemblock)
if decoded == nil {
break
}
if decoded.Type == "PRIVATE KEY" || strings.HasSuffix(decoded.Type, " PRIVATE KEY") {
key = decoded
break
} else {
skippedBytes = append(skippedBytes, decoded.Type)
}
}
if key == nil {
data.WarningValidation = "No valid keys were found"
return errors.New(data.WarningValidation)
}
// parse the decoded key
_, keytype, err := parsePrivateKey(key.Bytes)
if err != nil {
data.WarningValidation = fmt.Sprintf("Failed to parse private key: %s", err)
return errors.New(data.WarningValidation)
}
data.ValidKey = true
data.KeyType = keytype
return nil
}
// Process certificate data and its private key.
// All parameters are optional.
// On error, return partially set object
// with 'WarningValidation' field containing error description.
func validateCertificates(certChain, pkey, serverName string) tlsConfigStatus {
var data tlsConfigStatus
// check only public certificate separately from the key
if certChain != "" {
if verifyCertChain(&data, certChain, serverName) != nil {
return data
}
}
// validate private key (right now the only validation possible is just parsing it)
if pkey != "" {
if validatePkey(&data, pkey) != nil {
return data
}
}
// if both are set, validate both in unison
if pkey != "" && certChain != "" {
_, err := tls.X509KeyPair([]byte(certChain), []byte(pkey))
if err != nil {
data.WarningValidation = fmt.Sprintf("Invalid certificate or key: %s", err)
return data
}
data.ValidPair = true
}
return data
}
// Attempt to parse the given private key DER block. OpenSSL 0.9.8 generates
// PKCS#1 private keys by default, while OpenSSL 1.0.0 generates PKCS#8 keys.
// OpenSSL ecparam generates SEC1 EC private keys for ECDSA. We try all three.
func parsePrivateKey(der []byte) (crypto.PrivateKey, string, error) {
if key, err := x509.ParsePKCS1PrivateKey(der); err == nil {
return key, "RSA", nil
}
if key, err := x509.ParsePKCS8PrivateKey(der); err == nil {
switch key := key.(type) {
case *rsa.PrivateKey:
return key, "RSA", nil
case *ecdsa.PrivateKey:
return key, "ECDSA", nil
default:
return nil, "", errors.New("tls: found unknown private key type in PKCS#8 wrapping")
}
}
if key, err := x509.ParseECPrivateKey(der); err == nil {
return key, "ECDSA", nil
}
return nil, "", errors.New("tls: failed to parse private key")
}
// unmarshalTLS handles base64-encoded certificates transparently
func unmarshalTLS(r *http.Request) (tlsConfig, error) {
data := tlsConfig{}
err := json.NewDecoder(r.Body).Decode(&data)
if err != nil {
return data, errorx.Decorate(err, "Failed to parse new TLS config json")
}
if data.CertificateChain != "" {
certPEM, err := base64.StdEncoding.DecodeString(data.CertificateChain)
if err != nil {
return data, errorx.Decorate(err, "Failed to base64-decode certificate chain")
}
data.CertificateChain = string(certPEM)
}
if data.PrivateKey != "" {
keyPEM, err := base64.StdEncoding.DecodeString(data.PrivateKey)
if err != nil {
return data, errorx.Decorate(err, "Failed to base64-decode private key")
}
data.PrivateKey = string(keyPEM)
}
return data, nil
}
func marshalTLS(w http.ResponseWriter, data tlsConfig) {
w.Header().Set("Content-Type", "application/json")
if data.CertificateChain != "" {
encoded := base64.StdEncoding.EncodeToString([]byte(data.CertificateChain))
data.CertificateChain = encoded
}
if data.PrivateKey != "" {
encoded := base64.StdEncoding.EncodeToString([]byte(data.PrivateKey))
data.PrivateKey = encoded
}
err := json.NewEncoder(w).Encode(data)
if err != nil {
httpError(w, http.StatusInternalServerError, "Failed to marshal json with TLS status: %s", err)
return
}
}

22
dhcp.go
View File

@ -10,13 +10,14 @@ import (
"time" "time"
"github.com/AdguardTeam/AdGuardHome/dhcpd" "github.com/AdguardTeam/AdGuardHome/dhcpd"
"github.com/hmage/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/joomcode/errorx" "github.com/joomcode/errorx"
) )
var dhcpServer = dhcpd.Server{} var dhcpServer = dhcpd.Server{}
func handleDHCPStatus(w http.ResponseWriter, r *http.Request) { func handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
rawLeases := dhcpServer.Leases() rawLeases := dhcpServer.Leases()
leases := []map[string]string{} leases := []map[string]string{}
for i := range rawLeases { for i := range rawLeases {
@ -43,6 +44,7 @@ func handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
} }
func handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) { func handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
newconfig := dhcpd.ServerConfig{} newconfig := dhcpd.ServerConfig{}
err := json.NewDecoder(r.Body).Decode(&newconfig) err := json.NewDecoder(r.Body).Decode(&newconfig)
if err != nil { if err != nil {
@ -50,6 +52,11 @@ func handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
return return
} }
err = dhcpServer.Stop()
if err != nil {
log.Error("failed to stop the DHCP server: %s", err)
}
if newconfig.Enabled { if newconfig.Enabled {
err := dhcpServer.Start(&newconfig) err := dhcpServer.Start(&newconfig)
if err != nil { if err != nil {
@ -57,17 +64,13 @@ func handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
return return
} }
} }
if !newconfig.Enabled {
err := dhcpServer.Stop()
if err != nil {
log.Printf("failed to stop the DHCP server: %s", err)
}
}
config.DHCP = newconfig config.DHCP = newconfig
httpUpdateConfigReloadDNSReturnOK(w, r) httpUpdateConfigReloadDNSReturnOK(w, r)
} }
func handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) { func handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
response := map[string]interface{}{} response := map[string]interface{}{}
ifaces, err := getValidNetInterfaces() ifaces, err := getValidNetInterfaces()
@ -128,10 +131,11 @@ func handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
} }
func handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Request) { func handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
body, err := ioutil.ReadAll(r.Body) body, err := ioutil.ReadAll(r.Body)
if err != nil { if err != nil {
errorText := fmt.Sprintf("failed to read request body: %s", err) errorText := fmt.Sprintf("failed to read request body: %s", err)
log.Println(errorText) log.Error(errorText)
http.Error(w, errorText, http.StatusBadRequest) http.Error(w, errorText, http.StatusBadRequest)
return return
} }
@ -139,7 +143,7 @@ func handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Request) {
interfaceName := strings.TrimSpace(string(body)) interfaceName := strings.TrimSpace(string(body))
if interfaceName == "" { if interfaceName == "" {
errorText := fmt.Sprintf("empty interface name specified") errorText := fmt.Sprintf("empty interface name specified")
log.Println(errorText) log.Error(errorText)
http.Error(w, errorText, http.StatusBadRequest) http.Error(w, errorText, http.StatusBadRequest)
return return
} }

53
dhcpd/README.md Normal file
View File

@ -0,0 +1,53 @@
# DHCP server
Contents:
* [Test setup with Virtual Box](#vbox)
<a id="vbox"></a>
## Test setup with Virtual Box
To set up a test environment for DHCP server you need:
* Linux host machine
* Virtual Box
* Virtual machine (guest OS doesn't matter)
### Configure client
1. Install Virtual Box and run the following command to create a Host-Only network:
$ VBoxManage hostonlyif create
You can check its status by `ip a` command.
You can also set up Host-Only network using Virtual Box menu:
File -> Host Network Manager...
2. Create your virtual machine and set up its network:
VM Settings -> Network -> Host-only Adapter
3. Start your VM, install an OS. Configure your network interface to use DHCP and the OS should ask for a IP address from our DHCP server.
### Configure server
1. Edit server configuration file 'AdGuardHome.yaml', for example:
dhcp:
enabled: true
interface_name: vboxnet0
gateway_ip: 192.168.56.1
subnet_mask: 255.255.255.0
range_start: 192.168.56.2
range_end: 192.168.56.2
lease_duration: 86400
icmp_timeout_msec: 1000
2. Start the server
./AdGuardHome
There should be a message in log which shows that DHCP server is ready:
[info] DHCP: listening on 0.0.0.0:67

View File

@ -9,7 +9,7 @@ import (
"os" "os"
"time" "time"
"github.com/hmage/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/krolaw/dhcp4" "github.com/krolaw/dhcp4"
) )

98
dhcpd/db.go Normal file
View File

@ -0,0 +1,98 @@
// On-disk database for lease table
package dhcpd
import (
"encoding/json"
"io/ioutil"
"net"
"os"
"time"
"github.com/AdguardTeam/golibs/file"
"github.com/AdguardTeam/golibs/log"
"github.com/krolaw/dhcp4"
)
const dbFilename = "leases.db"
type leaseJSON struct {
HWAddr []byte `json:"mac"`
IP []byte `json:"ip"`
Hostname string `json:"host"`
Expiry int64 `json:"exp"`
}
// Load lease table from DB
func (s *Server) dbLoad() {
data, err := ioutil.ReadFile(dbFilename)
if err != nil {
if !os.IsNotExist(err) {
log.Error("DHCP: can't read file %s: %v", dbFilename, err)
}
return
}
obj := []leaseJSON{}
err = json.Unmarshal(data, &obj)
if err != nil {
log.Error("DHCP: invalid DB: %v", err)
return
}
s.leases = nil
s.IPpool = make(map[[4]byte]net.HardwareAddr)
numLeases := len(obj)
for i := range obj {
if !dhcp4.IPInRange(s.leaseStart, s.leaseStop, obj[i].IP) {
log.Tracef("Skipping a lease with IP %s: not within current IP range", obj[i].IP)
continue
}
lease := Lease{
HWAddr: obj[i].HWAddr,
IP: obj[i].IP,
Hostname: obj[i].Hostname,
Expiry: time.Unix(obj[i].Expiry, 0),
}
s.leases = append(s.leases, &lease)
s.reserveIP(lease.IP, lease.HWAddr)
}
log.Info("DHCP: loaded %d leases from DB", numLeases)
}
// Store lease table in DB
func (s *Server) dbStore() {
var leases []leaseJSON
for i := range s.leases {
if s.leases[i].Expiry.Unix() == 0 {
continue
}
lease := leaseJSON{
HWAddr: s.leases[i].HWAddr,
IP: s.leases[i].IP,
Hostname: s.leases[i].Hostname,
Expiry: s.leases[i].Expiry.Unix(),
}
leases = append(leases, lease)
}
data, err := json.Marshal(leases)
if err != nil {
log.Error("json.Marshal: %v", err)
return
}
err = file.SafeWrite(dbFilename, data)
if err != nil {
log.Error("DHCP: can't store lease table on disk: %v filename: %s",
err, dbFilename)
return
}
log.Info("DHCP: stored %d leases in DB", len(leases))
}

View File

@ -4,11 +4,13 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"net" "net"
"strings"
"sync" "sync"
"time" "time"
"github.com/hmage/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/krolaw/dhcp4" "github.com/krolaw/dhcp4"
ping "github.com/sparrc/go-ping"
) )
const defaultDiscoverTime = time.Second * 3 const defaultDiscoverTime = time.Second * 3
@ -32,6 +34,10 @@ type ServerConfig struct {
RangeStart string `json:"range_start" yaml:"range_start"` RangeStart string `json:"range_start" yaml:"range_start"`
RangeEnd string `json:"range_end" yaml:"range_end"` RangeEnd string `json:"range_end" yaml:"range_end"`
LeaseDuration uint `json:"lease_duration" yaml:"lease_duration"` // in seconds LeaseDuration uint `json:"lease_duration" yaml:"lease_duration"` // in seconds
// IP conflict detector: time (ms) to wait for ICMP reply.
// 0: disable
ICMPTimeout uint `json:"icmp_timeout_msec" yaml:"icmp_timeout_msec"`
} }
// Server - the current state of the DHCP server // Server - the current state of the DHCP server
@ -40,6 +46,11 @@ type Server struct {
ipnet *net.IPNet // if interface name changes, this needs to be reset ipnet *net.IPNet // if interface name changes, this needs to be reset
cond *sync.Cond // Synchronize worker thread with main thread
mutex sync.Mutex // Mutex for 'cond'
running bool // Set if the worker thread is running
stopping bool // Set if the worker thread should be stopped
// leases // leases
leases []*Lease leases []*Lease
leaseStart net.IP // parsed from config RangeStart leaseStart net.IP // parsed from config RangeStart
@ -54,6 +65,16 @@ type Server struct {
sync.RWMutex sync.RWMutex
} }
// Print information about the available network interfaces
func printInterfaces() {
ifaces, _ := net.Interfaces()
var buf strings.Builder
for i := range ifaces {
buf.WriteString(fmt.Sprintf("\"%s\", ", ifaces[i].Name))
}
log.Info("Available network interfaces: %s", buf.String())
}
// Start will listen on port 67 and serve DHCP requests. // Start will listen on port 67 and serve DHCP requests.
// Even though config can be nil, it is not optional (at least for now), since there are no default values (yet). // Even though config can be nil, it is not optional (at least for now), since there are no default values (yet).
func (s *Server) Start(config *ServerConfig) error { func (s *Server) Start(config *ServerConfig) error {
@ -64,6 +85,7 @@ func (s *Server) Start(config *ServerConfig) error {
iface, err := net.InterfaceByName(s.InterfaceName) iface, err := net.InterfaceByName(s.InterfaceName)
if err != nil { if err != nil {
s.closeConn() // in case it was already started s.closeConn() // in case it was already started
printInterfaces()
return wrapErrPrint(err, "Couldn't find interface by name %s", s.InterfaceName) return wrapErrPrint(err, "Couldn't find interface by name %s", s.InterfaceName)
} }
@ -122,20 +144,27 @@ func (s *Server) Start(config *ServerConfig) error {
s.closeConn() s.closeConn()
} }
s.dbLoad()
c, err := newFilterConn(*iface, ":67") // it has to be bound to 0.0.0.0:67, otherwise it won't see DHCP discover/request packets c, err := newFilterConn(*iface, ":67") // it has to be bound to 0.0.0.0:67, otherwise it won't see DHCP discover/request packets
if err != nil { if err != nil {
return wrapErrPrint(err, "Couldn't start listening socket on 0.0.0.0:67") return wrapErrPrint(err, "Couldn't start listening socket on 0.0.0.0:67")
} }
log.Info("DHCP: listening on 0.0.0.0:67")
s.conn = c s.conn = c
s.cond = sync.NewCond(&s.mutex)
s.running = true
go func() { go func() {
// operate on c instead of c.conn because c.conn can change over time // operate on c instead of c.conn because c.conn can change over time
err := dhcp4.Serve(c, s) err := dhcp4.Serve(c, s)
if err != nil { if err != nil && !s.stopping {
log.Printf("dhcp4.Serve() returned with error: %s", err) log.Printf("dhcp4.Serve() returned with error: %s", err)
} }
c.Close() // in case Serve() exits for other reason than listening socket closure c.Close() // in case Serve() exits for other reason than listening socket closure
s.running = false
s.cond.Signal()
}() }()
return nil return nil
@ -147,11 +176,23 @@ func (s *Server) Stop() error {
// nothing to do, return silently // nothing to do, return silently
return nil return nil
} }
s.stopping = true
err := s.closeConn() err := s.closeConn()
if err != nil { if err != nil {
return wrapErrPrint(err, "Couldn't close UDP listening socket") return wrapErrPrint(err, "Couldn't close UDP listening socket")
} }
// We've just closed the listening socket.
// Worker thread should exit right after it tries to read from the socket.
s.mutex.Lock()
for s.running {
s.cond.Wait()
}
s.mutex.Unlock()
s.dbStore()
return nil return nil
} }
@ -165,6 +206,7 @@ func (s *Server) closeConn() error {
return err return err
} }
// Reserve a lease for the client
func (s *Server) reserveLease(p dhcp4.Packet) (*Lease, error) { func (s *Server) reserveLease(p dhcp4.Packet) (*Lease, error) {
// WARNING: do not remove copy() // WARNING: do not remove copy()
// the given hwaddr by p.CHAddr() in the packet survives only during ServeDHCP() call // the given hwaddr by p.CHAddr() in the packet survives only during ServeDHCP() call
@ -172,27 +214,39 @@ func (s *Server) reserveLease(p dhcp4.Packet) (*Lease, error) {
hwaddrCOW := p.CHAddr() hwaddrCOW := p.CHAddr()
hwaddr := make(net.HardwareAddr, len(hwaddrCOW)) hwaddr := make(net.HardwareAddr, len(hwaddrCOW))
copy(hwaddr, hwaddrCOW) copy(hwaddr, hwaddrCOW)
foundLease := s.locateLease(p)
if foundLease != nil {
// log.Tracef("found lease for %s: %+v", hwaddr, foundLease)
return foundLease, nil
}
// not assigned a lease, create new one, find IP from LRU // not assigned a lease, create new one, find IP from LRU
hostname := p.ParseOptions()[dhcp4.OptionHostName]
lease := &Lease{HWAddr: hwaddr, Hostname: string(hostname)}
log.Tracef("Lease not found for %s: creating new one", hwaddr) log.Tracef("Lease not found for %s: creating new one", hwaddr)
ip, err := s.findFreeIP(hwaddr) ip, err := s.findFreeIP(hwaddr)
if err != nil { if err != nil {
i := s.findExpiredLease()
if i < 0 {
return nil, wrapErrPrint(err, "Couldn't find free IP for the lease %s", hwaddr.String()) return nil, wrapErrPrint(err, "Couldn't find free IP for the lease %s", hwaddr.String())
} }
log.Tracef("Assigning IP address %s to %s (lease for %s expired at %s)",
s.leases[i].IP, hwaddr, s.leases[i].HWAddr, s.leases[i].Expiry)
lease.IP = s.leases[i].IP
s.Lock()
s.leases[i] = lease
s.Unlock()
s.reserveIP(lease.IP, hwaddr)
return lease, nil
}
log.Tracef("Assigning to %s IP address %s", hwaddr, ip.String()) log.Tracef("Assigning to %s IP address %s", hwaddr, ip.String())
hostname := p.ParseOptions()[dhcp4.OptionHostName] lease.IP = ip
lease := &Lease{HWAddr: hwaddr, IP: ip, Hostname: string(hostname)}
s.Lock() s.Lock()
s.leases = append(s.leases, lease) s.leases = append(s.leases, lease)
s.Unlock() s.Unlock()
return lease, nil return lease, nil
} }
func (s *Server) locateLease(p dhcp4.Packet) *Lease { // Find a lease for the client
func (s *Server) findLease(p dhcp4.Packet) *Lease {
hwaddr := p.CHAddr() hwaddr := p.CHAddr()
for i := range s.leases { for i := range s.leases {
if bytes.Equal([]byte(hwaddr), []byte(s.leases[i].HWAddr)) { if bytes.Equal([]byte(hwaddr), []byte(s.leases[i].HWAddr)) {
@ -203,6 +257,17 @@ func (s *Server) locateLease(p dhcp4.Packet) *Lease {
return nil return nil
} }
// Find an expired lease and return its index or -1
func (s *Server) findExpiredLease() int {
now := time.Now().Unix()
for i, lease := range s.leases {
if lease.Expiry.Unix() <= now {
return i
}
}
return -1
}
func (s *Server) findFreeIP(hwaddr net.HardwareAddr) (net.IP, error) { func (s *Server) findFreeIP(hwaddr net.HardwareAddr) (net.IP, error) {
// if IP pool is nil, lazy initialize it // if IP pool is nil, lazy initialize it
if s.IPpool == nil { if s.IPpool == nil {
@ -213,13 +278,12 @@ func (s *Server) findFreeIP(hwaddr net.HardwareAddr) (net.IP, error) {
var foundIP net.IP var foundIP net.IP
for i := 0; i < dhcp4.IPRange(s.leaseStart, s.leaseStop); i++ { for i := 0; i < dhcp4.IPRange(s.leaseStart, s.leaseStop); i++ {
newIP := dhcp4.IPAdd(s.leaseStart, i) newIP := dhcp4.IPAdd(s.leaseStart, i)
foundHWaddr := s.getIPpool(newIP) foundHWaddr := s.findReservedHWaddr(newIP)
log.Tracef("tried IP %v, got hwaddr %v", newIP, foundHWaddr) log.Tracef("tried IP %v, got hwaddr %v", newIP, foundHWaddr)
if foundHWaddr != nil && len(foundHWaddr) != 0 { if foundHWaddr != nil && len(foundHWaddr) != 0 {
// if !bytes.Equal(foundHWaddr, hwaddr) { // if !bytes.Equal(foundHWaddr, hwaddr) {
// log.Tracef("SHOULD NOT HAPPEN: hwaddr in IP pool %s is not equal to hwaddr in lease %s", foundHWaddr, hwaddr) // log.Tracef("SHOULD NOT HAPPEN: hwaddr in IP pool %s is not equal to hwaddr in lease %s", foundHWaddr, hwaddr)
// } // }
log.Tracef("will try again")
continue continue
} }
foundIP = newIP foundIP = newIP
@ -236,7 +300,7 @@ func (s *Server) findFreeIP(hwaddr net.HardwareAddr) (net.IP, error) {
return foundIP, nil return foundIP, nil
} }
func (s *Server) getIPpool(ip net.IP) net.HardwareAddr { func (s *Server) findReservedHWaddr(ip net.IP) net.HardwareAddr {
rawIP := []byte(ip) rawIP := []byte(ip)
IP4 := [4]byte{rawIP[0], rawIP[1], rawIP[2], rawIP[3]} IP4 := [4]byte{rawIP[0], rawIP[1], rawIP[2], rawIP[3]}
return s.IPpool[IP4] return s.IPpool[IP4]
@ -256,133 +320,230 @@ func (s *Server) unreserveIP(ip net.IP) {
// ServeDHCP handles an incoming DHCP request // ServeDHCP handles an incoming DHCP request
func (s *Server) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options dhcp4.Options) dhcp4.Packet { func (s *Server) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options dhcp4.Options) dhcp4.Packet {
log.Tracef("Got %v message", msgType) s.printLeases()
log.Tracef("Leases:")
for i, lease := range s.leases {
log.Tracef("Lease #%d: hwaddr %s, ip %s, expiry %s", i, lease.HWAddr, lease.IP, lease.Expiry)
}
log.Tracef("IP pool:")
for ip, hwaddr := range s.IPpool {
log.Tracef("IP pool entry %s -> %s", net.IPv4(ip[0], ip[1], ip[2], ip[3]), hwaddr)
}
switch msgType { switch msgType {
case dhcp4.Discover: // Broadcast Packet From Client - Can I have an IP? case dhcp4.Discover: // Broadcast Packet From Client - Can I have an IP?
// find a lease, but don't update lease time return s.handleDiscover(p, options)
log.Tracef("Got from client: Discover")
lease, err := s.reserveLease(p)
if err != nil {
log.Tracef("Couldn't find free lease: %s", err)
// couldn't find lease, don't respond
return nil
}
reply := dhcp4.ReplyPacket(p, dhcp4.Offer, s.ipnet.IP, lease.IP, s.leaseTime, s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList]))
log.Tracef("Replying with offer: offered IP %v for %v with options %+v", lease.IP, s.leaseTime, reply.ParseOptions())
return reply
case dhcp4.Request: // Broadcast From Client - I'll take that IP (Also start for renewals) case dhcp4.Request: // Broadcast From Client - I'll take that IP (Also start for renewals)
// start/renew a lease -- update lease time // start/renew a lease -- update lease time
// some clients (OSX) just go right ahead and do Request first from previously known IP, if they get NAK, they restart full cycle with Discover then Request // some clients (OSX) just go right ahead and do Request first from previously known IP, if they get NAK, they restart full cycle with Discover then Request
return s.handleDHCP4Request(p, options) return s.handleDHCP4Request(p, options)
case dhcp4.Decline: // Broadcast From Client - Sorry I can't use that IP case dhcp4.Decline: // Broadcast From Client - Sorry I can't use that IP
log.Tracef("Got from client: Decline") return s.handleDecline(p, options)
case dhcp4.Release: // From Client, I don't need that IP anymore case dhcp4.Release: // From Client, I don't need that IP anymore
log.Tracef("Got from client: Release") return s.handleRelease(p, options)
case dhcp4.Inform: // From Client, I have this IP and there's nothing you can do about it case dhcp4.Inform: // From Client, I have this IP and there's nothing you can do about it
log.Tracef("Got from client: Inform") return s.handleInform(p, options)
// do nothing
// from server -- ignore those but enumerate just in case // from server -- ignore those but enumerate just in case
case dhcp4.Offer: // Broadcast From Server - Here's an IP case dhcp4.Offer: // Broadcast From Server - Here's an IP
log.Printf("SHOULD NOT HAPPEN -- FROM ANOTHER DHCP SERVER: Offer") log.Printf("DHCP: received message from %s: Offer", p.CHAddr())
case dhcp4.ACK: // From Server, Yes you can have that IP case dhcp4.ACK: // From Server, Yes you can have that IP
log.Printf("SHOULD NOT HAPPEN -- FROM ANOTHER DHCP SERVER: ACK") log.Printf("DHCP: received message from %s: ACK", p.CHAddr())
case dhcp4.NAK: // From Server, No you cannot have that IP case dhcp4.NAK: // From Server, No you cannot have that IP
log.Printf("SHOULD NOT HAPPEN -- FROM ANOTHER DHCP SERVER: NAK") log.Printf("DHCP: received message from %s: NAK", p.CHAddr())
default: default:
log.Printf("Unknown DHCP packet detected, ignoring: %v", msgType) log.Printf("DHCP: unknown packet %v from %s", msgType, p.CHAddr())
return nil return nil
} }
return nil return nil
} }
// Send ICMP to the specified machine
// Return TRUE if it doesn't reply, which probably means that the IP is available
func (s *Server) addrAvailable(target net.IP) bool {
if s.ICMPTimeout == 0 {
return true
}
pinger, err := ping.NewPinger(target.String())
if err != nil {
log.Error("ping.NewPinger(): %v", err)
return true
}
pinger.SetPrivileged(true)
pinger.Timeout = time.Duration(s.ICMPTimeout) * time.Millisecond
pinger.Count = 1
reply := false
pinger.OnRecv = func(pkt *ping.Packet) {
// log.Tracef("Received ICMP Reply from %v", target)
reply = true
}
log.Tracef("Sending ICMP Echo to %v", target)
pinger.Run()
if reply {
log.Info("DHCP: IP conflict: %v is already used by another device", target)
return false
}
log.Tracef("ICMP procedure is complete: %v", target)
return true
}
// Add the specified IP to the black list for a time period
func (s *Server) blacklistLease(lease *Lease) {
hw := make(net.HardwareAddr, 6)
s.reserveIP(lease.IP, hw)
s.Lock()
lease.HWAddr = hw
lease.Hostname = ""
lease.Expiry = time.Now().Add(s.leaseTime)
s.Unlock()
}
// Return TRUE if DHCP packet is correct
func isValidPacket(p dhcp4.Packet) bool {
hw := p.CHAddr()
zeroes := make([]byte, len(hw))
if bytes.Equal(hw, zeroes) {
log.Tracef("Packet has empty CHAddr")
return false
}
return true
}
func (s *Server) handleDiscover(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet {
// find a lease, but don't update lease time
var lease *Lease
var err error
reqIP := net.IP(options[dhcp4.OptionRequestedIPAddress])
hostname := p.ParseOptions()[dhcp4.OptionHostName]
log.Tracef("Message from client: Discover. ReqIP: %s HW: %s Hostname: %s",
reqIP, p.CHAddr(), hostname)
if !isValidPacket(p) {
return nil
}
lease = s.findLease(p)
for lease == nil {
lease, err = s.reserveLease(p)
if err != nil {
log.Error("Couldn't find free lease: %s", err)
return nil
}
if !s.addrAvailable(lease.IP) {
s.blacklistLease(lease)
lease = nil
continue
}
break
}
opt := s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList])
reply := dhcp4.ReplyPacket(p, dhcp4.Offer, s.ipnet.IP, lease.IP, s.leaseTime, opt)
log.Tracef("Replying with offer: offered IP %v for %v with options %+v", lease.IP, s.leaseTime, reply.ParseOptions())
return reply
}
func (s *Server) handleDHCP4Request(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet { func (s *Server) handleDHCP4Request(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet {
log.Tracef("Got from client: Request") var lease *Lease
if server, ok := options[dhcp4.OptionServerIdentifier]; ok && !net.IP(server).Equal(s.ipnet.IP) {
reqIP := net.IP(options[dhcp4.OptionRequestedIPAddress])
log.Tracef("Message from client: Request. IP: %s ReqIP: %s HW: %s",
p.CIAddr(), reqIP, p.CHAddr())
if !isValidPacket(p) {
return nil
}
server := options[dhcp4.OptionServerIdentifier]
if server != nil && !net.IP(server).Equal(s.ipnet.IP) {
log.Tracef("Request message not for this DHCP server (%v vs %v)", server, s.ipnet.IP) log.Tracef("Request message not for this DHCP server (%v vs %v)", server, s.ipnet.IP)
return nil // Message not for this dhcp server return nil // Message not for this dhcp server
} }
reqIP := net.IP(options[dhcp4.OptionRequestedIPAddress])
if reqIP == nil { if reqIP == nil {
reqIP = p.CIAddr() reqIP = p.CIAddr()
}
if reqIP.To4() == nil { } else if reqIP == nil || reqIP.To4() == nil {
log.Tracef("Replying with NAK: request IP isn't valid IPv4: %s", reqIP) log.Tracef("Requested IP isn't a valid IPv4: %s", reqIP)
return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil) return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil)
} }
if reqIP.Equal(net.IPv4zero) { lease = s.findLease(p)
log.Tracef("Replying with NAK: request IP is 0.0.0.0") if lease == nil {
log.Tracef("Lease for %s isn't found", p.CHAddr())
return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil) return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil)
} }
log.Tracef("requested IP is %s", reqIP) if !lease.IP.Equal(reqIP) {
lease, err := s.reserveLease(p) log.Tracef("Lease for %s doesn't match requested/client IP: %s vs %s",
if err != nil { lease.HWAddr, lease.IP, reqIP)
log.Tracef("Couldn't find free lease: %s", err) return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil)
// couldn't find lease, don't respond
return nil
} }
if lease.IP.Equal(reqIP) {
// IP matches lease IP, nothing else to do
lease.Expiry = time.Now().Add(s.leaseTime) lease.Expiry = time.Now().Add(s.leaseTime)
log.Tracef("Replying with ACK: request IP matches lease IP, nothing else to do. IP %v for %v", lease.IP, p.CHAddr()) log.Tracef("Replying with ACK. IP: %s HW: %s Expire: %s",
return dhcp4.ReplyPacket(p, dhcp4.ACK, s.ipnet.IP, lease.IP, s.leaseTime, s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList])) lease.IP, lease.HWAddr, lease.Expiry)
} opt := s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList])
return dhcp4.ReplyPacket(p, dhcp4.ACK, s.ipnet.IP, lease.IP, s.leaseTime, opt)
//
// requested IP different from lease
//
log.Tracef("lease IP is different from requested IP: %s vs %s", lease.IP, reqIP)
hwaddr := s.getIPpool(reqIP)
if hwaddr == nil {
// not in pool, check if it's in DHCP range
if dhcp4.IPInRange(s.leaseStart, s.leaseStop, reqIP) {
// okay, we can give it to our client -- it's in our DHCP range and not taken, so let them use their IP
log.Tracef("Replying with ACK: request IP %v is not taken, so assigning lease IP %v to it, for %v", reqIP, lease.IP, p.CHAddr())
s.unreserveIP(lease.IP)
lease.IP = reqIP
s.reserveIP(reqIP, p.CHAddr())
lease.Expiry = time.Now().Add(s.leaseTime)
return dhcp4.ReplyPacket(p, dhcp4.ACK, s.ipnet.IP, lease.IP, s.leaseTime, s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList]))
}
}
if hwaddr != nil && !bytes.Equal(hwaddr, lease.HWAddr) {
log.Printf("SHOULD NOT HAPPEN: IP pool hwaddr does not match lease hwaddr: %s vs %s", hwaddr, lease.HWAddr)
}
// requsted IP is not sufficient, reply with NAK
if hwaddr != nil {
log.Tracef("Replying with NAK: request IP %s is taken, asked by %v", reqIP, p.CHAddr())
return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil)
}
// requested IP is outside of DHCP range
log.Tracef("Replying with NAK: request IP %s is outside of DHCP range [%s, %s], asked by %v", reqIP, s.leaseStart, s.leaseStop, p.CHAddr())
return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil)
} }
// Leases returns the list of current DHCP leases func (s *Server) handleInform(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet {
func (s *Server) Leases() []*Lease { log.Tracef("Message from client: Inform. IP: %s HW: %s",
p.CIAddr(), p.CHAddr())
return nil
}
func (s *Server) handleRelease(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet {
log.Tracef("Message from client: Release. IP: %s HW: %s",
p.CIAddr(), p.CHAddr())
return nil
}
func (s *Server) handleDecline(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet {
reqIP := net.IP(options[dhcp4.OptionRequestedIPAddress])
log.Tracef("Message from client: Decline. IP: %s HW: %s",
reqIP, p.CHAddr())
return nil
}
// Leases returns the list of current DHCP leases (thread-safe)
func (s *Server) Leases() []Lease {
var result []Lease
now := time.Now().Unix()
s.RLock() s.RLock()
result := s.leases for _, lease := range s.leases {
if lease.Expiry.Unix() > now {
result = append(result, *lease)
}
}
s.RUnlock() s.RUnlock()
return result return result
} }
// Print information about the current leases
func (s *Server) printLeases() {
log.Tracef("Leases:")
for i, lease := range s.leases {
log.Tracef("Lease #%d: hwaddr %s, ip %s, expiry %s",
i, lease.HWAddr, lease.IP, lease.Expiry)
}
}
// Reset internal state
func (s *Server) reset() {
s.Lock()
s.leases = nil
s.Unlock()
s.IPpool = make(map[[4]byte]net.HardwareAddr)
}

170
dhcpd/dhcpd_test.go Normal file
View File

@ -0,0 +1,170 @@
package dhcpd
import (
"bytes"
"net"
"os"
"testing"
"time"
"github.com/krolaw/dhcp4"
)
func check(t *testing.T, result bool, msg string) {
if !result {
t.Fatal(msg)
}
}
// Tests performed:
// . Handle Discover message (lease reserve)
// . Handle Request message (lease commit)
func TestDHCP(t *testing.T) {
var s = Server{}
var p, p2 dhcp4.Packet
var hw net.HardwareAddr
var lease *Lease
var opt dhcp4.Options
s.leaseStart = []byte{1, 1, 1, 1}
s.leaseStop = []byte{1, 1, 1, 2}
s.leaseTime = 5 * time.Second
s.leaseOptions = dhcp4.Options{}
s.ipnet = &net.IPNet{
IP: []byte{1, 2, 3, 4},
Mask: []byte{0xff, 0xff, 0xff, 0xff},
}
p = make(dhcp4.Packet, 241)
// Reserve an IP
hw = []byte{3, 2, 3, 4, 5, 6}
p.SetCHAddr(hw)
lease, _ = s.reserveLease(p)
check(t, bytes.Equal(lease.HWAddr, hw), "lease.HWAddr")
check(t, bytes.Equal(lease.IP, []byte{1, 1, 1, 1}), "lease.IP")
lease = s.findLease(p)
check(t, bytes.Equal(lease.HWAddr, hw), "lease.HWAddr")
check(t, bytes.Equal(lease.IP, []byte{1, 1, 1, 1}), "lease.IP")
// Reserve an IP - the next IP from the range
hw = []byte{2, 2, 3, 4, 5, 6}
p.SetCHAddr(hw)
lease, _ = s.reserveLease(p)
check(t, bytes.Equal(lease.HWAddr, hw), "lease.HWAddr")
check(t, bytes.Equal(lease.IP, []byte{1, 1, 1, 2}), "lease.IP")
// Reserve an IP - we have no more available IPs,
// so the first expired (or, in our case, not yet committed) lease is returned
hw = []byte{1, 2, 3, 4, 5, 6}
p.SetCHAddr(hw)
lease, _ = s.reserveLease(p)
check(t, bytes.Equal(lease.HWAddr, hw), "lease.HWAddr")
check(t, bytes.Equal(lease.IP, []byte{1, 1, 1, 1}), "lease.IP")
// Decline request for a lease which doesn't match our internal state
hw = []byte{1, 2, 3, 4, 5, 6}
p.SetCHAddr(hw)
p.SetCIAddr([]byte{0, 0, 0, 0})
opt = make(dhcp4.Options, 10)
// ask a different IP
opt[dhcp4.OptionRequestedIPAddress] = []byte{1, 1, 1, 2}
p2 = s.handleDHCP4Request(p, opt)
opt = p2.ParseOptions()
check(t, bytes.Equal(opt[dhcp4.OptionDHCPMessageType], []byte{byte(dhcp4.NAK)}), "dhcp4.NAK")
// Commit the previously reserved lease
hw = []byte{1, 2, 3, 4, 5, 6}
p.SetCHAddr(hw)
p.SetCIAddr([]byte{0, 0, 0, 0})
opt = make(dhcp4.Options, 10)
opt[dhcp4.OptionRequestedIPAddress] = []byte{1, 1, 1, 1}
p2 = s.handleDHCP4Request(p, opt)
opt = p2.ParseOptions()
check(t, bytes.Equal(opt[dhcp4.OptionDHCPMessageType], []byte{byte(dhcp4.ACK)}), "dhcp4.ACK")
check(t, bytes.Equal(p2.YIAddr(), []byte{1, 1, 1, 1}), "p2.YIAddr")
check(t, bytes.Equal(p2.CHAddr(), hw), "p2.CHAddr")
check(t, bytes.Equal(opt[dhcp4.OptionIPAddressLeaseTime], dhcp4.OptionsLeaseTime(5*time.Second)), "OptionIPAddressLeaseTime")
check(t, bytes.Equal(opt[dhcp4.OptionServerIdentifier], s.ipnet.IP), "OptionServerIdentifier")
// Commit the previously reserved lease #2
hw = []byte{2, 2, 3, 4, 5, 6}
p.SetCHAddr(hw)
p.SetCIAddr([]byte{0, 0, 0, 0})
opt = make(dhcp4.Options, 10)
opt[dhcp4.OptionRequestedIPAddress] = []byte{1, 1, 1, 2}
p2 = s.handleDHCP4Request(p, opt)
check(t, bytes.Equal(p2.YIAddr(), []byte{1, 1, 1, 2}), "p2.YIAddr")
// Reserve an IP - we have no more available IPs
hw = []byte{3, 2, 3, 4, 5, 6}
p.SetCHAddr(hw)
lease, _ = s.reserveLease(p)
check(t, lease == nil, "lease == nil")
s.reset()
misc(t, &s)
}
// Small tests that don't require a static server's state
func misc(t *testing.T, s *Server) {
var p, p2 dhcp4.Packet
var hw net.HardwareAddr
var opt dhcp4.Options
p = make(dhcp4.Packet, 241)
// Try to commit a lease for an IP without prior Discover-Offer packets
hw = []byte{2, 2, 3, 4, 5, 6}
p.SetCHAddr(hw)
p.SetCIAddr([]byte{0, 0, 0, 0})
opt = make(dhcp4.Options, 10)
opt[dhcp4.OptionRequestedIPAddress] = []byte{1, 1, 1, 1}
p2 = s.handleDHCP4Request(p, opt)
opt = p2.ParseOptions()
check(t, bytes.Equal(opt[dhcp4.OptionDHCPMessageType], []byte{byte(dhcp4.NAK)}), "dhcp4.NAK")
}
// Leases database store/load
func TestDB(t *testing.T) {
var s = Server{}
var p dhcp4.Packet
var hw1, hw2 net.HardwareAddr
var lease *Lease
s.leaseStart = []byte{1, 1, 1, 1}
s.leaseStop = []byte{1, 1, 1, 2}
s.leaseTime = 5 * time.Second
s.leaseOptions = dhcp4.Options{}
s.ipnet = &net.IPNet{
IP: []byte{1, 2, 3, 4},
Mask: []byte{0xff, 0xff, 0xff, 0xff},
}
p = make(dhcp4.Packet, 241)
hw1 = []byte{1, 2, 3, 4, 5, 6}
p.SetCHAddr(hw1)
lease, _ = s.reserveLease(p)
lease.Expiry = time.Unix(4000000001, 0)
hw2 = []byte{2, 2, 3, 4, 5, 6}
p.SetCHAddr(hw2)
lease, _ = s.reserveLease(p)
lease.Expiry = time.Unix(4000000002, 0)
os.Remove("leases.db")
s.dbStore()
s.reset()
s.dbLoad()
check(t, bytes.Equal(s.leases[0].HWAddr, hw1), "leases[0].HWAddr")
check(t, bytes.Equal(s.leases[0].IP, []byte{1, 1, 1, 1}), "leases[0].IP")
check(t, s.leases[0].Expiry.Unix() == 4000000001, "leases[0].Expiry")
check(t, bytes.Equal(s.leases[1].HWAddr, hw2), "leases[1].HWAddr")
check(t, bytes.Equal(s.leases[1].IP, []byte{1, 1, 1, 2}), "leases[1].IP")
check(t, s.leases[1].Expiry.Unix() == 4000000002, "leases[1].Expiry")
os.Remove("leases.db")
}

View File

@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"net" "net"
"github.com/hmage/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/joomcode/errorx" "github.com/joomcode/errorx"
) )

View File

@ -8,7 +8,7 @@ import (
"time" "time"
"github.com/AdguardTeam/AdGuardHome/dhcpd" "github.com/AdguardTeam/AdGuardHome/dhcpd"
"github.com/hmage/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/krolaw/dhcp4" "github.com/krolaw/dhcp4"
) )

20
dns.go
View File

@ -7,8 +7,8 @@ import (
"github.com/AdguardTeam/AdGuardHome/dnsfilter" "github.com/AdguardTeam/AdGuardHome/dnsfilter"
"github.com/AdguardTeam/AdGuardHome/dnsforward" "github.com/AdguardTeam/AdGuardHome/dnsforward"
"github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/dnsproxy/proxy"
"github.com/hmage/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/joomcode/errorx" "github.com/joomcode/errorx"
) )
@ -58,19 +58,13 @@ func generateServerConfig() dnsforward.ServerConfig {
} }
} }
for _, u := range config.DNS.UpstreamDNS { upstreamConfig, err := proxy.ParseUpstreamsConfig(config.DNS.UpstreamDNS, config.DNS.BootstrapDNS, dnsforward.DefaultTimeout)
opts := upstream.Options{
Timeout: dnsforward.DefaultTimeout,
Bootstrap: []string{config.DNS.BootstrapDNS},
}
dnsUpstream, err := upstream.AddressToUpstream(u, opts)
if err != nil { if err != nil {
log.Printf("Couldn't get upstream: %s", err) log.Error("Couldn't get upstreams configuration cause: %s", err)
// continue, just ignore the upstream
continue
}
newconfig.Upstreams = append(newconfig.Upstreams, dnsUpstream)
} }
newconfig.Upstreams = upstreamConfig.Upstreams
newconfig.DomainsReservedUpstreams = upstreamConfig.DomainReservedUpstreams
newconfig.AllServers = config.DNS.AllServers
return newconfig return newconfig
} }

View File

@ -16,8 +16,8 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/AdguardTeam/golibs/log"
"github.com/bluele/gcache" "github.com/bluele/gcache"
"github.com/hmage/golibs/log"
"golang.org/x/net/publicsuffix" "golang.org/x/net/publicsuffix"
) )
@ -600,6 +600,11 @@ func hostnameToHashParam(host string, addslash bool) (string, map[string]bool) {
} }
func (d *Dnsfilter) checkSafeSearch(host string) (Result, error) { func (d *Dnsfilter) checkSafeSearch(host string) (Result, error) {
if log.GetLevel() >= log.DEBUG {
timer := log.StartTimer()
defer timer.LogElapsed("SafeSearch HTTP lookup for %s", host)
}
if safeSearchCache == nil { if safeSearchCache == nil {
safeSearchCache = gcache.New(defaultCacheSize).LRU().Expiration(defaultCacheTime).Build() safeSearchCache = gcache.New(defaultCacheSize).LRU().Expiration(defaultCacheTime).Build()
} }
@ -608,6 +613,7 @@ func (d *Dnsfilter) checkSafeSearch(host string) (Result, error) {
cachedValue, isFound, err := getCachedReason(safeSearchCache, host) cachedValue, isFound, err := getCachedReason(safeSearchCache, host)
if isFound { if isFound {
atomic.AddUint64(&stats.Safesearch.CacheHits, 1) atomic.AddUint64(&stats.Safesearch.CacheHits, 1)
log.Tracef("%s: found in SafeSearch cache", host)
return cachedValue, nil return cachedValue, nil
} }
@ -656,6 +662,11 @@ func (d *Dnsfilter) checkSafeSearch(host string) (Result, error) {
} }
func (d *Dnsfilter) checkSafeBrowsing(host string) (Result, error) { func (d *Dnsfilter) checkSafeBrowsing(host string) (Result, error) {
if log.GetLevel() >= log.DEBUG {
timer := log.StartTimer()
defer timer.LogElapsed("SafeBrowsing HTTP lookup for %s", host)
}
// prevent recursion -- checking the host of safebrowsing server makes no sense // prevent recursion -- checking the host of safebrowsing server makes no sense
if host == d.safeBrowsingServer { if host == d.safeBrowsingServer {
return Result{}, nil return Result{}, nil
@ -697,6 +708,11 @@ func (d *Dnsfilter) checkSafeBrowsing(host string) (Result, error) {
} }
func (d *Dnsfilter) checkParental(host string) (Result, error) { func (d *Dnsfilter) checkParental(host string) (Result, error) {
if log.GetLevel() >= log.DEBUG {
timer := log.StartTimer()
defer timer.LogElapsed("Parental HTTP lookup for %s", host)
}
// prevent recursion -- checking the host of parental safety server makes no sense // prevent recursion -- checking the host of parental safety server makes no sense
if host == d.parentalServer { if host == d.parentalServer {
return Result{}, nil return Result{}, nil
@ -754,6 +770,7 @@ func (d *Dnsfilter) lookupCommon(host string, lookupstats *LookupStats, cache gc
cachedValue, isFound, err := getCachedReason(cache, host) cachedValue, isFound, err := getCachedReason(cache, host)
if isFound { if isFound {
atomic.AddUint64(&lookupstats.CacheHits, 1) atomic.AddUint64(&lookupstats.CacheHits, 1)
log.Tracef("%s: found in the lookup cache", host)
return cachedValue, nil return cachedValue, nil
} }
if err != nil { if err != nil {

View File

@ -18,7 +18,7 @@ import (
"os" "os"
"runtime" "runtime"
"github.com/hmage/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/shirou/gopsutil/process" "github.com/shirou/gopsutil/process"
"go.uber.org/goleak" "go.uber.org/goleak"
) )

View File

@ -13,7 +13,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/dnsfilter" "github.com/AdguardTeam/AdGuardHome/dnsfilter"
"github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/dnsproxy/upstream"
"github.com/hmage/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/joomcode/errorx" "github.com/joomcode/errorx"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
@ -62,11 +62,12 @@ type FilteringConfig struct {
ProtectionEnabled bool `yaml:"protection_enabled"` // whether or not use any of dnsfilter features ProtectionEnabled bool `yaml:"protection_enabled"` // whether or not use any of dnsfilter features
FilteringEnabled bool `yaml:"filtering_enabled"` // whether or not use filter lists FilteringEnabled bool `yaml:"filtering_enabled"` // whether or not use filter lists
BlockedResponseTTL uint32 `yaml:"blocked_response_ttl"` // if 0, then default is used (3600) BlockedResponseTTL uint32 `yaml:"blocked_response_ttl"` // if 0, then default is used (3600)
QueryLogEnabled bool `yaml:"querylog_enabled"` QueryLogEnabled bool `yaml:"querylog_enabled"` // if true, query log is enabled
Ratelimit int `yaml:"ratelimit"` Ratelimit int `yaml:"ratelimit"` // max number of requests per second from a given IP (0 to disable)
RatelimitWhitelist []string `yaml:"ratelimit_whitelist"` RatelimitWhitelist []string `yaml:"ratelimit_whitelist"` // a list of whitelisted client IP addresses
RefuseAny bool `yaml:"refuse_any"` RefuseAny bool `yaml:"refuse_any"` // if true, refuse ANY requests
BootstrapDNS string `yaml:"bootstrap_dns"` BootstrapDNS []string `yaml:"bootstrap_dns"` // a list of bootstrap DNS for DoH and DoT (plain DNS only)
AllServers bool `yaml:"all_servers"` // if true, parallel queries to all configured upstream servers are enabled
dnsfilter.Config `yaml:",inline"` dnsfilter.Config `yaml:",inline"`
} }
@ -84,6 +85,7 @@ type ServerConfig struct {
UDPListenAddr *net.UDPAddr // UDP listen address UDPListenAddr *net.UDPAddr // UDP listen address
TCPListenAddr *net.TCPAddr // TCP listen address TCPListenAddr *net.TCPAddr // TCP listen address
Upstreams []upstream.Upstream // Configured upstreams Upstreams []upstream.Upstream // Configured upstreams
DomainsReservedUpstreams map[string][]upstream.Upstream // Map of domains and lists of configured upstreams
Filters []dnsfilter.Filter // A list of filters to use Filters []dnsfilter.Filter // A list of filters to use
FilteringConfig FilteringConfig
@ -162,7 +164,9 @@ func (s *Server) startInternal(config *ServerConfig) error {
RefuseAny: s.RefuseAny, RefuseAny: s.RefuseAny,
CacheEnabled: true, CacheEnabled: true,
Upstreams: s.Upstreams, Upstreams: s.Upstreams,
DomainsReservedUpstreams: s.DomainsReservedUpstreams,
Handler: s.handleDNSRequest, Handler: s.handleDNSRequest,
AllServers: s.AllServers,
} }
if s.TLSListenAddr != nil && s.CertificateChain != "" && s.PrivateKey != "" { if s.TLSListenAddr != nil && s.CertificateChain != "" && s.PrivateKey != "" {

View File

@ -10,7 +10,7 @@ import (
"time" "time"
"github.com/AdguardTeam/AdGuardHome/dnsfilter" "github.com/AdguardTeam/AdGuardHome/dnsfilter"
"github.com/hmage/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/miekg/dns" "github.com/miekg/dns"
) )

View File

@ -9,8 +9,8 @@ import (
"sync" "sync"
"time" "time"
"github.com/AdguardTeam/golibs/log"
"github.com/go-test/deep" "github.com/go-test/deep"
"github.com/hmage/golibs/log"
) )
var ( var (
@ -28,7 +28,7 @@ func (l *queryLog) flushLogBuffer() error {
l.logBufferLock.Unlock() l.logBufferLock.Unlock()
err := l.flushToFile(flushBuffer) err := l.flushToFile(flushBuffer)
if err != nil { if err != nil {
log.Printf("Saving querylog to file failed: %s", err) log.Error("Saving querylog to file failed: %s", err)
return err return err
} }
return nil return nil
@ -46,17 +46,17 @@ func (l *queryLog) flushToFile(buffer []*logEntry) error {
for _, entry := range buffer { for _, entry := range buffer {
err := e.Encode(entry) err := e.Encode(entry)
if err != nil { if err != nil {
log.Printf("Failed to marshal entry: %s", err) log.Error("Failed to marshal entry: %s", err)
return err return err
} }
} }
elapsed := time.Since(start) elapsed := time.Since(start)
log.Printf("%d elements serialized via json in %v: %d kB, %v/entry, %v/entry", len(buffer), elapsed, b.Len()/1024, float64(b.Len())/float64(len(buffer)), elapsed/time.Duration(len(buffer))) log.Debug("%d elements serialized via json in %v: %d kB, %v/entry, %v/entry", len(buffer), elapsed, b.Len()/1024, float64(b.Len())/float64(len(buffer)), elapsed/time.Duration(len(buffer)))
err := checkBuffer(buffer, b) err := checkBuffer(buffer, b)
if err != nil { if err != nil {
log.Printf("failed to check buffer: %s", err) log.Error("failed to check buffer: %s", err)
return err return err
} }
@ -73,13 +73,13 @@ func (l *queryLog) flushToFile(buffer []*logEntry) error {
_, err = zw.Write(b.Bytes()) _, err = zw.Write(b.Bytes())
if err != nil { if err != nil {
log.Printf("Couldn't compress to gzip: %s", err) log.Error("Couldn't compress to gzip: %s", err)
zw.Close() zw.Close()
return err return err
} }
if err = zw.Close(); err != nil { if err = zw.Close(); err != nil {
log.Printf("Couldn't close gzip writer: %s", err) log.Error("Couldn't close gzip writer: %s", err)
return err return err
} }
} else { } else {
@ -90,18 +90,18 @@ func (l *queryLog) flushToFile(buffer []*logEntry) error {
defer fileWriteLock.Unlock() defer fileWriteLock.Unlock()
f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil { if err != nil {
log.Printf("failed to create file \"%s\": %s", filename, err) log.Error("failed to create file \"%s\": %s", filename, err)
return err return err
} }
defer f.Close() defer f.Close()
n, err := f.Write(zb.Bytes()) n, err := f.Write(zb.Bytes())
if err != nil { if err != nil {
log.Printf("Couldn't write to file: %s", err) log.Error("Couldn't write to file: %s", err)
return err return err
} }
log.Printf("ok \"%s\": %v bytes written", filename, n) log.Debug("ok \"%s\": %v bytes written", filename, n)
return nil return nil
} }
@ -115,21 +115,21 @@ func checkBuffer(buffer []*logEntry, b bytes.Buffer) error {
entry := &logEntry{} entry := &logEntry{}
err := d.Decode(entry) err := d.Decode(entry)
if err != nil { if err != nil {
log.Printf("Failed to decode: %s", err) log.Error("Failed to decode: %s", err)
return err return err
} }
if diff := deep.Equal(entry, buffer[i]); diff != nil { if diff := deep.Equal(entry, buffer[i]); diff != nil {
log.Printf("decoded buffer differs: %s", diff) log.Error("decoded buffer differs: %s", diff)
return fmt.Errorf("decoded buffer differs: %s", diff) return fmt.Errorf("decoded buffer differs: %s", diff)
} }
i++ i++
} }
if i != l { if i != l {
err := fmt.Errorf("check fail: %d vs %d entries", l, i) err := fmt.Errorf("check fail: %d vs %d entries", l, i)
log.Print(err) log.Error("%v", err)
return err return err
} }
log.Printf("check ok: %d entries", i) log.Debug("check ok: %d entries", i)
return nil return nil
} }
@ -150,11 +150,11 @@ func (l *queryLog) rotateQueryLog() error {
err := os.Rename(from, to) err := os.Rename(from, to)
if err != nil { if err != nil {
log.Printf("Failed to rename querylog: %s", err) log.Error("Failed to rename querylog: %s", err)
return err return err
} }
log.Printf("Rotated from %s to %s successfully", from, to) log.Debug("Rotated from %s to %s successfully", from, to)
return nil return nil
} }
@ -163,7 +163,7 @@ func (l *queryLog) periodicQueryLogRotate() {
for range time.Tick(queryLogRotationPeriod) { for range time.Tick(queryLogRotationPeriod) {
err := l.rotateQueryLog() err := l.rotateQueryLog()
if err != nil { if err != nil {
log.Printf("Failed to rotate querylog: %s", err) log.Error("Failed to rotate querylog: %s", err)
// do nothing, continue rotating // do nothing, continue rotating
} }
} }
@ -198,7 +198,7 @@ func (l *queryLog) genericLoader(onEntry func(entry *logEntry) error, needMore f
f, err := os.Open(file) f, err := os.Open(file)
if err != nil { if err != nil {
log.Printf("Failed to open file \"%s\": %s", file, err) log.Error("Failed to open file \"%s\": %s", file, err)
// try next file // try next file
continue continue
} }
@ -209,7 +209,7 @@ func (l *queryLog) genericLoader(onEntry func(entry *logEntry) error, needMore f
if enableGzip { if enableGzip {
zr, err := gzip.NewReader(f) zr, err := gzip.NewReader(f)
if err != nil { if err != nil {
log.Printf("Failed to create gzip reader: %s", err) log.Error("Failed to create gzip reader: %s", err)
continue continue
} }
defer zr.Close() defer zr.Close()
@ -231,7 +231,7 @@ func (l *queryLog) genericLoader(onEntry func(entry *logEntry) error, needMore f
var entry logEntry var entry logEntry
err := d.Decode(&entry) err := d.Decode(&entry)
if err != nil { if err != nil {
log.Printf("Failed to decode: %s", err) log.Error("Failed to decode: %s", err)
// next entry can be fine, try more // next entry can be fine, try more
continue continue
} }
@ -260,7 +260,7 @@ func (l *queryLog) genericLoader(onEntry func(entry *logEntry) error, needMore f
perunit = elapsed / time.Duration(i) perunit = elapsed / time.Duration(i)
avg = sum / time.Duration(i) avg = sum / time.Duration(i)
} }
log.Printf("file \"%s\": read %d entries in %v, %v/entry, %v over %v, %v avg", file, i, elapsed, perunit, over, max, avg) log.Debug("file \"%s\": read %d entries in %v, %v/entry, %v over %v, %v avg", file, i, elapsed, perunit, over, max, avg)
} }
return nil return nil
} }

View File

@ -9,8 +9,8 @@ import (
"sync" "sync"
"time" "time"
"github.com/AdguardTeam/golibs/log"
"github.com/bluele/gcache" "github.com/bluele/gcache"
"github.com/hmage/golibs/log"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
@ -158,6 +158,11 @@ func (d *dayTop) addEntry(entry *logEntry, q *dns.Msg, now time.Time) error {
hostname := strings.ToLower(strings.TrimSuffix(q.Question[0].Name, ".")) hostname := strings.ToLower(strings.TrimSuffix(q.Question[0].Name, "."))
// if question hostname is empty, do nothing
if hostname == "" {
return nil
}
// get value, if not set, crate one // get value, if not set, crate one
d.hoursReadLock() d.hoursReadLock()
defer d.hoursReadUnlock() defer d.hoursReadUnlock()

224
filter.go
View File

@ -2,17 +2,18 @@ package main
import ( import (
"fmt" "fmt"
"hash/crc32"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"reflect"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/AdguardTeam/AdGuardHome/dnsfilter" "github.com/AdguardTeam/AdGuardHome/dnsfilter"
"github.com/hmage/golibs/log" "github.com/AdguardTeam/golibs/file"
"github.com/AdguardTeam/golibs/log"
) )
var ( var (
@ -27,6 +28,7 @@ type filter struct {
Name string `json:"name" yaml:"name"` Name string `json:"name" yaml:"name"`
RulesCount int `json:"rulesCount" yaml:"-"` RulesCount int `json:"rulesCount" yaml:"-"`
LastUpdated time.Time `json:"lastUpdated,omitempty" yaml:"-"` LastUpdated time.Time `json:"lastUpdated,omitempty" yaml:"-"`
checksum uint32 // checksum of the file data
dnsfilter.Filter `yaml:",inline"` dnsfilter.Filter `yaml:",inline"`
} }
@ -42,6 +44,88 @@ func userFilter() filter {
} }
} }
// Enable or disable a filter
func filterEnable(url string, enable bool) bool {
r := false
config.Lock()
for i := range config.Filters {
filter := &config.Filters[i] // otherwise we will be operating on a copy
if filter.URL == url {
filter.Enabled = enable
if enable {
e := filter.load()
if e != nil {
// This isn't a fatal error,
// because it may occur when someone removes the file from disk.
// In this case the periodic update task will try to download the file.
filter.LastUpdated = time.Time{}
log.Tracef("%s filter load: %v", url, e)
}
} else {
filter.unload()
}
r = true
break
}
}
config.Unlock()
return r
}
// Return TRUE if a filter with this URL exists
func filterExists(url string) bool {
r := false
config.RLock()
for i := range config.Filters {
if config.Filters[i].URL == url {
r = true
break
}
}
config.RUnlock()
return r
}
// Add a filter
// Return FALSE if a filter with this URL exists
func filterAdd(f filter) bool {
config.Lock()
// Check for duplicates
for i := range config.Filters {
if config.Filters[i].URL == f.URL {
config.Unlock()
return false
}
}
config.Filters = append(config.Filters, f)
config.Unlock()
return true
}
// Load filters from the disk
// And if any filter has zero ID, assign a new one
func loadFilters() {
for i := range config.Filters {
filter := &config.Filters[i] // otherwise we're operating on a copy
if filter.ID == 0 {
filter.ID = assignUniqueFilterID()
}
if !filter.Enabled {
// No need to load a filter that is not enabled
continue
}
err := filter.load()
if err != nil {
// This is okay for the first start, the filter will be loaded later
log.Debug("Couldn't load filter %d contents due to %s", filter.ID, err)
}
}
}
func deduplicateFilters() { func deduplicateFilters() {
// Deduplicate filters // Deduplicate filters
i := 0 // output index, used for deletion later i := 0 // output index, used for deletion later
@ -83,41 +167,83 @@ func periodicallyRefreshFilters() {
// Checks filters updates if necessary // Checks filters updates if necessary
// If force is true, it ignores the filter.LastUpdated field value // If force is true, it ignores the filter.LastUpdated field value
//
// Algorithm:
// . Get the list of filters to be updated
// . For each filter run the download and checksum check operation
// . If filter data hasn't changed, set new update time
// . If filter data has changed, parse it, save it on disk, set new update time
// . Apply changes to the current configuration
// . Restart server
func refreshFiltersIfNecessary(force bool) int { func refreshFiltersIfNecessary(force bool) int {
config.Lock() var updateFilters []filter
// fetch URLs config.RLock()
updateCount := 0
for i := range config.Filters { for i := range config.Filters {
filter := &config.Filters[i] // otherwise we will be operating on a copy f := &config.Filters[i] // otherwise we will be operating on a copy
if filter.ID == 0 { // protect against users modifying the yaml and removing the ID if !f.Enabled {
filter.ID = assignUniqueFilterID() continue
} }
if len(filter.Rules) == 0 { if !force && time.Since(f.LastUpdated) <= updatePeriod {
// Try reloading filter from the disk before updating continue
// This is useful for the case when we simply enable a previously downloaded filter
_ = filter.load()
} }
updated, err := filter.update(force) var uf filter
uf.ID = f.ID
uf.URL = f.URL
uf.checksum = f.checksum
updateFilters = append(updateFilters, uf)
}
config.RUnlock()
updateCount := 0
for i := range updateFilters {
uf := &updateFilters[i]
updated, err := uf.update()
if err != nil { if err != nil {
log.Printf("Failed to update filter %s: %s\n", filter.URL, err) log.Printf("Failed to update filter %s: %s\n", uf.URL, err)
continue continue
} }
if updated { if updated {
// Saving it to the filters dir now // Saving it to the filters dir now
err = filter.save() err = uf.save()
if err != nil { if err != nil {
log.Printf("Failed to save the updated filter %d: %s", filter.ID, err) log.Printf("Failed to save the updated filter %d: %s", uf.ID, err)
continue continue
} }
} else {
mtime := time.Now()
e := os.Chtimes(uf.Path(), mtime, mtime)
if e != nil {
log.Error("os.Chtimes(): %v", e)
}
uf.LastUpdated = mtime
}
config.Lock()
for k := range config.Filters {
f := &config.Filters[k]
if f.ID != uf.ID || f.URL != uf.URL {
continue
}
f.LastUpdated = uf.LastUpdated
if !updated {
continue
}
log.Info("Updated filter #%d. Rules: %d -> %d",
f.ID, f.RulesCount, uf.RulesCount)
f.Name = uf.Name
f.Rules = uf.Rules
f.RulesCount = uf.RulesCount
f.checksum = uf.checksum
updateCount++ updateCount++
} }
}
config.Unlock() config.Unlock()
}
if updateCount > 0 && isRunning() { if updateCount > 0 && isRunning() {
err := reconfigureDNSServer() err := reconfigureDNSServer()
@ -138,13 +264,19 @@ func parseFilterContents(contents []byte) (int, string, []string) {
// Count lines in the filter // Count lines in the filter
for _, line := range lines { for _, line := range lines {
line = strings.TrimSpace(line) line = strings.TrimSpace(line)
if len(line) > 0 && line[0] == '!' { if len(line) == 0 {
if m := filterTitleRegexp.FindAllStringSubmatch(line, -1); len(m) > 0 && len(m[0]) >= 2 && !seenTitle { continue
}
if line[0] == '!' {
m := filterTitleRegexp.FindAllStringSubmatch(line, -1)
if len(m) > 0 && len(m[0]) >= 2 && !seenTitle {
name = m[0][1] name = m[0][1]
seenTitle = true seenTitle = true
} }
} else if len(line) != 0 { } else {
rulesCount++ rulesCount++
} }
} }
@ -152,20 +284,8 @@ func parseFilterContents(contents []byte) (int, string, []string) {
return rulesCount, name, lines return rulesCount, name, lines
} }
// Checks for filters updates // Perform upgrade on a filter
// If "force" is true -- does not check the filter's LastUpdated field func (filter *filter) update() (bool, error) {
// Call "save" to persist the filter contents
func (filter *filter) update(force bool) (bool, error) {
if filter.ID == 0 { // protect against users deleting the ID
filter.ID = assignUniqueFilterID()
}
if !filter.Enabled {
return false, nil
}
if !force && time.Since(filter.LastUpdated) <= updatePeriod {
return false, nil
}
log.Tracef("Downloading update for filter %d from %s", filter.ID, filter.URL) log.Tracef("Downloading update for filter %d from %s", filter.ID, filter.URL)
resp, err := client.Get(filter.URL) resp, err := client.Get(filter.URL)
@ -194,22 +314,22 @@ func (filter *filter) update(force bool) (bool, error) {
return false, err return false, err
} }
// Extract filter name and count number of rules
rulesCount, filterName, rules := parseFilterContents(body)
if filterName != "" {
filter.Name = filterName
}
// Check if the filter has been really changed // Check if the filter has been really changed
if reflect.DeepEqual(filter.Rules, rules) { checksum := crc32.ChecksumIEEE(body)
if filter.checksum == checksum {
log.Tracef("Filter #%d at URL %s hasn't changed, not updating it", filter.ID, filter.URL) log.Tracef("Filter #%d at URL %s hasn't changed, not updating it", filter.ID, filter.URL)
return false, nil return false, nil
} }
// Extract filter name and count number of rules
rulesCount, filterName, rules := parseFilterContents(body)
log.Printf("Filter %d has been updated: %d bytes, %d rules", filter.ID, len(body), rulesCount) log.Printf("Filter %d has been updated: %d bytes, %d rules", filter.ID, len(body), rulesCount)
if filterName != "" {
filter.Name = filterName
}
filter.RulesCount = rulesCount filter.RulesCount = rulesCount
filter.Rules = rules filter.Rules = rules
filter.checksum = checksum
return true, nil return true, nil
} }
@ -220,7 +340,7 @@ func (filter *filter) save() error {
log.Printf("Saving filter %d contents to: %s", filter.ID, filterFilePath) log.Printf("Saving filter %d contents to: %s", filter.ID, filterFilePath)
body := []byte(strings.Join(filter.Rules, "\n")) body := []byte(strings.Join(filter.Rules, "\n"))
err := safeWriteFile(filterFilePath, body) err := file.SafeWrite(filterFilePath, body)
// update LastUpdated field after saving the file // update LastUpdated field after saving the file
filter.LastUpdated = filter.LastTimeUpdated() filter.LastUpdated = filter.LastTimeUpdated()
@ -229,11 +349,6 @@ func (filter *filter) save() error {
// loads filter contents from the file in dataDir // loads filter contents from the file in dataDir
func (filter *filter) load() error { func (filter *filter) load() error {
if !filter.Enabled {
// No need to load a filter that is not enabled
return nil
}
filterFilePath := filter.Path() filterFilePath := filter.Path()
log.Tracef("Loading filter %d contents to: %s", filter.ID, filterFilePath) log.Tracef("Loading filter %d contents to: %s", filter.ID, filterFilePath)
@ -252,25 +367,32 @@ func (filter *filter) load() error {
filter.RulesCount = rulesCount filter.RulesCount = rulesCount
filter.Rules = rules filter.Rules = rules
filter.checksum = crc32.ChecksumIEEE(filterFileContents)
filter.LastUpdated = filter.LastTimeUpdated() filter.LastUpdated = filter.LastTimeUpdated()
return nil return nil
} }
// Clear filter rules
func (filter *filter) unload() {
filter.Rules = []string{}
filter.RulesCount = 0
}
// Path to the filter contents // Path to the filter contents
func (filter *filter) Path() string { func (filter *filter) Path() string {
return filepath.Join(config.ourWorkingDir, dataDir, filterDir, strconv.FormatInt(filter.ID, 10)+".txt") return filepath.Join(config.ourWorkingDir, dataDir, filterDir, strconv.FormatInt(filter.ID, 10)+".txt")
} }
// LastUpdated returns the time when the filter was last time updated // LastTimeUpdated returns the time when the filter was last time updated
func (filter *filter) LastTimeUpdated() time.Time { func (filter *filter) LastTimeUpdated() time.Time {
filterFilePath := filter.Path() filterFilePath := filter.Path()
if _, err := os.Stat(filterFilePath); os.IsNotExist(err) { s, err := os.Stat(filterFilePath)
if os.IsNotExist(err) {
// if the filter file does not exist, return 0001-01-01 // if the filter file does not exist, return 0001-01-01
return time.Time{} return time.Time{}
} }
s, err := os.Stat(filterFilePath)
if err != nil { if err != nil {
// if the filter file does not exist, return 0001-01-01 // if the filter file does not exist, return 0001-01-01
return time.Time{} return time.Time{}

9
go.mod
View File

@ -1,13 +1,15 @@
module github.com/AdguardTeam/AdGuardHome module github.com/AdguardTeam/AdGuardHome
go 1.12
require ( require (
github.com/AdguardTeam/dnsproxy v0.11.1 github.com/AdguardTeam/dnsproxy v0.12.0
github.com/AdguardTeam/golibs v0.1.3
github.com/StackExchange/wmi v0.0.0-20180725035823-b12b22c5341f // indirect github.com/StackExchange/wmi v0.0.0-20180725035823-b12b22c5341f // indirect
github.com/bluele/gcache v0.0.0-20171010155617-472614239ac7 github.com/bluele/gcache v0.0.0-20190203144525-2016d595ccb0
github.com/go-ole/go-ole v1.2.1 // indirect github.com/go-ole/go-ole v1.2.1 // indirect
github.com/go-test/deep v1.0.1 github.com/go-test/deep v1.0.1
github.com/gobuffalo/packr v1.19.0 github.com/gobuffalo/packr v1.19.0
github.com/hmage/golibs v0.0.0-20190121112702-20153bd03c24
github.com/joomcode/errorx v0.1.0 github.com/joomcode/errorx v0.1.0
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 // indirect github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 // indirect
github.com/kardianos/service v0.0.0-20181115005516-4c239ee84e7b github.com/kardianos/service v0.0.0-20181115005516-4c239ee84e7b
@ -15,6 +17,7 @@ require (
github.com/miekg/dns v1.1.1 github.com/miekg/dns v1.1.1
github.com/shirou/gopsutil v2.18.10+incompatible github.com/shirou/gopsutil v2.18.10+incompatible
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 // indirect github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 // indirect
github.com/sparrc/go-ping v0.0.0-20181106165434-ef3ab45e41b0
github.com/stretchr/testify v1.2.2 github.com/stretchr/testify v1.2.2
go.uber.org/goleak v0.10.0 go.uber.org/goleak v0.10.0
golang.org/x/net v0.0.0-20190119204137-ed066c81e75e golang.org/x/net v0.0.0-20190119204137-ed066c81e75e

15
go.sum
View File

@ -1,5 +1,8 @@
github.com/AdguardTeam/dnsproxy v0.11.1 h1:qO5VH0GYF9vdksQRG8frEfJ+CJjsPBwuct8FH6Mij7o= github.com/AdguardTeam/dnsproxy v0.12.0 h1:BPgv2PlH2u4xakFcaW4EqU3Visk1BNidrqGSgxe5Qzg=
github.com/AdguardTeam/dnsproxy v0.11.1/go.mod h1:lEi2srAWwfSQWoy8GeZR6lwS+FSMoiZid8bQPreOLb0= github.com/AdguardTeam/dnsproxy v0.12.0/go.mod h1:lcZM2QPwcWGEL3pz8RYy06nQdbjj4pr+94H45jnVSHg=
github.com/AdguardTeam/golibs v0.1.2/go.mod h1:b0XkhgIcn2TxwX6C5AQMtpIFAgjPehNgxJErWkwA3ko=
github.com/AdguardTeam/golibs v0.1.3 h1:hmapdTtMtIk3T8eQDwTOLdqZLGDKNKk9325uC8z12xg=
github.com/AdguardTeam/golibs v0.1.3/go.mod h1:b0XkhgIcn2TxwX6C5AQMtpIFAgjPehNgxJErWkwA3ko=
github.com/StackExchange/wmi v0.0.0-20180725035823-b12b22c5341f h1:5ZfJxyXo8KyX8DgGXC5B7ILL8y51fci/qYz2B4j8iLY= github.com/StackExchange/wmi v0.0.0-20180725035823-b12b22c5341f h1:5ZfJxyXo8KyX8DgGXC5B7ILL8y51fci/qYz2B4j8iLY=
github.com/StackExchange/wmi v0.0.0-20180725035823-b12b22c5341f/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/StackExchange/wmi v0.0.0-20180725035823-b12b22c5341f/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
@ -12,8 +15,8 @@ github.com/ameshkov/dnsstamps v1.0.1 h1:LhGvgWDzhNJh+kBQd/AfUlq1vfVe109huiXw4Jhn
github.com/ameshkov/dnsstamps v1.0.1/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A= github.com/ameshkov/dnsstamps v1.0.1/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
github.com/beefsack/go-rate v0.0.0-20180408011153-efa7637bb9b6 h1:KXlsf+qt/X5ttPGEjR0tPH1xaWWoKBEg9Q1THAj2h3I= github.com/beefsack/go-rate v0.0.0-20180408011153-efa7637bb9b6 h1:KXlsf+qt/X5ttPGEjR0tPH1xaWWoKBEg9Q1THAj2h3I=
github.com/beefsack/go-rate v0.0.0-20180408011153-efa7637bb9b6/go.mod h1:6YNgTHLutezwnBvyneBbwvB8C82y3dcoOj5EQJIdGXA= github.com/beefsack/go-rate v0.0.0-20180408011153-efa7637bb9b6/go.mod h1:6YNgTHLutezwnBvyneBbwvB8C82y3dcoOj5EQJIdGXA=
github.com/bluele/gcache v0.0.0-20171010155617-472614239ac7 h1:NpQ+gkFOH27AyDypSCJ/LdsIi/b4rdnEb1N5+IpFfYs= github.com/bluele/gcache v0.0.0-20190203144525-2016d595ccb0 h1:vUdUwmQLnT/yuk8PsDhhMVkrfr4aMdcv/0GWzIqOjEY=
github.com/bluele/gcache v0.0.0-20171010155617-472614239ac7/go.mod h1:8c4/i2VlovMO2gBnHGQPN5EJw+H0lx1u/5p+cgsXtCk= github.com/bluele/gcache v0.0.0-20190203144525-2016d595ccb0/go.mod h1:8c4/i2VlovMO2gBnHGQPN5EJw+H0lx1u/5p+cgsXtCk=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-ole/go-ole v1.2.1 h1:2lOsA72HgjxAuMlKpFiCbHTvu44PIVkZ5hqm3RSdI/E= github.com/go-ole/go-ole v1.2.1 h1:2lOsA72HgjxAuMlKpFiCbHTvu44PIVkZ5hqm3RSdI/E=
@ -26,8 +29,6 @@ github.com/gobuffalo/packd v0.0.0-20181031195726-c82734870264 h1:roWyi0eEdiFreSq
github.com/gobuffalo/packd v0.0.0-20181031195726-c82734870264/go.mod h1:Yf2toFaISlyQrr5TfO3h6DB9pl9mZRmyvBGQb/aQ/pI= github.com/gobuffalo/packd v0.0.0-20181031195726-c82734870264/go.mod h1:Yf2toFaISlyQrr5TfO3h6DB9pl9mZRmyvBGQb/aQ/pI=
github.com/gobuffalo/packr v1.19.0 h1:3UDmBDxesCOPF8iZdMDBBWKfkBoYujIMIZePnobqIUI= github.com/gobuffalo/packr v1.19.0 h1:3UDmBDxesCOPF8iZdMDBBWKfkBoYujIMIZePnobqIUI=
github.com/gobuffalo/packr v1.19.0/go.mod h1:MstrNkfCQhd5o+Ct4IJ0skWlxN8emOq8DsoT1G98VIU= github.com/gobuffalo/packr v1.19.0/go.mod h1:MstrNkfCQhd5o+Ct4IJ0skWlxN8emOq8DsoT1G98VIU=
github.com/hmage/golibs v0.0.0-20190121112702-20153bd03c24 h1:yyDtaSMcAZdm1I6uL8YLghpWiJljfBHs8NC/P86PYQk=
github.com/hmage/golibs v0.0.0-20190121112702-20153bd03c24/go.mod h1:H6Ev6svFxUVPFThxLtdnFfcE9e3GWufpfmcVFpqV6HM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
@ -55,6 +56,8 @@ github.com/shirou/gopsutil v2.18.10+incompatible h1:cy84jW6EVRPa5g9HAHrlbxMSIjBh
github.com/shirou/gopsutil v2.18.10+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shirou/gopsutil v2.18.10+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 h1:udFKJ0aHUL60LboW/A+DfgoHVedieIzIXE8uylPue0U= github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 h1:udFKJ0aHUL60LboW/A+DfgoHVedieIzIXE8uylPue0U=
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
github.com/sparrc/go-ping v0.0.0-20181106165434-ef3ab45e41b0 h1:mu7brOsdaH5Dqf93vdch+mr/0To8Sgc+yInt/jE/RJM=
github.com/sparrc/go-ping v0.0.0-20181106165434-ef3ab45e41b0/go.mod h1:eMyUVp6f/5jnzM+3zahzl7q6UXLbgSc3MKg/+ow9QW0=
github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=

View File

@ -5,7 +5,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
@ -19,26 +18,6 @@ import (
"github.com/joomcode/errorx" "github.com/joomcode/errorx"
) )
// ----------------------------------
// helper functions for working with files
// ----------------------------------
// Writes data first to a temporary file and then renames it to what's specified in path
func safeWriteFile(path string, data []byte) error {
dir := filepath.Dir(path)
err := os.MkdirAll(dir, 0755)
if err != nil {
return err
}
tmpPath := path + ".tmp"
err = ioutil.WriteFile(tmpPath, data, 0644)
if err != nil {
return err
}
return os.Rename(tmpPath, path)
}
// ---------------------------------- // ----------------------------------
// helper functions for HTTP handlers // helper functions for HTTP handlers
// ---------------------------------- // ----------------------------------
@ -48,6 +27,12 @@ func ensure(method string, handler func(http.ResponseWriter, *http.Request)) fun
http.Error(w, "This request must be "+method, http.StatusMethodNotAllowed) http.Error(w, "This request must be "+method, http.StatusMethodNotAllowed)
return return
} }
if method == "POST" || method == "PUT" || method == "DELETE" {
controlLock.Lock()
defer controlLock.Unlock()
}
handler(w, r) handler(w, r)
} }
} }

View File

@ -3,7 +3,7 @@ package main
import ( import (
"testing" "testing"
"github.com/hmage/golibs/log" "github.com/AdguardTeam/golibs/log"
) )
func TestGetValidNetInterfacesForWeb(t *testing.T) { func TestGetValidNetInterfacesForWeb(t *testing.T) {

View File

@ -6,7 +6,7 @@ import (
"net/http" "net/http"
"strings" "strings"
"github.com/hmage/golibs/log" "github.com/AdguardTeam/golibs/log"
) )
// -------------------- // --------------------
@ -22,6 +22,8 @@ var allowedLanguages = map[string]bool{
"sv": true, "sv": true,
"pt-br": true, "pt-br": true,
"zh-tw": true, "zh-tw": true,
"bg": true,
"zh-cn": true,
} }
func isLanguageAllowed(language string) bool { func isLanguageAllowed(language string) bool {

View File

@ -87,26 +87,21 @@ paths:
200: 200:
description: OK description: OK
/set_upstream_dns: /set_upstreams_config:
post: post:
tags: tags:
- global - global
operationId: setUpstreamDNS operationId: setUpstreamsConfig
summary: 'Set upstream DNS for coredns, empty value will reset it to default values' summary: "Updates the current upstreams configuration"
consumes: consumes:
- text/plain - application/json
parameters: parameters:
- in: body - in: "body"
name: upstream name: "body"
description: 'Upstream servers, separated by newline or space, port is optional after colon' description: "Upstreams configuration JSON"
required: true
schema: schema:
# TODO: use JSON $ref: "#/definitions/UpstreamsConfig"
type: string
example: |
1.1.1.1
1.0.0.1
8.8.8.8 8.8.4.4
192.168.1.104:53535
responses: responses:
200: 200:
description: OK description: OK
@ -116,21 +111,15 @@ paths:
tags: tags:
- global - global
operationId: testUpstreamDNS operationId: testUpstreamDNS
summary: 'Test upstream DNS' summary: "Test upstream configuration"
consumes: consumes:
- text/plain - application/json
parameters: parameters:
- in: body - in: "body"
name: upstream name: "body"
description: 'Upstream servers, separated by newline or space, port is optional after colon' description: "Upstream configuration to be tested"
schema: schema:
# TODO: use JSON $ref: "#/definitions/UpstreamsConfig"
type: string
example: |
1.1.1.1
1.0.0.1
8.8.8.8 8.8.4.4
192.168.1.104:53535
responses: responses:
200: 200:
description: 'Status of testing each requested server, with "OK" meaning that server works, any other text means an error.' description: 'Status of testing each requested server, with "OK" meaning that server works, any other text means an error.'
@ -798,6 +787,33 @@ definitions:
language: language:
type: "string" type: "string"
example: "en" example: "en"
UpstreamsConfig:
type: "object"
description: "Upstreams configuration"
required:
- "bootstrap_dns"
- "upstream_dns"
- "all_servers"
properties:
bootstrap_dns:
type: "array"
description: 'Bootstrap servers, port is optional after colon. Empty value will reset it to default values'
items:
type: "string"
example:
- "8.8.8.8:53"
- "1.1.1.1:53"
upstream_dns:
type: "array"
description: 'Upstream servers, port is optional after colon. Empty value will reset it to default values'
items:
type: "string"
example:
- "tls://1.1.1.1"
- "tls://1.0.0.1"
all_servers:
type: "boolean"
description: "If true, parallel queries to all configured upstream servers are enabled"
Filter: Filter:
type: "object" type: "object"
description: "Filter subscription info" description: "Filter subscription info"
@ -1247,6 +1263,10 @@ definitions:
type: "string" type: "string"
example: "You have specified an empty certificate" example: "You have specified an empty certificate"
description: "warning_validation is a validation warning message with the issue description" description: "warning_validation is a validation warning message with the issue description"
valid_pair:
type: "boolean"
example: "true"
description: "valid_pair is true if both certificate and private key are correct"
NetInterface: NetInterface:
type: "object" type: "object"
description: "Network interface info" description: "Network interface info"

View File

@ -14,6 +14,8 @@ const LOCALES_LIST = [
'sv', 'sv',
'pt-br', 'pt-br',
'zh-tw', 'zh-tw',
'bg',
'zh-cn',
]; ];
/** /**

View File

@ -4,7 +4,7 @@ import (
"os" "os"
"runtime" "runtime"
"github.com/hmage/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/kardianos/service" "github.com/kardianos/service"
) )
@ -127,11 +127,6 @@ func configureService(c *service.Config) {
// POSIX // POSIX
// Redirect StdErr & StdOut to files. // Redirect StdErr & StdOut to files.
c.Option["LogOutput"] = true c.Option["LogOutput"] = true
// Windows
if runtime.GOOS == "windows" {
c.UserName = "NT AUTHORITY\\NetworkService"
}
} }
// cleanupService called on the service uninstall, cleans up additional files if needed // cleanupService called on the service uninstall, cleans up additional files if needed

View File

@ -6,11 +6,12 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"github.com/hmage/golibs/log" "github.com/AdguardTeam/golibs/file"
"github.com/AdguardTeam/golibs/log"
yaml "gopkg.in/yaml.v2" yaml "gopkg.in/yaml.v2"
) )
const currentSchemaVersion = 2 // used for upgrading from old configs to new config const currentSchemaVersion = 3 // used for upgrading from old configs to new config
// Performs necessary upgrade operations if needed // Performs necessary upgrade operations if needed
func upgradeConfig() error { func upgradeConfig() error {
@ -59,12 +60,17 @@ func upgradeConfig() error {
func upgradeConfigSchema(oldVersion int, diskConfig *map[string]interface{}) error { func upgradeConfigSchema(oldVersion int, diskConfig *map[string]interface{}) error {
switch oldVersion { switch oldVersion {
case 0: case 0:
err := upgradeSchema0to2(diskConfig) err := upgradeSchema0to3(diskConfig)
if err != nil { if err != nil {
return err return err
} }
case 1: case 1:
err := upgradeSchema1to2(diskConfig) err := upgradeSchema1to3(diskConfig)
if err != nil {
return err
}
case 2:
err := upgradeSchema2to3(diskConfig)
if err != nil { if err != nil {
return err return err
} }
@ -81,7 +87,7 @@ func upgradeConfigSchema(oldVersion int, diskConfig *map[string]interface{}) err
return err return err
} }
err = safeWriteFile(configFile, body) err = file.SafeWrite(configFile, body)
if err != nil { if err != nil {
log.Printf("Couldn't save YAML config: %s", err) log.Printf("Couldn't save YAML config: %s", err)
return err return err
@ -135,12 +141,60 @@ func upgradeSchema1to2(diskConfig *map[string]interface{}) error {
return nil return nil
} }
// jump two schemas at once -- this time we just do it sequentially // Third schema upgrade:
func upgradeSchema0to2(diskConfig *map[string]interface{}) error { // Bootstrap DNS becomes an array
func upgradeSchema2to3(diskConfig *map[string]interface{}) error {
log.Printf("%s(): called", _Func())
// Let's read dns configuration from diskConfig
dnsConfig, ok := (*diskConfig)["dns"]
if !ok {
return fmt.Errorf("no DNS configuration in config file")
}
// Convert interface{} to map[string]interface{}
newDNSConfig := make(map[string]interface{})
switch v := dnsConfig.(type) {
case map[interface{}]interface{}:
for k, v := range v {
newDNSConfig[fmt.Sprint(k)] = v
}
default:
return fmt.Errorf("DNS configuration is not a map")
}
// Replace bootstrap_dns value filed with new array contains old bootstrap_dns inside
if bootstrapDNS, ok := (newDNSConfig)["bootstrap_dns"]; ok {
newBootstrapConfig := []string{fmt.Sprint(bootstrapDNS)}
(newDNSConfig)["bootstrap_dns"] = newBootstrapConfig
(*diskConfig)["dns"] = newDNSConfig
} else {
return fmt.Errorf("no bootstrap DNS in DNS config")
}
// Bump schema version
(*diskConfig)["schema_version"] = 3
return nil
}
// jump three schemas at once -- this time we just do it sequentially
func upgradeSchema0to3(diskConfig *map[string]interface{}) error {
err := upgradeSchema0to1(diskConfig) err := upgradeSchema0to1(diskConfig)
if err != nil { if err != nil {
return err return err
} }
return upgradeSchema1to2(diskConfig) return upgradeSchema1to3(diskConfig)
}
// jump two schemas at once -- this time we just do it sequentially
func upgradeSchema1to3(diskConfig *map[string]interface{}) error {
err := upgradeSchema1to2(diskConfig)
if err != nil {
return err
}
return upgradeSchema2to3(diskConfig)
} }

230
upgrade_test.go Normal file
View File

@ -0,0 +1,230 @@
package main
import (
"fmt"
"testing"
)
func TestUpgrade1to2(t *testing.T) {
// let's create test config for 1 schema version
diskConfig := createTestDiskConfig(1)
// update config
err := upgradeSchema1to2(&diskConfig)
if err != nil {
t.Fatalf("Can't upgrade schema version from 1 to 2")
}
// ensure that schema version was bumped
compareSchemaVersion(t, diskConfig["schema_version"], 2)
// old coredns entry should be removed
_, ok := diskConfig["coredns"]
if ok {
t.Fatalf("Core DNS config was not removed after upgrade schema version from 1 to 2")
}
// pull out new dns config
dnsMap, ok := diskConfig["dns"]
if !ok {
t.Fatalf("No DNS config after upgrade schema version from 1 to 2")
}
// cast dns configurations to maps and compare them
oldDNSConfig := castInterfaceToMap(t, createTestDNSConfig(1))
newDNSConfig := castInterfaceToMap(t, dnsMap)
compareConfigs(t, &oldDNSConfig, &newDNSConfig)
// exclude dns config and schema version from disk config comparison
oldExcludedEntries := []string{"coredns", "schema_version"}
newExcludedEntries := []string{"dns", "schema_version"}
oldDiskConfig := createTestDiskConfig(1)
compareConfigsWithoutEntries(t, &oldDiskConfig, &diskConfig, oldExcludedEntries, newExcludedEntries)
}
func TestUpgrade2to3(t *testing.T) {
// let's create test config
diskConfig := createTestDiskConfig(2)
// upgrade schema from 2 to 3
err := upgradeSchema2to3(&diskConfig)
if err != nil {
t.Fatalf("Can't update schema version from 2 to 3: %s", err)
}
// check new schema version
compareSchemaVersion(t, diskConfig["schema_version"], 3)
// pull out new dns configuration
dnsMap, ok := diskConfig["dns"]
if !ok {
t.Fatalf("No dns config in new configuration")
}
// cast dns configuration to map
newDNSConfig := castInterfaceToMap(t, dnsMap)
// check if bootstrap DNS becomes an array
bootstrapDNS := newDNSConfig["bootstrap_dns"]
switch v := bootstrapDNS.(type) {
case []string:
if len(v) != 1 {
t.Fatalf("Wrong count of bootsrap DNS servers: %d", len(v))
}
if v[0] != "8.8.8.8:53" {
t.Fatalf("Bootsrap DNS server is not 8.8.8.8:53 : %s", v[0])
}
default:
t.Fatalf("Wrong type for bootsrap DNS: %T", v)
}
// exclude bootstrap DNS from DNS configs comparison
excludedEntries := []string{"bootstrap_dns"}
oldDNSConfig := castInterfaceToMap(t, createTestDNSConfig(2))
compareConfigsWithoutEntries(t, &oldDNSConfig, &newDNSConfig, excludedEntries, excludedEntries)
// excluded dns config and schema version from disk config comparison
excludedEntries = []string{"dns", "schema_version"}
oldDiskConfig := createTestDiskConfig(2)
compareConfigsWithoutEntries(t, &oldDiskConfig, &diskConfig, excludedEntries, excludedEntries)
}
func castInterfaceToMap(t *testing.T, oldConfig interface{}) (newConfig map[string]interface{}) {
newConfig = make(map[string]interface{})
switch v := oldConfig.(type) {
case map[interface{}]interface{}:
for key, value := range v {
newConfig[fmt.Sprint(key)] = value
}
case map[string]interface{}:
for key, value := range v {
newConfig[key] = value
}
default:
t.Fatalf("DNS configuration is not a map")
}
return
}
// compareConfigsWithoutEntry removes entries from configs and returns result of compareConfigs
func compareConfigsWithoutEntries(t *testing.T, oldConfig, newConfig *map[string]interface{}, oldKey, newKey []string) {
for _, k := range oldKey {
delete(*oldConfig, k)
}
for _, k := range newKey {
delete(*newConfig, k)
}
compareConfigs(t, oldConfig, newConfig)
}
// compares configs before and after schema upgrade
func compareConfigs(t *testing.T, oldConfig, newConfig *map[string]interface{}) {
if len(*oldConfig) != len(*newConfig) {
t.Fatalf("wrong config entries count! Before upgrade: %d; After upgrade: %d", len(*oldConfig), len(*oldConfig))
}
// Check old and new entries
for k, v := range *newConfig {
switch value := v.(type) {
case string:
if value != (*oldConfig)[k] {
t.Fatalf("wrong value for string %s. Before update: %s; After update: %s", k, (*oldConfig)[k], value)
}
case int:
if value != (*oldConfig)[k] {
t.Fatalf("wrong value for int %s. Before update: %d; After update: %d", k, (*oldConfig)[k], value)
}
case []string:
for i, line := range value {
if len((*oldConfig)[k].([]string)) != len(value) {
t.Fatalf("wrong array length for %s. Before update: %d; After update: %d", k, len((*oldConfig)[k].([]string)), len(value))
}
if (*oldConfig)[k].([]string)[i] != line {
t.Fatalf("wrong data for string array %s. Before update: %s; After update: %s", k, (*oldConfig)[k].([]string)[i], line)
}
}
case bool:
if v != (*oldConfig)[k].(bool) {
t.Fatalf("wrong boolean value for %s", k)
}
case []filter:
if len((*oldConfig)[k].([]filter)) != len(value) {
t.Fatalf("wrong filters count. Before update: %d; After update: %d", len((*oldConfig)[k].([]filter)), len(value))
}
for i, newFilter := range value {
oldFilter := (*oldConfig)[k].([]filter)[i]
if oldFilter.Enabled != newFilter.Enabled || oldFilter.Name != newFilter.Name || oldFilter.RulesCount != newFilter.RulesCount {
t.Fatalf("old filter %s not equals new filter %s", oldFilter.Name, newFilter.Name)
}
}
default:
t.Fatalf("uknown data type for %s: %T", k, value)
}
}
}
// compareSchemaVersion check if newSchemaVersion equals schemaVersion
func compareSchemaVersion(t *testing.T, newSchemaVersion interface{}, schemaVersion int) {
switch v := newSchemaVersion.(type) {
case int:
if v != schemaVersion {
t.Fatalf("Wrong schema version in new config file")
}
default:
t.Fatalf("Schema version is not an integer after update")
}
}
func createTestDiskConfig(schemaVersion int) (diskConfig map[string]interface{}) {
diskConfig = make(map[string]interface{})
diskConfig["language"] = "en"
diskConfig["filters"] = []filter{
{
URL: "https://filters.adtidy.org/android/filters/111_optimized.txt",
Name: "Latvian filter",
RulesCount: 100,
},
{
URL: "https://easylist.to/easylistgermany/easylistgermany.txt",
Name: "Germany filter",
RulesCount: 200,
},
}
diskConfig["user_rules"] = []string{}
diskConfig["schema_version"] = schemaVersion
diskConfig["bind_host"] = "0.0.0.0"
diskConfig["bind_port"] = 80
diskConfig["auth_name"] = "name"
diskConfig["auth_pass"] = "pass"
dnsConfig := createTestDNSConfig(schemaVersion)
if schemaVersion > 1 {
diskConfig["dns"] = dnsConfig
} else {
diskConfig["coredns"] = dnsConfig
}
return diskConfig
}
func createTestDNSConfig(schemaVersion int) map[interface{}]interface{} {
dnsConfig := make(map[interface{}]interface{})
dnsConfig["port"] = 53
dnsConfig["blocked_response_ttl"] = 10
dnsConfig["querylog_enabled"] = true
dnsConfig["ratelimit"] = 20
dnsConfig["bootstrap_dns"] = "8.8.8.8:53"
if schemaVersion > 2 {
dnsConfig["bootstrap_dns"] = []string{"8.8.8.8:53"}
}
dnsConfig["parental_sensitivity"] = 13
dnsConfig["ratelimit_whitelist"] = []string{}
dnsConfig["upstream_dns"] = []string{"tls://1.1.1.1", "tls://1.0.0.1", "8.8.8.8"}
dnsConfig["filtering_enabled"] = true
dnsConfig["refuse_any"] = true
dnsConfig["parental_enabled"] = true
dnsConfig["bind_host"] = "0.0.0.0"
dnsConfig["protection_enabled"] = true
dnsConfig["safesearch_enabled"] = true
dnsConfig["safebrowsing_enabled"] = true
return dnsConfig
}