Merge with master
This commit is contained in:
commit
7ebfaccc6e
@ -2,7 +2,7 @@ language: go
|
||||
sudo: false
|
||||
|
||||
go:
|
||||
- 1.11.x
|
||||
- 1.12.x
|
||||
- 1.x
|
||||
os:
|
||||
- linux
|
||||
@ -38,7 +38,7 @@ matrix:
|
||||
# Release build configuration
|
||||
- name: release
|
||||
go:
|
||||
- 1.11.x
|
||||
- 1.12.x
|
||||
os:
|
||||
- linux
|
||||
|
||||
@ -66,7 +66,7 @@ matrix:
|
||||
- name: docker
|
||||
if: type != pull_request AND (branch = master OR tag IS present)
|
||||
go:
|
||||
- 1.11.x
|
||||
- 1.12.x
|
||||
os:
|
||||
- linux
|
||||
services:
|
||||
|
@ -70,7 +70,7 @@ Alternatively, you can use our [official Docker image](https://hub.docker.com/r/
|
||||
|
||||
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.
|
||||
|
||||
You can either install it via the provided links or use [brew.sh](https://brew.sh/) if you're on Mac:
|
||||
|
76
app.go
76
app.go
@ -3,7 +3,6 @@ package main
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
stdlog "log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
@ -13,11 +12,9 @@ import (
|
||||
"strconv"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/gobuffalo/packr"
|
||||
|
||||
"github.com/hmage/golibs/log"
|
||||
)
|
||||
|
||||
// VersionString will be set through ldflags, contains current version
|
||||
@ -71,11 +68,14 @@ func run(args options) {
|
||||
// configure log level and output
|
||||
configureLogger(args)
|
||||
|
||||
// enable TLS 1.3
|
||||
enableTLS13()
|
||||
|
||||
// print the first message after logger is configured
|
||||
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 {
|
||||
log.Printf("AdGuard Home is running as a service")
|
||||
log.Info("AdGuard Home is running as a service")
|
||||
}
|
||||
|
||||
config.firstRun = detectFirstRun()
|
||||
@ -100,24 +100,7 @@ func run(args options) {
|
||||
config.BindPort = args.bindPort
|
||||
}
|
||||
|
||||
// Load filters from the disk
|
||||
// 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{}
|
||||
}
|
||||
}
|
||||
loadFilters()
|
||||
|
||||
// Save the updated config
|
||||
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
|
||||
go func() {
|
||||
refreshFiltersIfNecessary(false)
|
||||
// Save the updated config
|
||||
err := config.write()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
// Schedule automatic filters updates
|
||||
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
|
||||
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)))
|
||||
registerInstallHandlers()
|
||||
}
|
||||
@ -178,13 +156,13 @@ func run(args options) {
|
||||
}
|
||||
address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.TLS.PortHTTPS))
|
||||
// validate current TLS config and update warnings (it could have been loaded from file)
|
||||
data := validateCertificates(config.TLS)
|
||||
if !data.usable {
|
||||
data := validateCertificates(config.TLS.CertificateChain, config.TLS.PrivateKey, config.TLS.ServerName)
|
||||
if !data.ValidPair {
|
||||
log.Fatal(data.WarningValidation)
|
||||
os.Exit(1)
|
||||
}
|
||||
config.Lock()
|
||||
config.TLS = data // update warnings
|
||||
config.TLS.tlsConfigStatus = data // update warnings
|
||||
config.Unlock()
|
||||
|
||||
// prepare certs for HTTPS server
|
||||
@ -263,7 +241,11 @@ func configureLogger(args options) {
|
||||
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" {
|
||||
// When running as a Windows service, use eventlog by default if nothing else is configured
|
||||
@ -283,24 +265,36 @@ func configureLogger(args options) {
|
||||
}
|
||||
} else {
|
||||
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 {
|
||||
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() {
|
||||
log.Printf("Stopping AdGuard Home")
|
||||
log.Info("Stopping AdGuard Home")
|
||||
|
||||
err := stopDNSServer()
|
||||
if err != nil {
|
||||
log.Printf("Couldn't stop DNS server: %s", err)
|
||||
log.Error("Couldn't stop DNS server: %s", err)
|
||||
}
|
||||
err = stopDHCPServer()
|
||||
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 opt.callbackWithValue != nil {
|
||||
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)
|
||||
}
|
||||
i++
|
||||
@ -386,7 +380,7 @@ func loadOptions() options {
|
||||
}
|
||||
}
|
||||
if !knownParam {
|
||||
log.Printf("ERROR: unknown option %v\n", v)
|
||||
log.Error("unknown option %v\n", v)
|
||||
printHelp()
|
||||
os.Exit(64)
|
||||
}
|
||||
|
32
client/package-lock.json
generated
vendored
32
client/package-lock.json
generated
vendored
@ -3260,6 +3260,38 @@
|
||||
"integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=",
|
||||
"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": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz",
|
||||
|
1
client/package.json
vendored
1
client/package.json
vendored
@ -47,6 +47,7 @@
|
||||
"babel-runtime": "6.26.0",
|
||||
"clean-webpack-plugin": "^0.1.19",
|
||||
"compression-webpack-plugin": "^1.1.11",
|
||||
"copy-webpack-plugin": "^4.6.0",
|
||||
"css-loader": "^0.28.11",
|
||||
"eslint": "^4.19.1",
|
||||
"eslint-config-airbnb-base": "^12.1.0",
|
||||
|
BIN
client/public/favicon.ico
Normal file
BIN
client/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<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>
|
||||
</head>
|
||||
<body>
|
||||
|
@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<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>
|
||||
</head>
|
||||
<body>
|
||||
|
250
client/src/__locales/bg.json
Normal file
250
client/src/__locales/bg.json
Normal 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."
|
||||
}
|
@ -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",
|
||||
"save_config": "Save config",
|
||||
"enabled_dhcp": "DHCP server enabled",
|
||||
@ -79,7 +82,7 @@
|
||||
"no_settings": "No settings",
|
||||
"general_settings": "General settings",
|
||||
"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",
|
||||
"apply_btn": "Apply",
|
||||
"disabled_filtering_toast": "Disabled filtering",
|
||||
@ -118,10 +121,11 @@
|
||||
"example_comment_hash": "# Also a comment",
|
||||
"example_regex_meaning": "block access to the domains matching the specified regular expression",
|
||||
"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_doh": "encrypted <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a>",
|
||||
"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_dot": "encrypted <0>DNS-over-TLS</0>",
|
||||
"example_upstream_doh": "encrypted <0>DNS-over-HTTPS</0>",
|
||||
"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_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",
|
||||
"updated_upstream_dns_toast": "Updated the upstream DNS servers",
|
||||
"dns_test_ok_toast": "Specified DNS servers are working correctly",
|
||||
@ -211,11 +215,11 @@
|
||||
"open_dashboard": "Open Dashboard",
|
||||
"install_saved": "Saved successfully",
|
||||
"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_server": "Server 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_desc": "If checked, AdGuard Home will automatically redirect you from HTTP to HTTPS addresses.",
|
||||
"encryption_https": "HTTPS port",
|
||||
@ -223,12 +227,12 @@
|
||||
"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 cerificates here.",
|
||||
"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 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_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",
|
||||
@ -239,12 +243,14 @@
|
||||
"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>.",
|
||||
"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": "Password mismatched",
|
||||
"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"
|
||||
}
|
@ -115,9 +115,9 @@
|
||||
"example_comment_meaning": "solo un comentario",
|
||||
"example_comment_hash": "# Tambi\u00e9n un comentario",
|
||||
"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_doh": "encriptado <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-a-trav\u00e9s-de-TLS<\/a>",
|
||||
"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_dot": "encriptado <0>DNS-a-trav\u00e9s-de-TLS<\/0>",
|
||||
"example_upstream_doh": "encriptado <0>DNS-a-trav\u00e9s-de-TLS<\/0>",
|
||||
"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)",
|
||||
"all_filters_up_to_date_toast": "Todos los filtros son actualizados",
|
||||
"updated_upstream_dns_toast": "Servidores DNS upstream actualizados",
|
||||
|
@ -116,9 +116,9 @@
|
||||
"example_comment_meaning": "commentaire",
|
||||
"example_comment_hash": "# Et comme \u00e7a aussi on peut laisser des commentaires",
|
||||
"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_doh": "<a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-au-dessus-de-HTTPS<\/a> 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_dot": "<0>DNS-au-dessus-de-TLS<\/0> chiffr\u00e9",
|
||||
"example_upstream_doh": "<0>DNS-au-dessus-de-HTTPS<\/0> chiffr\u00e9",
|
||||
"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)",
|
||||
"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",
|
||||
|
@ -115,9 +115,9 @@
|
||||
"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_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_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_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_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 <0>DNS-over-HTTPS<\/0>",
|
||||
"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",
|
||||
"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",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"url_added_successfully": "Url adicionada com sucesso",
|
||||
"check_dhcp_servers": "Verifique se h\u00e1 servidores DHCP",
|
||||
"url_added_successfully": "URL adicionada com sucesso",
|
||||
"check_dhcp_servers": "Verificar por servidores DHCP",
|
||||
"save_config": "Salvar configura\u00e7\u00e3o",
|
||||
"enabled_dhcp": "Servidor DHCP ativado",
|
||||
"disabled_dhcp": "Servidor DHCP desativado",
|
||||
@ -28,6 +28,7 @@
|
||||
"dhcp_ip_addresses": "Endere\u00e7o de IP",
|
||||
"dhcp_table_hostname": "Hostname",
|
||||
"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",
|
||||
"dashboard": "Painel",
|
||||
"settings": "Configura\u00e7\u00f5es",
|
||||
@ -115,10 +116,11 @@
|
||||
"example_comment": "! Aqui vai um coment\u00e1rio",
|
||||
"example_comment_meaning": "apenas 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_dot": "DNS criptografado <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>atrav\u00e9s do TLS<\/a>",
|
||||
"example_upstream_doh": "DNS criptografado <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>atrav\u00e9s do HTTPS<\/a>",
|
||||
"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_dot": "DNS criptografado <0>atrav\u00e9s do TLS<\/0>",
|
||||
"example_upstream_doh": "DNS criptografado <0>atrav\u00e9s do HTTPS<\/0>",
|
||||
"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)",
|
||||
"all_filters_up_to_date_toast": "Todos os filtros j\u00e1 est\u00e3o atualizados",
|
||||
"updated_upstream_dns_toast": "Atualizado os servidores DNS upstream",
|
||||
@ -156,5 +158,93 @@
|
||||
"category_label": "Categoria",
|
||||
"rule_label": "Regra",
|
||||
"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."
|
||||
}
|
@ -115,9 +115,9 @@
|
||||
"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_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_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_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_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 <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 <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)",
|
||||
"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",
|
||||
|
@ -28,6 +28,7 @@
|
||||
"dhcp_ip_addresses": "IP-adresser",
|
||||
"dhcp_table_hostname": "V\u00e4rdnamn",
|
||||
"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",
|
||||
"dashboard": "Kontrollpanel",
|
||||
"settings": "Inst\u00e4llningar",
|
||||
@ -115,10 +116,11 @@
|
||||
"example_comment": "! H\u00e4r kommer en kommentar",
|
||||
"example_comment_meaning": "Endast 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_dot": "krypterat <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-over-TLS<\/a>",
|
||||
"example_upstream_doh": "krypterat <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a>",
|
||||
"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_dot": "krypterat <0>DNS-over-TLS<\/0>",
|
||||
"example_upstream_doh": "krypterat <0>DNS-over-HTTPS<\/0>",
|
||||
"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)",
|
||||
"all_filters_up_to_date_toast": "Alla filter \u00e4r redan aktuella",
|
||||
"updated_upstream_dns_toast": "Uppdaterade uppstr\u00f6ms-dns-servrar",
|
||||
@ -156,5 +158,93 @@
|
||||
"category_label": "Kategori",
|
||||
"rule_label": "Regel",
|
||||
"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."
|
||||
}
|
@ -113,9 +113,9 @@
|
||||
"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_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_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_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_dot": "\u0111\u01b0\u1ee3c m\u00e3 ho\u00e1 <0>DNS-over-TLS<\/0>",
|
||||
"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 <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)",
|
||||
"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",
|
||||
|
253
client/src/__locales/zh-cn.json
Normal file
253
client/src/__locales/zh-cn.json
Normal 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"
|
||||
}
|
@ -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",
|
||||
"check_dhcp_servers": "\u6aa2\u67e5\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668",
|
||||
"save_config": "\u5132\u5b58\u914d\u7f6e",
|
||||
@ -79,7 +82,7 @@
|
||||
"no_settings": "\u7121\u8a2d\u5b9a",
|
||||
"general_settings": "\u4e00\u822c\u7684\u8a2d\u5b9a",
|
||||
"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",
|
||||
"apply_btn": "\u5957\u7528",
|
||||
"disabled_filtering_toast": "\u5df2\u7981\u7528\u904e\u6ffe",
|
||||
@ -116,11 +119,11 @@
|
||||
"example_comment": "! \u770b\uff0c\u4e00\u500b\u8a3b\u89e3",
|
||||
"example_comment_meaning": "\u53ea\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_dot": "\u52a0\u5bc6\u7684 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-over-TLS<\/a>",
|
||||
"example_upstream_doh": "\u52a0\u5bc6\u7684 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS <\/a>",
|
||||
"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_dot": "\u52a0\u5bc6\u7684 <0>DNS-over-TLS<\/0>",
|
||||
"example_upstream_doh": "\u52a0\u5bc6\u7684 <0>DNS-over-HTTPS <\/0>",
|
||||
"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",
|
||||
"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",
|
||||
@ -210,5 +213,43 @@
|
||||
"next": "\u4e0b\u4e00\u6b65",
|
||||
"open_dashboard": "\u958b\u555f\u5100\u8868\u677f",
|
||||
"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"
|
||||
}
|
@ -3,7 +3,7 @@ import round from 'lodash/round';
|
||||
import { t } from 'i18next';
|
||||
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 Api from '../api/Api';
|
||||
|
||||
@ -452,10 +452,18 @@ export const setUpstreamRequest = createAction('SET_UPSTREAM_REQUEST');
|
||||
export const setUpstreamFailure = createAction('SET_UPSTREAM_FAILURE');
|
||||
export const setUpstreamSuccess = createAction('SET_UPSTREAM_SUCCESS');
|
||||
|
||||
export const setUpstream = url => async (dispatch) => {
|
||||
export const setUpstream = config => async (dispatch) => {
|
||||
dispatch(setUpstreamRequest());
|
||||
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(setUpstreamSuccess());
|
||||
} catch (error) {
|
||||
@ -468,11 +476,18 @@ export const testUpstreamRequest = createAction('TEST_UPSTREAM_REQUEST');
|
||||
export const testUpstreamFailure = createAction('TEST_UPSTREAM_FAILURE');
|
||||
export const testUpstreamSuccess = createAction('TEST_UPSTREAM_SUCCESS');
|
||||
|
||||
export const testUpstream = servers => async (dispatch) => {
|
||||
export const testUpstream = config => async (dispatch) => {
|
||||
dispatch(testUpstreamRequest());
|
||||
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 message = upstreamResponse[key];
|
||||
if (message !== 'OK') {
|
||||
|
@ -34,7 +34,7 @@ export default class Api {
|
||||
GLOBAL_QUERY_LOG = { path: 'querylog', method: 'GET' };
|
||||
GLOBAL_QUERY_LOG_ENABLE = { path: 'querylog_enable', 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_VERSION = { path: 'version.json', method: 'GET' };
|
||||
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 config = {
|
||||
data: url,
|
||||
header: { 'Content-Type': 'text/plain' },
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
};
|
||||
return this.makeRequest(path, method, config);
|
||||
}
|
||||
@ -119,7 +119,7 @@ export default class Api {
|
||||
const { path, method } = this.GLOBAL_TEST_UPSTREAM_DNS;
|
||||
const config = {
|
||||
data: servers,
|
||||
header: { 'Content-Type': 'text/plain' },
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
};
|
||||
return this.makeRequest(path, method, config);
|
||||
}
|
||||
|
@ -14,8 +14,9 @@ import Dashboard from '../../containers/Dashboard';
|
||||
import Settings from '../../containers/Settings';
|
||||
import Filters from '../../containers/Filters';
|
||||
import Logs from '../../containers/Logs';
|
||||
import Footer from '../ui/Footer';
|
||||
import SetupGuide from '../../containers/SetupGuide';
|
||||
import Toasts from '../Toasts';
|
||||
import Footer from '../ui/Footer';
|
||||
import Status from '../ui/Status';
|
||||
import UpdateTopline from '../ui/UpdateTopline';
|
||||
import EncryptionTopline from '../ui/EncryptionTopline';
|
||||
@ -86,6 +87,7 @@ class App extends Component {
|
||||
<Route path="/settings" component={Settings} />
|
||||
<Route path="/filters" component={Filters} />
|
||||
<Route path="/logs" component={Logs} />
|
||||
<Route path="/guide" component={SetupGuide} />
|
||||
</Fragment>
|
||||
}
|
||||
</div>
|
||||
|
@ -76,6 +76,13 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.nav-version__link {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
border-bottom: 1px dashed #495057;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.header-brand-img {
|
||||
height: 32px;
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
|
||||
import enhanceWithClickOutside from 'react-click-outside';
|
||||
import classnames from 'classnames';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
import { REPOSITORY } from '../../helpers/constants';
|
||||
|
||||
class Menu extends Component {
|
||||
handleClickOutside = () => {
|
||||
@ -56,10 +55,10 @@ class Menu extends Component {
|
||||
</NavLink>
|
||||
</li>
|
||||
<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>
|
||||
<Trans>faq</Trans>
|
||||
</a>
|
||||
<Trans>setup_guide</Trans>
|
||||
</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -2,24 +2,35 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
|
||||
import { getDnsAddress } from '../../helpers/helpers';
|
||||
|
||||
function Version(props) {
|
||||
const { dnsVersion, dnsAddress, dnsPort } = props;
|
||||
const { dnsVersion, dnsAddresses, dnsPort } = props;
|
||||
return (
|
||||
<div className="nav-version">
|
||||
<div className="nav-version__text">
|
||||
<Trans>version</Trans>: <span className="nav-version__value">{dnsVersion}</span>
|
||||
</div>
|
||||
<div className="nav-version__text">
|
||||
<Trans>address</Trans>: <span className="nav-version__value">{dnsAddress}:{dnsPort}</span>
|
||||
<div className="nav-version__link">
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
Version.propTypes = {
|
||||
dnsVersion: PropTypes.string,
|
||||
dnsAddress: PropTypes.string,
|
||||
dnsPort: PropTypes.number,
|
||||
dnsVersion: PropTypes.string.isRequired,
|
||||
dnsAddresses: PropTypes.array.isRequired,
|
||||
dnsPort: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export default withNamespaces()(Version);
|
||||
|
@ -56,11 +56,13 @@ class Header extends Component {
|
||||
toggleMenuOpen={this.toggleMenuOpen}
|
||||
closeMenu={this.closeMenu}
|
||||
/>
|
||||
<div className="col col-sm-6 col-lg-3">
|
||||
<Version
|
||||
{ ...this.props.dashboard }
|
||||
/>
|
||||
</div>
|
||||
{!dashboard.processing &&
|
||||
<div className="col col-sm-6 col-lg-3">
|
||||
<Version
|
||||
{ ...this.props.dashboard }
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -57,6 +57,7 @@ let Form = (props) => {
|
||||
valid_chain,
|
||||
valid_key,
|
||||
valid_cert,
|
||||
valid_pair,
|
||||
dns_names,
|
||||
key_type,
|
||||
issuer,
|
||||
@ -65,6 +66,15 @@ let Form = (props) => {
|
||||
setTlsConfig,
|
||||
} = props;
|
||||
|
||||
const isSavingDisabled = invalid
|
||||
|| submitting
|
||||
|| processingConfig
|
||||
|| processingValidate
|
||||
|| (isEnabled && (!privateKey || !certificateChain))
|
||||
|| (privateKey && !valid_key)
|
||||
|| (certificateChain && !valid_cert)
|
||||
|| (privateKey && certificateChain && !valid_pair);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="row">
|
||||
@ -291,15 +301,7 @@ let Form = (props) => {
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-success btn-standart"
|
||||
disabled={
|
||||
invalid
|
||||
|| submitting
|
||||
|| processingConfig
|
||||
|| processingValidate
|
||||
|| (isEnabled && (!privateKey || !certificateChain))
|
||||
|| (privateKey && !valid_key)
|
||||
|| (certificateChain && !valid_cert)
|
||||
}
|
||||
disabled={isSavingDisabled}
|
||||
>
|
||||
<Trans>save_config</Trans>
|
||||
</button>
|
||||
@ -334,6 +336,7 @@ Form.propTypes = {
|
||||
valid_chain: PropTypes.bool,
|
||||
valid_key: PropTypes.bool,
|
||||
valid_cert: PropTypes.bool,
|
||||
valid_pair: PropTypes.bool,
|
||||
dns_names: PropTypes.string,
|
||||
key_type: PropTypes.string,
|
||||
issuer: PropTypes.string,
|
||||
|
@ -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);
|
85
client/src/components/Settings/Upstream/Examples.js
Normal file
85
client/src/components/Settings/Upstream/Examples.js
Normal 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> –
|
||||
<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> –
|
||||
<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> –
|
||||
<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> –
|
||||
<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);
|
144
client/src/components/Settings/Upstream/Form.js
Normal file
144
client/src/components/Settings/Upstream/Form.js
Normal 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);
|
64
client/src/components/Settings/Upstream/index.js
Normal file
64
client/src/components/Settings/Upstream/index.js
Normal 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);
|
@ -41,22 +41,6 @@ class Settings extends Component {
|
||||
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) => {
|
||||
if (Object.keys(settings).length > 0) {
|
||||
return Object.keys(settings).map((key) => {
|
||||
@ -75,8 +59,7 @@ class Settings extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { settings, t } = this.props;
|
||||
const { upstreamDns } = this.props.dashboard;
|
||||
const { settings, dashboard, t } = this.props;
|
||||
return (
|
||||
<Fragment>
|
||||
<PageTitle title={ t('settings') } />
|
||||
@ -91,11 +74,13 @@ class Settings extends Component {
|
||||
</div>
|
||||
</Card>
|
||||
<Upstream
|
||||
upstreamDns={upstreamDns}
|
||||
upstreamDns={dashboard.upstreamDns}
|
||||
bootstrapDns={dashboard.bootstrapDns}
|
||||
allServers={dashboard.allServers}
|
||||
setUpstream={this.props.setUpstream}
|
||||
testUpstream={this.props.testUpstream}
|
||||
processingTestUpstream={settings.processingTestUpstream}
|
||||
handleUpstreamChange={this.handleUpstreamChange}
|
||||
handleUpstreamSubmit={this.handleUpstreamSubmit}
|
||||
handleUpstreamTest={this.handleUpstreamTest}
|
||||
processingSetUpstream={settings.processingSetUpstream}
|
||||
/>
|
||||
<Encryption
|
||||
encryption={this.props.encryption}
|
||||
@ -125,7 +110,6 @@ Settings.propTypes = {
|
||||
toggleSetting: PropTypes.func,
|
||||
handleUpstreamChange: PropTypes.func,
|
||||
setUpstream: PropTypes.func,
|
||||
upstream: PropTypes.string,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
|
15
client/src/components/SetupGuide/Guide.css
Normal file
15
client/src/components/SetupGuide/Guide.css
Normal 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;
|
||||
}
|
46
client/src/components/SetupGuide/index.js
Normal file
46
client/src/components/SetupGuide/index.js
Normal 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);
|
@ -91,6 +91,10 @@
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.checkbox__label-text--long {
|
||||
max-width: initial;
|
||||
}
|
||||
|
||||
.checkbox__label-title {
|
||||
display: block;
|
||||
line-height: 1.5;
|
||||
|
83
client/src/components/ui/Guide.js
Normal file
83
client/src/components/ui/Guide.js
Normal 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);
|
@ -22,6 +22,16 @@
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.popover__trigger--address {
|
||||
top: 0;
|
||||
margin: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.popover__trigger--address:after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.popover__body {
|
||||
content: "";
|
||||
display: flex;
|
||||
@ -57,6 +67,38 @@
|
||||
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__body:hover {
|
||||
visibility: visible;
|
||||
@ -73,6 +115,10 @@
|
||||
stroke: #66b574;
|
||||
}
|
||||
|
||||
.popover__list--bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.popover__list-title {
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
14
client/src/containers/SetupGuide.js
Normal file
14
client/src/containers/SetupGuide.js
Normal 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);
|
@ -47,6 +47,10 @@ export const LANGUAGES = [
|
||||
key: 'vi',
|
||||
name: 'Tiếng Việt',
|
||||
},
|
||||
{
|
||||
key: 'bg',
|
||||
name: 'Български',
|
||||
},
|
||||
{
|
||||
key: 'ru',
|
||||
name: 'Русский',
|
||||
@ -59,6 +63,10 @@ export const LANGUAGES = [
|
||||
key: 'zh-tw',
|
||||
name: '正體中文',
|
||||
},
|
||||
{
|
||||
key: 'zh-cn',
|
||||
name: '简体中文',
|
||||
},
|
||||
];
|
||||
|
||||
export const INSTALL_FIRST_STEP = 1;
|
||||
|
@ -32,7 +32,7 @@ export const renderSelectField = ({
|
||||
disabled={disabled}
|
||||
/>
|
||||
<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>
|
||||
</span>
|
||||
|
@ -201,3 +201,5 @@ export const redirectToCurrentProtocol = (values, httpPort = 80) => {
|
||||
window.location.replace(`http://${hostname}:${httpPort}/${hash}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const normalizeTextarea = text => text && text.replace(/[;, ]/g, '\n').split('\n').filter(n => n);
|
||||
|
@ -12,6 +12,8 @@ import ja from './__locales/ja.json';
|
||||
import sv from './__locales/sv.json';
|
||||
import ptBR from './__locales/pt-br.json';
|
||||
import zhTW from './__locales/zh-tw.json';
|
||||
import bg from './__locales/bg.json';
|
||||
import zhCN from './__locales/zh-cn.json';
|
||||
|
||||
const resources = {
|
||||
en: {
|
||||
@ -41,6 +43,12 @@ const resources = {
|
||||
'zh-TW': {
|
||||
translation: zhTW,
|
||||
},
|
||||
bg: {
|
||||
translation: bg,
|
||||
},
|
||||
'zh-CN': {
|
||||
translation: zhCN,
|
||||
},
|
||||
};
|
||||
|
||||
i18n
|
||||
|
@ -5,8 +5,7 @@ import { reduxForm, formValueSelector } from 'redux-form';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
import flow from 'lodash/flow';
|
||||
|
||||
import Tabs from '../../components/ui/Tabs';
|
||||
import Icons from '../../components/ui/Icons';
|
||||
import Guide from '../../components/ui/Guide';
|
||||
import Controls from './Controls';
|
||||
import AddressList from './AddressList';
|
||||
|
||||
@ -30,77 +29,7 @@ let Devices = props => (
|
||||
/>
|
||||
</div>
|
||||
</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>
|
||||
<Guide />
|
||||
</div>
|
||||
<Controls />
|
||||
</div>
|
||||
|
@ -70,6 +70,7 @@ const encryption = handleActions({
|
||||
valid_chain: false,
|
||||
valid_key: false,
|
||||
valid_cert: false,
|
||||
valid_pair: false,
|
||||
status_cert: '',
|
||||
status_key: '',
|
||||
certificate_chain: '',
|
||||
|
@ -48,9 +48,11 @@ const dashboard = handleActions({
|
||||
version,
|
||||
running,
|
||||
dns_port: dnsPort,
|
||||
dns_address: dnsAddress,
|
||||
dns_addresses: dnsAddresses,
|
||||
querylog_enabled: queryLogEnabled,
|
||||
upstream_dns: upstreamDns,
|
||||
bootstrap_dns: bootstrapDns,
|
||||
all_servers: allServers,
|
||||
protection_enabled: protectionEnabled,
|
||||
language,
|
||||
http_port: httpPort,
|
||||
@ -61,9 +63,11 @@ const dashboard = handleActions({
|
||||
processing: false,
|
||||
dnsVersion: version,
|
||||
dnsPort,
|
||||
dnsAddress,
|
||||
dnsAddresses,
|
||||
queryLogEnabled,
|
||||
upstreamDns: upstreamDns.join('\n'),
|
||||
bootstrapDns: bootstrapDns.join('\n'),
|
||||
allServers,
|
||||
protectionEnabled,
|
||||
language,
|
||||
httpPort,
|
||||
@ -171,10 +175,15 @@ const dashboard = handleActions({
|
||||
logStatusProcessing: false,
|
||||
processingVersion: true,
|
||||
processingFiltering: true,
|
||||
upstreamDns: [],
|
||||
upstreamDns: '',
|
||||
bootstrapDns: '',
|
||||
allServers: false,
|
||||
protectionEnabled: false,
|
||||
processingProtection: false,
|
||||
httpPort: 80,
|
||||
dnsPort: 53,
|
||||
dnsAddresses: [],
|
||||
dnsVersion: '',
|
||||
});
|
||||
|
||||
const queryLogs = handleActions({
|
||||
|
5
client/webpack.common.js
vendored
5
client/webpack.common.js
vendored
@ -5,12 +5,14 @@ const ExtractTextPlugin = require('extract-text-webpack-plugin');
|
||||
const webpack = require('webpack');
|
||||
const flexBugsFixes = require('postcss-flexbugs-fixes');
|
||||
const CleanWebpackPlugin = require('clean-webpack-plugin');
|
||||
const CopyPlugin = require('copy-webpack-plugin');
|
||||
|
||||
const RESOURCES_PATH = path.resolve(__dirname);
|
||||
const ENTRY_REACT = path.resolve(RESOURCES_PATH, 'src/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_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');
|
||||
|
||||
@ -117,6 +119,9 @@ const config = {
|
||||
new ExtractTextPlugin({
|
||||
filename: '[name].[contenthash].css',
|
||||
}),
|
||||
new CopyPlugin([
|
||||
{ from: FAVICON_PATH, to: PUBLIC_PATH },
|
||||
]),
|
||||
],
|
||||
};
|
||||
|
||||
|
49
config.go
49
config.go
@ -11,7 +11,8 @@ import (
|
||||
"github.com/AdguardTeam/AdGuardHome/dhcpd"
|
||||
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
|
||||
"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"
|
||||
)
|
||||
|
||||
@ -61,6 +62,9 @@ type dnsConfig struct {
|
||||
UpstreamDNS []string `yaml:"upstream_dns"`
|
||||
}
|
||||
|
||||
var defaultDNS = []string{"https://dns.cloudflare.com/dns-query"}
|
||||
var defaultBootstrap = []string{"1.1.1.1"}
|
||||
|
||||
type tlsConfigSettings struct {
|
||||
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
|
||||
@ -86,7 +90,7 @@ type tlsConfigStatus struct {
|
||||
KeyType string `yaml:"-" json:"key_type,omitempty"` // KeyType is one of RSA or ECDSA
|
||||
|
||||
// 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
|
||||
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"`
|
||||
}
|
||||
|
||||
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
|
||||
var config = configuration{
|
||||
ourConfigFilename: "AdGuardHome.yaml",
|
||||
@ -115,7 +117,8 @@ var config = configuration{
|
||||
QueryLogEnabled: true,
|
||||
Ratelimit: 20,
|
||||
RefuseAny: true,
|
||||
BootstrapDNS: "8.8.8.8:53",
|
||||
BootstrapDNS: defaultBootstrap,
|
||||
AllServers: false,
|
||||
},
|
||||
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: 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: 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,
|
||||
}
|
||||
@ -146,9 +149,15 @@ func init() {
|
||||
|
||||
// getConfigFilename returns path to the current config file
|
||||
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) {
|
||||
configFile = filepath.Join(config.ourWorkingDir, config.ourConfigFilename)
|
||||
configFile = filepath.Join(config.ourWorkingDir, configFile)
|
||||
}
|
||||
return configFile
|
||||
}
|
||||
@ -163,7 +172,7 @@ func getLogSettings() logSettings {
|
||||
}
|
||||
err = yaml.Unmarshal(yamlFile, &l)
|
||||
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
|
||||
}
|
||||
@ -171,19 +180,19 @@ func getLogSettings() logSettings {
|
||||
// parseConfig loads configuration from the YAML file
|
||||
func parseConfig() error {
|
||||
configFile := config.getConfigFilename()
|
||||
log.Printf("Reading config file: %s", configFile)
|
||||
log.Debug("Reading config file: %s", configFile)
|
||||
yamlFile, err := readConfigFile()
|
||||
if err != nil {
|
||||
log.Printf("Couldn't read config file: %s", err)
|
||||
log.Error("Couldn't read config file: %s", err)
|
||||
return err
|
||||
}
|
||||
if yamlFile == nil {
|
||||
log.Printf("YAML file doesn't exist, skipping it")
|
||||
log.Error("YAML file doesn't exist, skipping it")
|
||||
return nil
|
||||
}
|
||||
err = yaml.Unmarshal(yamlFile, &config)
|
||||
if err != nil {
|
||||
log.Printf("Couldn't parse config file: %s", err)
|
||||
log.Error("Couldn't parse config file: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
@ -210,19 +219,19 @@ func (c *configuration) write() error {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
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
|
||||
}
|
||||
configFile := config.getConfigFilename()
|
||||
log.Tracef("Writing YAML file: %s", configFile)
|
||||
log.Debug("Writing YAML file: %s", configFile)
|
||||
yamlText, err := yaml.Marshal(&config)
|
||||
if err != nil {
|
||||
log.Printf("Couldn't generate YAML file: %s", err)
|
||||
log.Error("Couldn't generate YAML file: %s", err)
|
||||
return err
|
||||
}
|
||||
err = safeWriteFile(configFile, yamlText)
|
||||
err = file.SafeWrite(configFile, yamlText)
|
||||
if err != nil {
|
||||
log.Printf("Couldn't save YAML config: %s", err)
|
||||
log.Error("Couldn't save YAML config: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
@ -232,14 +241,14 @@ func (c *configuration) write() error {
|
||||
func writeAllConfigs() error {
|
||||
err := config.write()
|
||||
if err != nil {
|
||||
log.Printf("Couldn't write config: %s", err)
|
||||
log.Error("Couldn't write config: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
userFilter := userFilter()
|
||||
err = userFilter.save()
|
||||
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
|
||||
}
|
||||
|
||||
|
794
control.go
794
control.go
File diff suppressed because it is too large
Load Diff
153
control_test.go
Normal file
153
control_test.go
Normal 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
338
control_tls.go
Normal 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
22
dhcp.go
@ -10,13 +10,14 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/dhcpd"
|
||||
"github.com/hmage/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/joomcode/errorx"
|
||||
)
|
||||
|
||||
var dhcpServer = dhcpd.Server{}
|
||||
|
||||
func handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
|
||||
log.Tracef("%s %v", r.Method, r.URL)
|
||||
rawLeases := dhcpServer.Leases()
|
||||
leases := []map[string]string{}
|
||||
for i := range rawLeases {
|
||||
@ -43,6 +44,7 @@ func handleDHCPStatus(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{}
|
||||
err := json.NewDecoder(r.Body).Decode(&newconfig)
|
||||
if err != nil {
|
||||
@ -50,6 +52,11 @@ func handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
err = dhcpServer.Stop()
|
||||
if err != nil {
|
||||
log.Error("failed to stop the DHCP server: %s", err)
|
||||
}
|
||||
|
||||
if newconfig.Enabled {
|
||||
err := dhcpServer.Start(&newconfig)
|
||||
if err != nil {
|
||||
@ -57,17 +64,13 @@ func handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if !newconfig.Enabled {
|
||||
err := dhcpServer.Stop()
|
||||
if err != nil {
|
||||
log.Printf("failed to stop the DHCP server: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
config.DHCP = newconfig
|
||||
httpUpdateConfigReloadDNSReturnOK(w, r)
|
||||
}
|
||||
|
||||
func handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
|
||||
log.Tracef("%s %v", r.Method, r.URL)
|
||||
response := map[string]interface{}{}
|
||||
|
||||
ifaces, err := getValidNetInterfaces()
|
||||
@ -128,10 +131,11 @@ func handleDHCPInterfaces(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)
|
||||
if err != nil {
|
||||
errorText := fmt.Sprintf("failed to read request body: %s", err)
|
||||
log.Println(errorText)
|
||||
log.Error(errorText)
|
||||
http.Error(w, errorText, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
@ -139,7 +143,7 @@ func handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Request) {
|
||||
interfaceName := strings.TrimSpace(string(body))
|
||||
if interfaceName == "" {
|
||||
errorText := fmt.Sprintf("empty interface name specified")
|
||||
log.Println(errorText)
|
||||
log.Error(errorText)
|
||||
http.Error(w, errorText, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
53
dhcpd/README.md
Normal file
53
dhcpd/README.md
Normal 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
|
@ -9,7 +9,7 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/hmage/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/krolaw/dhcp4"
|
||||
)
|
||||
|
||||
|
98
dhcpd/db.go
Normal file
98
dhcpd/db.go
Normal 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))
|
||||
}
|
355
dhcpd/dhcpd.go
355
dhcpd/dhcpd.go
@ -4,11 +4,13 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/hmage/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/krolaw/dhcp4"
|
||||
ping "github.com/sparrc/go-ping"
|
||||
)
|
||||
|
||||
const defaultDiscoverTime = time.Second * 3
|
||||
@ -32,6 +34,10 @@ type ServerConfig struct {
|
||||
RangeStart string `json:"range_start" yaml:"range_start"`
|
||||
RangeEnd string `json:"range_end" yaml:"range_end"`
|
||||
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
|
||||
@ -40,6 +46,11 @@ type Server struct {
|
||||
|
||||
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 []*Lease
|
||||
leaseStart net.IP // parsed from config RangeStart
|
||||
@ -54,6 +65,16 @@ type Server struct {
|
||||
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.
|
||||
// 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 {
|
||||
@ -64,6 +85,7 @@ func (s *Server) Start(config *ServerConfig) error {
|
||||
iface, err := net.InterfaceByName(s.InterfaceName)
|
||||
if err != nil {
|
||||
s.closeConn() // in case it was already started
|
||||
printInterfaces()
|
||||
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.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
|
||||
if err != nil {
|
||||
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.cond = sync.NewCond(&s.mutex)
|
||||
|
||||
s.running = true
|
||||
go func() {
|
||||
// operate on c instead of c.conn because c.conn can change over time
|
||||
err := dhcp4.Serve(c, s)
|
||||
if err != nil {
|
||||
if err != nil && !s.stopping {
|
||||
log.Printf("dhcp4.Serve() returned with error: %s", err)
|
||||
}
|
||||
c.Close() // in case Serve() exits for other reason than listening socket closure
|
||||
s.running = false
|
||||
s.cond.Signal()
|
||||
}()
|
||||
|
||||
return nil
|
||||
@ -147,11 +176,23 @@ func (s *Server) Stop() error {
|
||||
// nothing to do, return silently
|
||||
return nil
|
||||
}
|
||||
|
||||
s.stopping = true
|
||||
|
||||
err := s.closeConn()
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
@ -165,6 +206,7 @@ func (s *Server) closeConn() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Reserve a lease for the client
|
||||
func (s *Server) reserveLease(p dhcp4.Packet) (*Lease, error) {
|
||||
// WARNING: do not remove copy()
|
||||
// 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()
|
||||
hwaddr := make(net.HardwareAddr, len(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
|
||||
hostname := p.ParseOptions()[dhcp4.OptionHostName]
|
||||
lease := &Lease{HWAddr: hwaddr, Hostname: string(hostname)}
|
||||
|
||||
log.Tracef("Lease not found for %s: creating new one", hwaddr)
|
||||
ip, err := s.findFreeIP(hwaddr)
|
||||
if err != nil {
|
||||
return nil, wrapErrPrint(err, "Couldn't find free IP for the lease %s", hwaddr.String())
|
||||
i := s.findExpiredLease()
|
||||
if i < 0 {
|
||||
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())
|
||||
hostname := p.ParseOptions()[dhcp4.OptionHostName]
|
||||
lease := &Lease{HWAddr: hwaddr, IP: ip, Hostname: string(hostname)}
|
||||
lease.IP = ip
|
||||
s.Lock()
|
||||
s.leases = append(s.leases, lease)
|
||||
s.Unlock()
|
||||
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()
|
||||
for i := range s.leases {
|
||||
if bytes.Equal([]byte(hwaddr), []byte(s.leases[i].HWAddr)) {
|
||||
@ -203,6 +257,17 @@ func (s *Server) locateLease(p dhcp4.Packet) *Lease {
|
||||
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) {
|
||||
// if IP pool is nil, lazy initialize it
|
||||
if s.IPpool == nil {
|
||||
@ -213,13 +278,12 @@ func (s *Server) findFreeIP(hwaddr net.HardwareAddr) (net.IP, error) {
|
||||
var foundIP net.IP
|
||||
for i := 0; i < dhcp4.IPRange(s.leaseStart, s.leaseStop); 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)
|
||||
if foundHWaddr != nil && len(foundHWaddr) != 0 {
|
||||
// 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("will try again")
|
||||
continue
|
||||
}
|
||||
foundIP = newIP
|
||||
@ -236,7 +300,7 @@ func (s *Server) findFreeIP(hwaddr net.HardwareAddr) (net.IP, error) {
|
||||
return foundIP, nil
|
||||
}
|
||||
|
||||
func (s *Server) getIPpool(ip net.IP) net.HardwareAddr {
|
||||
func (s *Server) findReservedHWaddr(ip net.IP) net.HardwareAddr {
|
||||
rawIP := []byte(ip)
|
||||
IP4 := [4]byte{rawIP[0], rawIP[1], rawIP[2], rawIP[3]}
|
||||
return s.IPpool[IP4]
|
||||
@ -256,133 +320,230 @@ func (s *Server) unreserveIP(ip net.IP) {
|
||||
|
||||
// ServeDHCP handles an incoming DHCP request
|
||||
func (s *Server) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options dhcp4.Options) dhcp4.Packet {
|
||||
log.Tracef("Got %v message", msgType)
|
||||
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)
|
||||
}
|
||||
s.printLeases()
|
||||
|
||||
switch msgType {
|
||||
case dhcp4.Discover: // Broadcast Packet From Client - Can I have an IP?
|
||||
// find a lease, but don't update lease time
|
||||
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
|
||||
return s.handleDiscover(p, options)
|
||||
|
||||
case dhcp4.Request: // Broadcast From Client - I'll take that IP (Also start for renewals)
|
||||
// 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
|
||||
return s.handleDHCP4Request(p, options)
|
||||
|
||||
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
|
||||
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
|
||||
log.Tracef("Got from client: Inform")
|
||||
// do nothing
|
||||
return s.handleInform(p, options)
|
||||
|
||||
// from server -- ignore those but enumerate just in case
|
||||
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
|
||||
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
|
||||
log.Printf("SHOULD NOT HAPPEN -- FROM ANOTHER DHCP SERVER: NAK")
|
||||
log.Printf("DHCP: received message from %s: NAK", p.CHAddr())
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
log.Tracef("Got from client: Request")
|
||||
if server, ok := options[dhcp4.OptionServerIdentifier]; ok && !net.IP(server).Equal(s.ipnet.IP) {
|
||||
var lease *Lease
|
||||
|
||||
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)
|
||||
return nil // Message not for this dhcp server
|
||||
}
|
||||
|
||||
reqIP := net.IP(options[dhcp4.OptionRequestedIPAddress])
|
||||
if reqIP == nil {
|
||||
reqIP = p.CIAddr()
|
||||
}
|
||||
|
||||
if reqIP.To4() == nil {
|
||||
log.Tracef("Replying with NAK: request IP isn't valid IPv4: %s", reqIP)
|
||||
} else if reqIP == nil || reqIP.To4() == nil {
|
||||
log.Tracef("Requested IP isn't a valid IPv4: %s", reqIP)
|
||||
return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil)
|
||||
}
|
||||
|
||||
if reqIP.Equal(net.IPv4zero) {
|
||||
log.Tracef("Replying with NAK: request IP is 0.0.0.0")
|
||||
lease = s.findLease(p)
|
||||
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)
|
||||
}
|
||||
|
||||
log.Tracef("requested IP is %s", reqIP)
|
||||
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
|
||||
if !lease.IP.Equal(reqIP) {
|
||||
log.Tracef("Lease for %s doesn't match requested/client IP: %s vs %s",
|
||||
lease.HWAddr, lease.IP, reqIP)
|
||||
return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil)
|
||||
}
|
||||
|
||||
if lease.IP.Equal(reqIP) {
|
||||
// IP matches lease IP, nothing else to do
|
||||
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())
|
||||
return dhcp4.ReplyPacket(p, dhcp4.ACK, s.ipnet.IP, lease.IP, s.leaseTime, s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList]))
|
||||
}
|
||||
lease.Expiry = time.Now().Add(s.leaseTime)
|
||||
log.Tracef("Replying with ACK. IP: %s HW: %s Expire: %s",
|
||||
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
|
||||
//
|
||||
func (s *Server) handleInform(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet {
|
||||
log.Tracef("Message from client: Inform. IP: %s HW: %s",
|
||||
p.CIAddr(), p.CHAddr())
|
||||
|
||||
log.Tracef("lease IP is different from requested IP: %s vs %s", lease.IP, reqIP)
|
||||
return nil
|
||||
}
|
||||
|
||||
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]))
|
||||
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()
|
||||
for _, lease := range s.leases {
|
||||
if lease.Expiry.Unix() > now {
|
||||
result = append(result, *lease)
|
||||
}
|
||||
}
|
||||
|
||||
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) Leases() []*Lease {
|
||||
s.RLock()
|
||||
result := s.leases
|
||||
s.RUnlock()
|
||||
|
||||
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
170
dhcpd/dhcpd_test.go
Normal 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")
|
||||
}
|
@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/hmage/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/joomcode/errorx"
|
||||
)
|
||||
|
||||
|
@ -8,7 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/dhcpd"
|
||||
"github.com/hmage/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/krolaw/dhcp4"
|
||||
)
|
||||
|
||||
|
22
dns.go
22
dns.go
@ -7,8 +7,8 @@ import (
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
|
||||
"github.com/AdguardTeam/AdGuardHome/dnsforward"
|
||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||
"github.com/hmage/golibs/log"
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/joomcode/errorx"
|
||||
)
|
||||
|
||||
@ -58,19 +58,13 @@ func generateServerConfig() dnsforward.ServerConfig {
|
||||
}
|
||||
}
|
||||
|
||||
for _, u := range config.DNS.UpstreamDNS {
|
||||
opts := upstream.Options{
|
||||
Timeout: dnsforward.DefaultTimeout,
|
||||
Bootstrap: []string{config.DNS.BootstrapDNS},
|
||||
}
|
||||
dnsUpstream, err := upstream.AddressToUpstream(u, opts)
|
||||
if err != nil {
|
||||
log.Printf("Couldn't get upstream: %s", err)
|
||||
// continue, just ignore the upstream
|
||||
continue
|
||||
}
|
||||
newconfig.Upstreams = append(newconfig.Upstreams, dnsUpstream)
|
||||
upstreamConfig, err := proxy.ParseUpstreamsConfig(config.DNS.UpstreamDNS, config.DNS.BootstrapDNS, dnsforward.DefaultTimeout)
|
||||
if err != nil {
|
||||
log.Error("Couldn't get upstreams configuration cause: %s", err)
|
||||
}
|
||||
newconfig.Upstreams = upstreamConfig.Upstreams
|
||||
newconfig.DomainsReservedUpstreams = upstreamConfig.DomainReservedUpstreams
|
||||
newconfig.AllServers = config.DNS.AllServers
|
||||
return newconfig
|
||||
}
|
||||
|
||||
|
@ -16,8 +16,8 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/bluele/gcache"
|
||||
"github.com/hmage/golibs/log"
|
||||
"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) {
|
||||
if log.GetLevel() >= log.DEBUG {
|
||||
timer := log.StartTimer()
|
||||
defer timer.LogElapsed("SafeSearch HTTP lookup for %s", host)
|
||||
}
|
||||
|
||||
if safeSearchCache == nil {
|
||||
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)
|
||||
if isFound {
|
||||
atomic.AddUint64(&stats.Safesearch.CacheHits, 1)
|
||||
log.Tracef("%s: found in SafeSearch cache", host)
|
||||
return cachedValue, nil
|
||||
}
|
||||
|
||||
@ -656,6 +662,11 @@ func (d *Dnsfilter) checkSafeSearch(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
|
||||
if host == d.safeBrowsingServer {
|
||||
return Result{}, nil
|
||||
@ -697,6 +708,11 @@ func (d *Dnsfilter) checkSafeBrowsing(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
|
||||
if host == d.parentalServer {
|
||||
return Result{}, nil
|
||||
@ -754,6 +770,7 @@ func (d *Dnsfilter) lookupCommon(host string, lookupstats *LookupStats, cache gc
|
||||
cachedValue, isFound, err := getCachedReason(cache, host)
|
||||
if isFound {
|
||||
atomic.AddUint64(&lookupstats.CacheHits, 1)
|
||||
log.Tracef("%s: found in the lookup cache", host)
|
||||
return cachedValue, nil
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -18,7 +18,7 @@ import (
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/hmage/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/shirou/gopsutil/process"
|
||||
"go.uber.org/goleak"
|
||||
)
|
||||
|
@ -13,7 +13,7 @@ import (
|
||||
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||
"github.com/hmage/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/joomcode/errorx"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
@ -62,11 +62,12 @@ type FilteringConfig struct {
|
||||
ProtectionEnabled bool `yaml:"protection_enabled"` // whether or not use any of dnsfilter features
|
||||
FilteringEnabled bool `yaml:"filtering_enabled"` // whether or not use filter lists
|
||||
BlockedResponseTTL uint32 `yaml:"blocked_response_ttl"` // if 0, then default is used (3600)
|
||||
QueryLogEnabled bool `yaml:"querylog_enabled"`
|
||||
Ratelimit int `yaml:"ratelimit"`
|
||||
RatelimitWhitelist []string `yaml:"ratelimit_whitelist"`
|
||||
RefuseAny bool `yaml:"refuse_any"`
|
||||
BootstrapDNS string `yaml:"bootstrap_dns"`
|
||||
QueryLogEnabled bool `yaml:"querylog_enabled"` // if true, query log is enabled
|
||||
Ratelimit int `yaml:"ratelimit"` // max number of requests per second from a given IP (0 to disable)
|
||||
RatelimitWhitelist []string `yaml:"ratelimit_whitelist"` // a list of whitelisted client IP addresses
|
||||
RefuseAny bool `yaml:"refuse_any"` // if true, refuse ANY requests
|
||||
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"`
|
||||
}
|
||||
@ -81,10 +82,11 @@ type TLSConfig struct {
|
||||
// ServerConfig represents server configuration.
|
||||
// The zero ServerConfig is empty and ready for use.
|
||||
type ServerConfig struct {
|
||||
UDPListenAddr *net.UDPAddr // UDP listen address
|
||||
TCPListenAddr *net.TCPAddr // TCP listen address
|
||||
Upstreams []upstream.Upstream // Configured upstreams
|
||||
Filters []dnsfilter.Filter // A list of filters to use
|
||||
UDPListenAddr *net.UDPAddr // UDP listen address
|
||||
TCPListenAddr *net.TCPAddr // TCP listen address
|
||||
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
|
||||
|
||||
FilteringConfig
|
||||
TLSConfig
|
||||
@ -155,14 +157,16 @@ func (s *Server) startInternal(config *ServerConfig) error {
|
||||
})
|
||||
|
||||
proxyConfig := proxy.Config{
|
||||
UDPListenAddr: s.UDPListenAddr,
|
||||
TCPListenAddr: s.TCPListenAddr,
|
||||
Ratelimit: s.Ratelimit,
|
||||
RatelimitWhitelist: s.RatelimitWhitelist,
|
||||
RefuseAny: s.RefuseAny,
|
||||
CacheEnabled: true,
|
||||
Upstreams: s.Upstreams,
|
||||
Handler: s.handleDNSRequest,
|
||||
UDPListenAddr: s.UDPListenAddr,
|
||||
TCPListenAddr: s.TCPListenAddr,
|
||||
Ratelimit: s.Ratelimit,
|
||||
RatelimitWhitelist: s.RatelimitWhitelist,
|
||||
RefuseAny: s.RefuseAny,
|
||||
CacheEnabled: true,
|
||||
Upstreams: s.Upstreams,
|
||||
DomainsReservedUpstreams: s.DomainsReservedUpstreams,
|
||||
Handler: s.handleDNSRequest,
|
||||
AllServers: s.AllServers,
|
||||
}
|
||||
|
||||
if s.TLSListenAddr != nil && s.CertificateChain != "" && s.PrivateKey != "" {
|
||||
|
@ -10,7 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
|
||||
"github.com/hmage/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
|
@ -9,8 +9,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/go-test/deep"
|
||||
"github.com/hmage/golibs/log"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -28,7 +28,7 @@ func (l *queryLog) flushLogBuffer() error {
|
||||
l.logBufferLock.Unlock()
|
||||
err := l.flushToFile(flushBuffer)
|
||||
if err != nil {
|
||||
log.Printf("Saving querylog to file failed: %s", err)
|
||||
log.Error("Saving querylog to file failed: %s", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@ -46,17 +46,17 @@ func (l *queryLog) flushToFile(buffer []*logEntry) error {
|
||||
for _, entry := range buffer {
|
||||
err := e.Encode(entry)
|
||||
if err != nil {
|
||||
log.Printf("Failed to marshal entry: %s", err)
|
||||
log.Error("Failed to marshal entry: %s", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
log.Printf("failed to check buffer: %s", err)
|
||||
log.Error("failed to check buffer: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
@ -73,13 +73,13 @@ func (l *queryLog) flushToFile(buffer []*logEntry) error {
|
||||
|
||||
_, err = zw.Write(b.Bytes())
|
||||
if err != nil {
|
||||
log.Printf("Couldn't compress to gzip: %s", err)
|
||||
log.Error("Couldn't compress to gzip: %s", err)
|
||||
zw.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
} else {
|
||||
@ -90,18 +90,18 @@ func (l *queryLog) flushToFile(buffer []*logEntry) error {
|
||||
defer fileWriteLock.Unlock()
|
||||
f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
|
||||
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
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
n, err := f.Write(zb.Bytes())
|
||||
if err != nil {
|
||||
log.Printf("Couldn't write to file: %s", err)
|
||||
log.Error("Couldn't write to file: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("ok \"%s\": %v bytes written", filename, n)
|
||||
log.Debug("ok \"%s\": %v bytes written", filename, n)
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -115,21 +115,21 @@ func checkBuffer(buffer []*logEntry, b bytes.Buffer) error {
|
||||
entry := &logEntry{}
|
||||
err := d.Decode(entry)
|
||||
if err != nil {
|
||||
log.Printf("Failed to decode: %s", err)
|
||||
log.Error("Failed to decode: %s", err)
|
||||
return err
|
||||
}
|
||||
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)
|
||||
}
|
||||
i++
|
||||
}
|
||||
if i != l {
|
||||
err := fmt.Errorf("check fail: %d vs %d entries", l, i)
|
||||
log.Print(err)
|
||||
log.Error("%v", err)
|
||||
return err
|
||||
}
|
||||
log.Printf("check ok: %d entries", i)
|
||||
log.Debug("check ok: %d entries", i)
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -150,11 +150,11 @@ func (l *queryLog) rotateQueryLog() error {
|
||||
|
||||
err := os.Rename(from, to)
|
||||
if err != nil {
|
||||
log.Printf("Failed to rename querylog: %s", err)
|
||||
log.Error("Failed to rename querylog: %s", 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
|
||||
}
|
||||
@ -163,7 +163,7 @@ func (l *queryLog) periodicQueryLogRotate() {
|
||||
for range time.Tick(queryLogRotationPeriod) {
|
||||
err := l.rotateQueryLog()
|
||||
if err != nil {
|
||||
log.Printf("Failed to rotate querylog: %s", err)
|
||||
log.Error("Failed to rotate querylog: %s", err)
|
||||
// do nothing, continue rotating
|
||||
}
|
||||
}
|
||||
@ -198,7 +198,7 @@ func (l *queryLog) genericLoader(onEntry func(entry *logEntry) error, needMore f
|
||||
|
||||
f, err := os.Open(file)
|
||||
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
|
||||
continue
|
||||
}
|
||||
@ -209,7 +209,7 @@ func (l *queryLog) genericLoader(onEntry func(entry *logEntry) error, needMore f
|
||||
if enableGzip {
|
||||
zr, err := gzip.NewReader(f)
|
||||
if err != nil {
|
||||
log.Printf("Failed to create gzip reader: %s", err)
|
||||
log.Error("Failed to create gzip reader: %s", err)
|
||||
continue
|
||||
}
|
||||
defer zr.Close()
|
||||
@ -231,7 +231,7 @@ func (l *queryLog) genericLoader(onEntry func(entry *logEntry) error, needMore f
|
||||
var entry logEntry
|
||||
err := d.Decode(&entry)
|
||||
if err != nil {
|
||||
log.Printf("Failed to decode: %s", err)
|
||||
log.Error("Failed to decode: %s", err)
|
||||
// next entry can be fine, try more
|
||||
continue
|
||||
}
|
||||
@ -260,7 +260,7 @@ func (l *queryLog) genericLoader(onEntry func(entry *logEntry) error, needMore f
|
||||
perunit = elapsed / 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
|
||||
}
|
||||
|
@ -9,8 +9,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/bluele/gcache"
|
||||
"github.com/hmage/golibs/log"
|
||||
"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, "."))
|
||||
|
||||
// if question hostname is empty, do nothing
|
||||
if hostname == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// get value, if not set, crate one
|
||||
d.hoursReadLock()
|
||||
defer d.hoursReadUnlock()
|
||||
|
224
filter.go
224
filter.go
@ -2,17 +2,18 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
|
||||
"github.com/hmage/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/file"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -27,6 +28,7 @@ type filter struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
RulesCount int `json:"rulesCount" yaml:"-"`
|
||||
LastUpdated time.Time `json:"lastUpdated,omitempty" yaml:"-"`
|
||||
checksum uint32 // checksum of the file data
|
||||
|
||||
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() {
|
||||
// Deduplicate filters
|
||||
i := 0 // output index, used for deletion later
|
||||
@ -83,41 +167,83 @@ func periodicallyRefreshFilters() {
|
||||
|
||||
// Checks filters updates if necessary
|
||||
// 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 {
|
||||
config.Lock()
|
||||
var updateFilters []filter
|
||||
|
||||
// fetch URLs
|
||||
updateCount := 0
|
||||
config.RLock()
|
||||
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
|
||||
filter.ID = assignUniqueFilterID()
|
||||
if !f.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(filter.Rules) == 0 {
|
||||
// Try reloading filter from the disk before updating
|
||||
// This is useful for the case when we simply enable a previously downloaded filter
|
||||
_ = filter.load()
|
||||
if !force && time.Since(f.LastUpdated) <= updatePeriod {
|
||||
continue
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
}
|
||||
if updated {
|
||||
// Saving it to the filters dir now
|
||||
err = filter.save()
|
||||
err = uf.save()
|
||||
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
|
||||
}
|
||||
|
||||
} 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++
|
||||
}
|
||||
config.Unlock()
|
||||
}
|
||||
config.Unlock()
|
||||
|
||||
if updateCount > 0 && isRunning() {
|
||||
err := reconfigureDNSServer()
|
||||
@ -138,13 +264,19 @@ func parseFilterContents(contents []byte) (int, string, []string) {
|
||||
|
||||
// Count lines in the filter
|
||||
for _, line := range lines {
|
||||
|
||||
line = strings.TrimSpace(line)
|
||||
if len(line) > 0 && line[0] == '!' {
|
||||
if m := filterTitleRegexp.FindAllStringSubmatch(line, -1); len(m) > 0 && len(m[0]) >= 2 && !seenTitle {
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if line[0] == '!' {
|
||||
m := filterTitleRegexp.FindAllStringSubmatch(line, -1)
|
||||
if len(m) > 0 && len(m[0]) >= 2 && !seenTitle {
|
||||
name = m[0][1]
|
||||
seenTitle = true
|
||||
}
|
||||
} else if len(line) != 0 {
|
||||
} else {
|
||||
rulesCount++
|
||||
}
|
||||
}
|
||||
@ -152,20 +284,8 @@ func parseFilterContents(contents []byte) (int, string, []string) {
|
||||
return rulesCount, name, lines
|
||||
}
|
||||
|
||||
// Checks for filters updates
|
||||
// If "force" is true -- does not check the filter's LastUpdated field
|
||||
// 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
|
||||
}
|
||||
|
||||
// Perform upgrade on a filter
|
||||
func (filter *filter) update() (bool, error) {
|
||||
log.Tracef("Downloading update for filter %d from %s", filter.ID, filter.URL)
|
||||
|
||||
resp, err := client.Get(filter.URL)
|
||||
@ -194,22 +314,22 @@ func (filter *filter) update(force bool) (bool, error) {
|
||||
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
|
||||
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)
|
||||
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)
|
||||
if filterName != "" {
|
||||
filter.Name = filterName
|
||||
}
|
||||
filter.RulesCount = rulesCount
|
||||
filter.Rules = rules
|
||||
filter.checksum = checksum
|
||||
|
||||
return true, nil
|
||||
}
|
||||
@ -220,7 +340,7 @@ func (filter *filter) save() error {
|
||||
log.Printf("Saving filter %d contents to: %s", filter.ID, filterFilePath)
|
||||
body := []byte(strings.Join(filter.Rules, "\n"))
|
||||
|
||||
err := safeWriteFile(filterFilePath, body)
|
||||
err := file.SafeWrite(filterFilePath, body)
|
||||
|
||||
// update LastUpdated field after saving the file
|
||||
filter.LastUpdated = filter.LastTimeUpdated()
|
||||
@ -229,11 +349,6 @@ func (filter *filter) save() error {
|
||||
|
||||
// loads filter contents from the file in dataDir
|
||||
func (filter *filter) load() error {
|
||||
if !filter.Enabled {
|
||||
// No need to load a filter that is not enabled
|
||||
return nil
|
||||
}
|
||||
|
||||
filterFilePath := filter.Path()
|
||||
log.Tracef("Loading filter %d contents to: %s", filter.ID, filterFilePath)
|
||||
|
||||
@ -252,25 +367,32 @@ func (filter *filter) load() error {
|
||||
|
||||
filter.RulesCount = rulesCount
|
||||
filter.Rules = rules
|
||||
filter.checksum = crc32.ChecksumIEEE(filterFileContents)
|
||||
filter.LastUpdated = filter.LastTimeUpdated()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clear filter rules
|
||||
func (filter *filter) unload() {
|
||||
filter.Rules = []string{}
|
||||
filter.RulesCount = 0
|
||||
}
|
||||
|
||||
// Path to the filter contents
|
||||
func (filter *filter) Path() string {
|
||||
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 {
|
||||
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
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
s, err := os.Stat(filterFilePath)
|
||||
if err != nil {
|
||||
// if the filter file does not exist, return 0001-01-01
|
||||
return time.Time{}
|
||||
|
9
go.mod
9
go.mod
@ -1,13 +1,15 @@
|
||||
module github.com/AdguardTeam/AdGuardHome
|
||||
|
||||
go 1.12
|
||||
|
||||
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/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-test/deep v1.0.1
|
||||
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/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 // indirect
|
||||
github.com/kardianos/service v0.0.0-20181115005516-4c239ee84e7b
|
||||
@ -15,6 +17,7 @@ require (
|
||||
github.com/miekg/dns v1.1.1
|
||||
github.com/shirou/gopsutil v2.18.10+incompatible
|
||||
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
|
||||
go.uber.org/goleak v0.10.0
|
||||
golang.org/x/net v0.0.0-20190119204137-ed066c81e75e
|
||||
|
15
go.sum
15
go.sum
@ -1,5 +1,8 @@
|
||||
github.com/AdguardTeam/dnsproxy v0.11.1 h1:qO5VH0GYF9vdksQRG8frEfJ+CJjsPBwuct8FH6Mij7o=
|
||||
github.com/AdguardTeam/dnsproxy v0.11.1/go.mod h1:lEi2srAWwfSQWoy8GeZR6lwS+FSMoiZid8bQPreOLb0=
|
||||
github.com/AdguardTeam/dnsproxy v0.12.0 h1:BPgv2PlH2u4xakFcaW4EqU3Visk1BNidrqGSgxe5Qzg=
|
||||
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/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
||||
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/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/bluele/gcache v0.0.0-20171010155617-472614239ac7 h1:NpQ+gkFOH27AyDypSCJ/LdsIi/b4rdnEb1N5+IpFfYs=
|
||||
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 h1:vUdUwmQLnT/yuk8PsDhhMVkrfr4aMdcv/0GWzIqOjEY=
|
||||
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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/packr v1.19.0 h1:3UDmBDxesCOPF8iZdMDBBWKfkBoYujIMIZePnobqIUI=
|
||||
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/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
|
||||
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/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/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/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||
|
27
helpers.go
27
helpers.go
@ -5,7 +5,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@ -19,26 +18,6 @@ import (
|
||||
"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
|
||||
// ----------------------------------
|
||||
@ -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)
|
||||
return
|
||||
}
|
||||
|
||||
if method == "POST" || method == "PUT" || method == "DELETE" {
|
||||
controlLock.Lock()
|
||||
defer controlLock.Unlock()
|
||||
}
|
||||
|
||||
handler(w, r)
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ package main
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hmage/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
func TestGetValidNetInterfacesForWeb(t *testing.T) {
|
||||
|
4
i18n.go
4
i18n.go
@ -6,7 +6,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/hmage/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// --------------------
|
||||
@ -22,6 +22,8 @@ var allowedLanguages = map[string]bool{
|
||||
"sv": true,
|
||||
"pt-br": true,
|
||||
"zh-tw": true,
|
||||
"bg": true,
|
||||
"zh-cn": true,
|
||||
}
|
||||
|
||||
func isLanguageAllowed(language string) bool {
|
||||
|
@ -87,26 +87,21 @@ paths:
|
||||
200:
|
||||
description: OK
|
||||
|
||||
/set_upstream_dns:
|
||||
/set_upstreams_config:
|
||||
post:
|
||||
tags:
|
||||
- global
|
||||
operationId: setUpstreamDNS
|
||||
summary: 'Set upstream DNS for coredns, empty value will reset it to default values'
|
||||
operationId: setUpstreamsConfig
|
||||
summary: "Updates the current upstreams configuration"
|
||||
consumes:
|
||||
- text/plain
|
||||
- application/json
|
||||
parameters:
|
||||
- in: body
|
||||
name: upstream
|
||||
description: 'Upstream servers, separated by newline or space, port is optional after colon'
|
||||
schema:
|
||||
# TODO: use JSON
|
||||
type: string
|
||||
example: |
|
||||
1.1.1.1
|
||||
1.0.0.1
|
||||
8.8.8.8 8.8.4.4
|
||||
192.168.1.104:53535
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "Upstreams configuration JSON"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/UpstreamsConfig"
|
||||
responses:
|
||||
200:
|
||||
description: OK
|
||||
@ -116,21 +111,15 @@ paths:
|
||||
tags:
|
||||
- global
|
||||
operationId: testUpstreamDNS
|
||||
summary: 'Test upstream DNS'
|
||||
summary: "Test upstream configuration"
|
||||
consumes:
|
||||
- text/plain
|
||||
- application/json
|
||||
parameters:
|
||||
- in: body
|
||||
name: upstream
|
||||
description: 'Upstream servers, separated by newline or space, port is optional after colon'
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "Upstream configuration to be tested"
|
||||
schema:
|
||||
# TODO: use JSON
|
||||
type: string
|
||||
example: |
|
||||
1.1.1.1
|
||||
1.0.0.1
|
||||
8.8.8.8 8.8.4.4
|
||||
192.168.1.104:53535
|
||||
$ref: "#/definitions/UpstreamsConfig"
|
||||
responses:
|
||||
200:
|
||||
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:
|
||||
type: "string"
|
||||
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:
|
||||
type: "object"
|
||||
description: "Filter subscription info"
|
||||
@ -1247,6 +1263,10 @@ definitions:
|
||||
type: "string"
|
||||
example: "You have specified an empty certificate"
|
||||
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:
|
||||
type: "object"
|
||||
description: "Network interface info"
|
||||
|
@ -14,6 +14,8 @@ const LOCALES_LIST = [
|
||||
'sv',
|
||||
'pt-br',
|
||||
'zh-tw',
|
||||
'bg',
|
||||
'zh-cn',
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -4,7 +4,7 @@ import (
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/hmage/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/kardianos/service"
|
||||
)
|
||||
|
||||
@ -127,11 +127,6 @@ func configureService(c *service.Config) {
|
||||
// POSIX
|
||||
// Redirect StdErr & StdOut to files.
|
||||
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
|
||||
|
70
upgrade.go
70
upgrade.go
@ -6,11 +6,12 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/hmage/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/file"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
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
|
||||
func upgradeConfig() error {
|
||||
@ -59,12 +60,17 @@ func upgradeConfig() error {
|
||||
func upgradeConfigSchema(oldVersion int, diskConfig *map[string]interface{}) error {
|
||||
switch oldVersion {
|
||||
case 0:
|
||||
err := upgradeSchema0to2(diskConfig)
|
||||
err := upgradeSchema0to3(diskConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case 1:
|
||||
err := upgradeSchema1to2(diskConfig)
|
||||
err := upgradeSchema1to3(diskConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case 2:
|
||||
err := upgradeSchema2to3(diskConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -81,7 +87,7 @@ func upgradeConfigSchema(oldVersion int, diskConfig *map[string]interface{}) err
|
||||
return err
|
||||
}
|
||||
|
||||
err = safeWriteFile(configFile, body)
|
||||
err = file.SafeWrite(configFile, body)
|
||||
if err != nil {
|
||||
log.Printf("Couldn't save YAML config: %s", err)
|
||||
return err
|
||||
@ -135,12 +141,60 @@ func upgradeSchema1to2(diskConfig *map[string]interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// jump two schemas at once -- this time we just do it sequentially
|
||||
func upgradeSchema0to2(diskConfig *map[string]interface{}) error {
|
||||
// Third schema upgrade:
|
||||
// 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)
|
||||
if err != nil {
|
||||
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
230
upgrade_test.go
Normal 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
|
||||
}
|
Loading…
Reference in New Issue
Block a user