Compare commits
232 Commits
v0.93
...
v0.95-hotf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d2df26335 | ||
|
|
ae403fb137 | ||
|
|
e1bb89c393 | ||
|
|
c4e67690f4 | ||
|
|
f6023b395e | ||
|
|
cedab695c2 | ||
|
|
1c339e5fcd | ||
|
|
4eb910c35e | ||
|
|
c6957bed64 | ||
|
|
8e6f7be5b8 | ||
|
|
528c1a72ca | ||
|
|
3087c54a15 | ||
|
|
5c65d0cabe | ||
|
|
4231920ee8 | ||
|
|
d5f46f51b8 | ||
|
|
a860c8e6ff | ||
|
|
0794704f74 | ||
|
|
173ab2a3c1 | ||
|
|
68dc8a1341 | ||
|
|
043e89a1a4 | ||
|
|
c5ed6da5bd | ||
|
|
69c5f175e8 | ||
|
|
79b0fac01a | ||
|
|
bc81a0ecff | ||
|
|
1ac9419c94 | ||
|
|
5f88abb322 | ||
|
|
33db419384 | ||
|
|
ceabad0fd0 | ||
|
|
f76b7c3d94 | ||
|
|
9e68a522cb | ||
|
|
faa7c7b2d4 | ||
|
|
d326d1bc8b | ||
|
|
73fbe8b95a | ||
|
|
87147ac89f | ||
|
|
8d936b5756 | ||
|
|
4ca24b7707 | ||
|
|
133dd75ec3 | ||
|
|
00c128f0a4 | ||
|
|
e4b53db558 | ||
|
|
1611057852 | ||
|
|
00ba63341b | ||
|
|
a1b1877667 | ||
|
|
bebdc1b5bc | ||
|
|
1836e56e6e | ||
|
|
f83d026c33 | ||
|
|
61554ba4e0 | ||
|
|
c82887d3aa | ||
|
|
faeda3f075 | ||
|
|
afeadbb454 | ||
|
|
0e031a4921 | ||
|
|
d0942c88c8 | ||
|
|
794d302ce5 | ||
|
|
7f1f85b08c | ||
|
|
c8e4f61534 | ||
|
|
3f404bc37e | ||
|
|
7911a24dc8 | ||
|
|
7746a3e6a9 | ||
|
|
08bedacf0a | ||
|
|
74a0938038 | ||
|
|
828d3121be | ||
|
|
b6ae539c36 | ||
|
|
fa5ff053b7 | ||
|
|
6bf57ae84e | ||
|
|
472dc0b77d | ||
|
|
4b821d67f5 | ||
|
|
24fc2957c5 | ||
|
|
6ba0e4686a | ||
|
|
b92fb34f37 | ||
|
|
ffd9f1aaa9 | ||
|
|
d43290fe31 | ||
|
|
ef22a31a93 | ||
|
|
0ed619c9c8 | ||
|
|
8b6e9ef5f9 | ||
|
|
cca61a33c6 | ||
|
|
4d217583b0 | ||
|
|
c78cee3396 | ||
|
|
b453d9f41d | ||
|
|
e231230f1b | ||
|
|
850e856e6e | ||
|
|
d6b83d4a63 | ||
|
|
0c973334be | ||
|
|
23ac1726b7 | ||
|
|
1f1e26f67b | ||
|
|
91af0cddce | ||
|
|
6f014fa53d | ||
|
|
6f2e852e09 | ||
|
|
fd82e7c26a | ||
|
|
2bb5b24d4e | ||
|
|
92e70515ae | ||
|
|
51aec7cbbc | ||
|
|
3367b9fb2a | ||
|
|
dfa39293a1 | ||
|
|
65364930f7 | ||
|
|
53ea2d28cf | ||
|
|
395ddb7b3c | ||
|
|
1448b7ab16 | ||
|
|
abd58004b8 | ||
|
|
800cb177f3 | ||
|
|
31a0dde515 | ||
|
|
6edfe1bb8e | ||
|
|
7ebfaccc6e | ||
|
|
bc0b0af06b | ||
|
|
5210d214ec | ||
|
|
ed942f3e31 | ||
|
|
5b417d9f17 | ||
|
|
f7860c893d | ||
|
|
a01ba5dd4d | ||
|
|
b6d0d94990 | ||
|
|
9ea5c1abe1 | ||
|
|
91ec996ffb | ||
|
|
9bc5a4570e | ||
|
|
8defb3b39e | ||
|
|
d5e57248a0 | ||
|
|
0647f3fe86 | ||
|
|
d664a9de1d | ||
|
|
b54f540f71 | ||
|
|
0884116de3 | ||
|
|
eefdf8449a | ||
|
|
ae2c7d00a9 | ||
|
|
afa54a1339 | ||
|
|
56271819ea | ||
|
|
a9b329daf6 | ||
|
|
61b1a30aa1 | ||
|
|
b4732c83c5 | ||
|
|
783ac967a1 | ||
|
|
c091d10a41 | ||
|
|
756c5ac0d3 | ||
|
|
ef789acee4 | ||
|
|
b1c11a718e | ||
|
|
d7b1825cf5 | ||
|
|
b5eb840d22 | ||
|
|
6f56eb4c12 | ||
|
|
6b223e2992 | ||
|
|
43e5d42070 | ||
|
|
a58bf0e24e | ||
|
|
f432eb3609 | ||
|
|
6e16654344 | ||
|
|
ef637e1313 | ||
|
|
9494b87ca5 | ||
|
|
d68600c5d0 | ||
|
|
8fa2f48136 | ||
|
|
542a67b84e | ||
|
|
d832d7ce95 | ||
|
|
92cf7c1aca | ||
|
|
b5f0d48e7f | ||
|
|
6f69fb73af | ||
|
|
67014c40f7 | ||
|
|
a2e9d69452 | ||
|
|
e164cff02b | ||
|
|
08bf9b0acb | ||
|
|
60fa3b2e95 | ||
|
|
3f93cb3397 | ||
|
|
10daf29e2f | ||
|
|
70c56f7a18 | ||
|
|
e9d20651e9 | ||
|
|
466807638c | ||
|
|
991717e150 | ||
|
|
b1f707d18c | ||
|
|
f857ed74ec | ||
|
|
2f497cf5d0 | ||
|
|
21db166be0 | ||
|
|
d06cc0f8ee | ||
|
|
b74eded414 | ||
|
|
ecf0b0c5c1 | ||
|
|
64c2f2d89c | ||
|
|
0b96bd17c4 | ||
|
|
392c16cd27 | ||
|
|
10fcfefcfd | ||
|
|
2bd9923691 | ||
|
|
c8c663f3f0 | ||
|
|
bf82ccede9 | ||
|
|
982f8dc1e1 | ||
|
|
83877fb871 | ||
|
|
ac131923a2 | ||
|
|
bc4c2e2ff7 | ||
|
|
1b15bee2b0 | ||
|
|
8e09424774 | ||
|
|
89b6323f03 | ||
|
|
cdc568d8d9 | ||
|
|
bda6f777c4 | ||
|
|
bf2781d465 | ||
|
|
f2e547a54e | ||
|
|
ceaa1e4ebf | ||
|
|
5d6c980ac7 | ||
|
|
e973c4b174 | ||
|
|
d9d641941c | ||
|
|
d318545913 | ||
|
|
7ec4aa91ea | ||
|
|
681fb4e705 | ||
|
|
c443672e35 | ||
|
|
53d680a5df | ||
|
|
6b7a8078b0 | ||
|
|
91f8ab0549 | ||
|
|
a8812908c1 | ||
|
|
b8595b87c4 | ||
|
|
acb4a98466 | ||
|
|
3929f0da44 | ||
|
|
224c2a891d | ||
|
|
81e88472cb | ||
|
|
6b2baba3c7 | ||
|
|
967a1e6b87 | ||
|
|
241e7ca20c | ||
|
|
bc325de13f | ||
|
|
ffa4429818 | ||
|
|
24edf7eeb6 | ||
|
|
99c8cd06c9 | ||
|
|
9a6266588d | ||
|
|
f21daae023 | ||
|
|
7b64f9ff42 | ||
|
|
1626b6bd5a | ||
|
|
eb22d9cdd9 | ||
|
|
1ed3a9673d | ||
|
|
5ad9f8ead2 | ||
|
|
523c5ef10a | ||
|
|
a9839e95a0 | ||
|
|
1223965cd4 | ||
|
|
141b14c94a | ||
|
|
3a9d436f8a | ||
|
|
01548c236e | ||
|
|
5cb6d97cd7 | ||
|
|
f4a6ca726c | ||
|
|
766fbab071 | ||
|
|
87c8114291 | ||
|
|
d218e047a3 | ||
|
|
bf893d488a | ||
|
|
bd31151c1f | ||
|
|
4476262357 | ||
|
|
2a93e45b67 | ||
|
|
5ba43a59f4 | ||
|
|
dc05556c5a | ||
|
|
5bc6d00aa0 | ||
|
|
2dc2a0946a |
@@ -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:
|
||||
|
||||
327
AGHTechDoc.md
Normal file
327
AGHTechDoc.md
Normal file
@@ -0,0 +1,327 @@
|
||||
# AdGuard Home Technical Document
|
||||
|
||||
The document describes technical details and internal algorithms of AdGuard Home.
|
||||
|
||||
Contents:
|
||||
* First startup
|
||||
* Installation wizard
|
||||
* "Get install settings" command
|
||||
* "Check configuration" command
|
||||
* Disable DNSStubListener
|
||||
* "Apply configuration" command
|
||||
* Enable DHCP server
|
||||
* "Check DHCP" command
|
||||
* "Enable DHCP" command
|
||||
* Static IP check/set
|
||||
|
||||
|
||||
## First startup
|
||||
|
||||
The first application startup is detected when there's no .yaml configuration file.
|
||||
|
||||
We check if the user is root, otherwise we fail with an error.
|
||||
|
||||
Web server is started up on port 3000 and automatically redirects requests to `/` to Installation wizard.
|
||||
|
||||
After Installation wizard steps are completed, we write configuration to a file and start normal operation.
|
||||
|
||||
|
||||
## Installation wizard
|
||||
|
||||
This is the collection of UI screens that are shown to a user on first application startup.
|
||||
|
||||
The screens are:
|
||||
|
||||
1. Welcome
|
||||
2. Set up network interface and listening ports for Web and DNS servers
|
||||
3. Set up administrator username and password
|
||||
4. Configuration complete
|
||||
5. Done
|
||||
|
||||
Algorithm:
|
||||
|
||||
Screen 2:
|
||||
* UI asks server for initial information and shows it
|
||||
* User edits the default settings, clicks on "Next" button
|
||||
* UI asks server to check new settings
|
||||
* Server searches for the known issues
|
||||
* UI shows information about the known issues and the means to fix them
|
||||
* Server applies automatic fixes of the known issues on command from UI
|
||||
|
||||
Screen 3:
|
||||
* UI asks server to apply the configuration
|
||||
* Server restarts DNS server
|
||||
|
||||
|
||||
### "Get install settings" command
|
||||
|
||||
Request:
|
||||
|
||||
GET /control/install/get_addresses
|
||||
|
||||
Response:
|
||||
|
||||
200 OK
|
||||
|
||||
{
|
||||
"web_port":80,
|
||||
"dns_port":53,
|
||||
"interfaces":{
|
||||
"enp2s0":{"name":"enp2s0","mtu":1500,"hardware_address":"","ip_addresses":["",""],"flags":"up|broadcast|multicast"},
|
||||
"lo":{"name":"lo","mtu":65536,"hardware_address":"","ip_addresses":["127.0.0.1","::1"],"flags":"up|loopback"},
|
||||
}
|
||||
}
|
||||
|
||||
If `interfaces.flags` doesn't contain `up` flag, UI must show `(Down)` status next to its IP address in interfaces selector.
|
||||
|
||||
|
||||
### "Check configuration" command
|
||||
|
||||
Request:
|
||||
|
||||
POST /control/install/check_config
|
||||
|
||||
{
|
||||
"web":{"port":80,"ip":"192.168.11.33"},
|
||||
"dns":{"port":53,"ip":"127.0.0.1","autofix":false},
|
||||
}
|
||||
|
||||
Server should check whether a port is available only in case it itself isn't already listening on that port.
|
||||
|
||||
Server replies on success:
|
||||
|
||||
200 OK
|
||||
|
||||
{
|
||||
"web":{"status":""},
|
||||
"dns":{"status":""},
|
||||
}
|
||||
|
||||
Server replies on error:
|
||||
|
||||
200 OK
|
||||
|
||||
{
|
||||
"web":{"status":"ERROR MESSAGE"},
|
||||
"dns":{"status":"ERROR MESSAGE", "can_autofix": true|false},
|
||||
}
|
||||
|
||||
|
||||
### Disable DNSStubListener
|
||||
|
||||
On Linux, if 53 port is not available, server performs several additional checks to determine if the issue can be fixed automatically.
|
||||
|
||||
#### Phase 1
|
||||
|
||||
Request:
|
||||
|
||||
POST /control/install/check_config
|
||||
|
||||
{
|
||||
"dns":{"port":53,"ip":"127.0.0.1","autofix":false}
|
||||
}
|
||||
|
||||
Check if DNSStubListener is enabled:
|
||||
|
||||
systemctl is-enabled systemd-resolved
|
||||
|
||||
Check if DNSStubListener is active:
|
||||
|
||||
grep -E '#?DNSStubListener=yes' /etc/systemd/resolved.conf
|
||||
|
||||
If the issue can be fixed automatically, server replies with `"can_autofix":true`
|
||||
|
||||
200 OK
|
||||
|
||||
{
|
||||
"dns":{"status":"ERROR MESSAGE", "can_autofix":true},
|
||||
}
|
||||
|
||||
In this case UI shows "Fix" button next to error message.
|
||||
|
||||
#### Phase 2
|
||||
|
||||
If user clicks on "Fix" button, UI sends request to perform an automatic fix
|
||||
|
||||
POST /control/install/check_config
|
||||
|
||||
{
|
||||
"dns":{"port":53,"ip":"127.0.0.1","autofix":true},
|
||||
}
|
||||
|
||||
Deactivate (save backup as `resolved.conf.orig`) and stop DNSStubListener:
|
||||
|
||||
sed -r -i.orig 's/#?DNSStubListener=yes/DNSStubListener=no/g' /etc/systemd/resolved.conf
|
||||
systemctl reload-or-restart systemd-resolved
|
||||
|
||||
Server replies:
|
||||
|
||||
200 OK
|
||||
|
||||
{
|
||||
"dns":{"status":""},
|
||||
}
|
||||
|
||||
|
||||
### "Apply configuration" command
|
||||
|
||||
Request:
|
||||
|
||||
POST /control/install/configure
|
||||
|
||||
{
|
||||
"web":{"port":80,"ip":"192.168.11.33"},
|
||||
"dns":{"port":53,"ip":"127.0.0.1"},
|
||||
"username":"u",
|
||||
"password":"p",
|
||||
}
|
||||
|
||||
Server checks the parameters once again, restarts DNS server, replies:
|
||||
|
||||
200 OK
|
||||
|
||||
On error, server responds with code 400 or 500. In this case UI should show error message and reset to the beginning.
|
||||
|
||||
400 Bad Request
|
||||
|
||||
ERROR MESSAGE
|
||||
|
||||
|
||||
## Enable DHCP server
|
||||
|
||||
Algorithm:
|
||||
|
||||
* UI shows DHCP configuration screen with "Enabled DHCP" button disabled, and "Check DHCP" button enabled
|
||||
* User clicks on "Check DHCP"; UI sends request to server
|
||||
* Server may fail to detect whether there is another DHCP server working in the network. In this case UI shows a warning.
|
||||
* Server may detect that a dynamic IP configuration is used for this interface. In this case UI shows a warning.
|
||||
* UI enables "Enable DHCP" button
|
||||
* User clicks on "Enable DHCP"; UI sends request to server
|
||||
* Server sets a static IP (if necessary), enables DHCP server, sends the status back to UI
|
||||
* UI shows the status
|
||||
|
||||
|
||||
### "Check DHCP" command
|
||||
|
||||
Request:
|
||||
|
||||
POST /control/dhcp/find_active_dhcp
|
||||
|
||||
vboxnet0
|
||||
|
||||
Response:
|
||||
|
||||
200 OK
|
||||
|
||||
{
|
||||
"other_server": {
|
||||
"found": "yes|no|error",
|
||||
"error": "Error message", // set if found=error
|
||||
},
|
||||
"static_ip": {
|
||||
"static": "yes|no|error",
|
||||
"ip": "<Current dynamic IP address>", // set if static=no
|
||||
}
|
||||
}
|
||||
|
||||
If `other_server.found` is:
|
||||
* `no`: everything is fine - there is no other DHCP server
|
||||
* `yes`: we found another DHCP server. UI shows a warning.
|
||||
* `error`: we failed to determine whether there's another DHCP server. `other_server.error` contains error details. UI shows a warning.
|
||||
|
||||
If `static_ip.static` is:
|
||||
* `yes`: everything is fine - server uses static IP address.
|
||||
|
||||
* `no`: `static_ip.ip` contains the current dynamic IP address which we may set as static. In this case UI shows a warning:
|
||||
|
||||
Your system uses dynamic IP address configuration for interface <CURRENT INTERFACE NAME>. In order to use DHCP server a static IP address must be set. Your current IP address is <static_ip.ip>. We will automatically set this IP address as static if you press Enable DHCP button.
|
||||
|
||||
* `error`: this means that the server failed to check for a static IP. In this case UI shows a warning:
|
||||
|
||||
In order to use DHCP server a static IP address must be set. We failed to determine if this network interface is configured using static IP address. Please set a static IP address manually.
|
||||
|
||||
|
||||
### "Enable DHCP" command
|
||||
|
||||
Request:
|
||||
|
||||
POST /control/dhcp/set_config
|
||||
|
||||
{
|
||||
"enabled":true,
|
||||
"interface_name":"vboxnet0",
|
||||
"gateway_ip":"192.169.56.1",
|
||||
"subnet_mask":"255.255.255.0",
|
||||
"range_start":"192.169.56.3",
|
||||
"range_end":"192.169.56.3",
|
||||
"lease_duration":60,
|
||||
"icmp_timeout_msec":0
|
||||
}
|
||||
|
||||
Response:
|
||||
|
||||
200 OK
|
||||
|
||||
OK
|
||||
|
||||
|
||||
### Static IP check/set
|
||||
|
||||
Before enabling DHCP server we have to make sure the network interface we use has a static IP configured.
|
||||
|
||||
#### Phase 1
|
||||
|
||||
On Debian systems DHCP is configured by `/etc/dhcpcd.conf`.
|
||||
|
||||
To detect if a static IP is used currently we search for line
|
||||
|
||||
interface eth0
|
||||
|
||||
and then look for line
|
||||
|
||||
static ip_address=...
|
||||
|
||||
If the interface already has a static IP, everything is set up, we don't have to change anything.
|
||||
|
||||
To get the current IP address along with netmask we execute
|
||||
|
||||
ip -oneline -family inet address show eth0
|
||||
|
||||
which will print:
|
||||
|
||||
2: eth0 inet 192.168.0.1/24 brd 192.168.0.255 scope global eth0\ valid_lft forever preferred_lft forever
|
||||
|
||||
To get the current gateway address:
|
||||
|
||||
ip route show dev enp2s0
|
||||
|
||||
which will print:
|
||||
|
||||
default via 192.168.0.1 proto dhcp metric 100
|
||||
|
||||
|
||||
#### Phase 2
|
||||
|
||||
This method only works on Raspbian.
|
||||
|
||||
On Ubuntu DHCP for a network interface can't be disabled via `dhcpcd.conf`. This must be configured in `/etc/netplan/01-netcfg.yaml`.
|
||||
|
||||
Fedora doesn't use `dhcpcd.conf` configuration at all.
|
||||
|
||||
Step 1.
|
||||
|
||||
To set a static IP address we add these lines to `dhcpcd.conf`:
|
||||
|
||||
interface eth0
|
||||
static ip_address=192.168.0.1/24
|
||||
static routers=192.168.0.1
|
||||
static domain_name_servers=192.168.0.1
|
||||
|
||||
* Don't set 'routers' if we couldn't find gateway IP
|
||||
* Set 'domain_name_servers' equal to our IP
|
||||
|
||||
Step 2.
|
||||
|
||||
If we would set a different IP address, we'd need to replace the IP address for the current network configuration. But currently this step isn't necessary.
|
||||
|
||||
ip addr replace dev eth0 192.168.0.1/24
|
||||
@@ -21,4 +21,4 @@ EXPOSE 53/tcp 53/udp 67/tcp 67/udp 68/tcp 68/udp 80/tcp 443/tcp 853/tcp 853/udp
|
||||
VOLUME ["/opt/adguardhome/conf", "/opt/adguardhome/work"]
|
||||
|
||||
ENTRYPOINT ["/opt/adguardhome/AdGuardHome"]
|
||||
CMD ["-h", "0.0.0.0", "-c", "/opt/adguardhome/conf/AdGuardHome.yaml", "-w", "/opt/adguardhome/work"]
|
||||
CMD ["-c", "/opt/adguardhome/conf/AdGuardHome.yaml", "-w", "/opt/adguardhome/work"]
|
||||
@@ -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:
|
||||
|
||||
262
app.go
262
app.go
@@ -1,23 +1,26 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
stdlog "log"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/NYTimes/gziphandler"
|
||||
"github.com/gobuffalo/packr"
|
||||
|
||||
"github.com/hmage/golibs/log"
|
||||
)
|
||||
|
||||
// VersionString will be set through ldflags, contains current version
|
||||
@@ -28,6 +31,7 @@ var httpsServer struct {
|
||||
cond *sync.Cond // reacts to config.TLS.Enabled, PortHTTPS, CertificateChain and PrivateKey
|
||||
sync.Mutex // protects config.TLS
|
||||
}
|
||||
var pidFileName string // PID file name. Empty if no PID file was created.
|
||||
|
||||
const (
|
||||
// Used in config to indicate that syslog or eventlog (win) should be used for logger output
|
||||
@@ -45,14 +49,6 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
signalChannel := make(chan os.Signal)
|
||||
signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
|
||||
go func() {
|
||||
<-signalChannel
|
||||
cleanup()
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
// run the protection
|
||||
run(args)
|
||||
}
|
||||
@@ -71,14 +67,29 @@ 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()
|
||||
if config.firstRun {
|
||||
requireAdminRights()
|
||||
}
|
||||
|
||||
signalChannel := make(chan os.Signal)
|
||||
signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
|
||||
go func() {
|
||||
<-signalChannel
|
||||
cleanup()
|
||||
cleanupAlways()
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
// Do the upgrade if necessary
|
||||
err := upgradeConfig()
|
||||
@@ -92,6 +103,11 @@ func run(args options) {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") &&
|
||||
config.RlimitNoFile != 0 {
|
||||
setRlimit(config.RlimitNoFile)
|
||||
}
|
||||
|
||||
// override bind host/port from the console
|
||||
if args.bindHost != "" {
|
||||
config.BindHost = args.bindHost
|
||||
@@ -100,24 +116,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()
|
||||
@@ -141,27 +140,27 @@ func run(args options) {
|
||||
}
|
||||
}
|
||||
|
||||
if len(args.pidFile) != 0 && writePIDFile(args.pidFile) {
|
||||
pidFileName = args.pidFile
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
// Initialize and run the admin Web interface
|
||||
box := packr.NewBox("build/static")
|
||||
|
||||
// if not configured, redirect / to /install.html, otherwise redirect /install.html to /
|
||||
http.Handle("/", postInstallHandler(optionalAuthHandler(http.FileServer(box))))
|
||||
http.Handle("/", postInstallHandler(optionalAuthHandler(gziphandler.GzipHandler(http.FileServer(box)))))
|
||||
registerControlHandlers()
|
||||
|
||||
// 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()
|
||||
}
|
||||
@@ -169,53 +168,7 @@ func run(args options) {
|
||||
httpsServer.cond = sync.NewCond(&httpsServer.Mutex)
|
||||
|
||||
// for https, we have a separate goroutine loop
|
||||
go func() {
|
||||
for { // this is an endless loop
|
||||
httpsServer.cond.L.Lock()
|
||||
// this mechanism doesn't let us through until all conditions are ment
|
||||
for config.TLS.Enabled == false || config.TLS.PortHTTPS == 0 || config.TLS.PrivateKey == "" || config.TLS.CertificateChain == "" { // sleep until necessary data is supplied
|
||||
httpsServer.cond.Wait()
|
||||
}
|
||||
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 {
|
||||
log.Fatal(data.WarningValidation)
|
||||
os.Exit(1)
|
||||
}
|
||||
config.Lock()
|
||||
config.TLS = data // update warnings
|
||||
config.Unlock()
|
||||
|
||||
// prepare certs for HTTPS server
|
||||
// important -- they have to be copies, otherwise changing the contents in config.TLS will break encryption for in-flight requests
|
||||
certchain := make([]byte, len(config.TLS.CertificateChain))
|
||||
copy(certchain, []byte(config.TLS.CertificateChain))
|
||||
privatekey := make([]byte, len(config.TLS.PrivateKey))
|
||||
copy(privatekey, []byte(config.TLS.PrivateKey))
|
||||
cert, err := tls.X509KeyPair(certchain, privatekey)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
httpsServer.cond.L.Unlock()
|
||||
|
||||
// prepare HTTPS server
|
||||
httpsServer.server = &http.Server{
|
||||
Addr: address,
|
||||
TLSConfig: &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
},
|
||||
}
|
||||
|
||||
printHTTPAddresses("https")
|
||||
err = httpsServer.server.ListenAndServeTLS("", "")
|
||||
if err != http.ErrServerClosed {
|
||||
log.Fatal(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}()
|
||||
go httpServerLoop()
|
||||
|
||||
// this loop is used as an ability to change listening host and/or port
|
||||
for {
|
||||
@@ -228,13 +181,107 @@ func run(args options) {
|
||||
}
|
||||
err := httpServer.ListenAndServe()
|
||||
if err != http.ErrServerClosed {
|
||||
cleanupAlways()
|
||||
log.Fatal(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
// We use ErrServerClosed as a sign that we need to rebind on new address, so go back to the start of the loop
|
||||
}
|
||||
}
|
||||
|
||||
func httpServerLoop() {
|
||||
for {
|
||||
httpsServer.cond.L.Lock()
|
||||
// this mechanism doesn't let us through until all conditions are met
|
||||
for config.TLS.Enabled == false ||
|
||||
config.TLS.PortHTTPS == 0 ||
|
||||
config.TLS.PrivateKey == "" ||
|
||||
config.TLS.CertificateChain == "" { // sleep until necessary data is supplied
|
||||
httpsServer.cond.Wait()
|
||||
}
|
||||
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.CertificateChain, config.TLS.PrivateKey, config.TLS.ServerName)
|
||||
if !data.ValidPair {
|
||||
cleanupAlways()
|
||||
log.Fatal(data.WarningValidation)
|
||||
}
|
||||
config.Lock()
|
||||
config.TLS.tlsConfigStatus = data // update warnings
|
||||
config.Unlock()
|
||||
|
||||
// prepare certs for HTTPS server
|
||||
// important -- they have to be copies, otherwise changing the contents in config.TLS will break encryption for in-flight requests
|
||||
certchain := make([]byte, len(config.TLS.CertificateChain))
|
||||
copy(certchain, []byte(config.TLS.CertificateChain))
|
||||
privatekey := make([]byte, len(config.TLS.PrivateKey))
|
||||
copy(privatekey, []byte(config.TLS.PrivateKey))
|
||||
cert, err := tls.X509KeyPair(certchain, privatekey)
|
||||
if err != nil {
|
||||
cleanupAlways()
|
||||
log.Fatal(err)
|
||||
}
|
||||
httpsServer.cond.L.Unlock()
|
||||
|
||||
// prepare HTTPS server
|
||||
httpsServer.server = &http.Server{
|
||||
Addr: address,
|
||||
TLSConfig: &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
MinVersion: tls.VersionTLS12,
|
||||
},
|
||||
}
|
||||
|
||||
printHTTPAddresses("https")
|
||||
err = httpsServer.server.ListenAndServeTLS("", "")
|
||||
if err != http.ErrServerClosed {
|
||||
cleanupAlways()
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the current user has root (administrator) rights
|
||||
// and if not, ask and try to run as root
|
||||
func requireAdminRights() {
|
||||
admin, _ := haveAdminRights()
|
||||
if admin {
|
||||
return
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
log.Fatal("This is the first launch of AdGuard Home. You must run it as Administrator.")
|
||||
|
||||
} else {
|
||||
log.Error("This is the first launch of AdGuard Home. You must run it as root.")
|
||||
|
||||
_, _ = io.WriteString(os.Stdout, "Do you want to start AdGuard Home as root user? [y/n] ")
|
||||
stdin := bufio.NewReader(os.Stdin)
|
||||
buf, _ := stdin.ReadString('\n')
|
||||
buf = strings.TrimSpace(buf)
|
||||
if buf != "y" {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
cmd := exec.Command("sudo", os.Args...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
_ = cmd.Run()
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Write PID to a file
|
||||
func writePIDFile(fn string) bool {
|
||||
data := fmt.Sprintf("%d", os.Getpid())
|
||||
err := ioutil.WriteFile(fn, []byte(data), 0644)
|
||||
if err != nil {
|
||||
log.Error("Couldn't write PID to file %s: %v", fn, err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// initWorkingDir initializes the ourWorkingDir
|
||||
// if no command-line arguments specified, we use the directory where our binary file is located
|
||||
func initWorkingDir(args options) {
|
||||
@@ -263,7 +310,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 +334,43 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
// This function is called before application exits
|
||||
func cleanupAlways() {
|
||||
if len(pidFileName) != 0 {
|
||||
os.Remove(pidFileName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,6 +382,7 @@ type options struct {
|
||||
bindHost string // host address to bind HTTP server on
|
||||
bindPort int // port to serve HTTP pages on
|
||||
logFile string // Path to the log file. If empty, write to stdout. If "syslog", writes to syslog
|
||||
pidFile string // File name to save PID to
|
||||
|
||||
// service control action (see service.ControlAction array + "status" command)
|
||||
serviceControlAction string
|
||||
@@ -348,6 +419,7 @@ func loadOptions() options {
|
||||
{"logfile", "l", "path to the log file. If empty, writes to stdout, if 'syslog' -- system log", func(value string) {
|
||||
o.logFile = value
|
||||
}, nil},
|
||||
{"pidfile", "", "File name to save PID to", func(value string) { o.pidFile = value }, nil},
|
||||
{"verbose", "v", "enable verbose output", nil, func() { o.verbose = true }},
|
||||
{"help", "", "print this help", nil, func() {
|
||||
printHelp()
|
||||
@@ -373,7 +445,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 +458,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)
|
||||
}
|
||||
|
||||
3051
client/package-lock.json
generated
vendored
3051
client/package-lock.json
generated
vendored
File diff suppressed because it is too large
Load Diff
3
client/package.json
vendored
3
client/package.json
vendored
@@ -47,7 +47,8 @@
|
||||
"babel-runtime": "6.26.0",
|
||||
"clean-webpack-plugin": "^0.1.19",
|
||||
"compression-webpack-plugin": "^1.1.11",
|
||||
"css-loader": "^0.28.11",
|
||||
"copy-webpack-plugin": "^4.6.0",
|
||||
"css-loader": "^2.1.1",
|
||||
"eslint": "^4.19.1",
|
||||
"eslint-config-airbnb-base": "^12.1.0",
|
||||
"eslint-config-react-app": "^2.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,8 @@
|
||||
<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">
|
||||
<meta name="google" content="notranslate">
|
||||
<link rel="shortcut icon" href="favicon.ico">
|
||||
<title>AdGuard Home</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
<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">
|
||||
<meta name="google" content="notranslate">
|
||||
<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,9 @@
|
||||
{
|
||||
"url_added_successfully": "Url added successfully",
|
||||
"example_upstream_reserved": "you can specify DNS upstream <0>for a specific domain(s)<\/0>",
|
||||
"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",
|
||||
@@ -8,8 +12,8 @@
|
||||
"dhcp_description": "If your router does not provide DHCP settings, you can use AdGuard's own built-in DHCP server.",
|
||||
"dhcp_enable": "Enable DHCP server",
|
||||
"dhcp_disable": "Disable DHCP server",
|
||||
"dhcp_not_found": "No active DHCP servers found on the network. It is safe to enable the built-in DHCP server.",
|
||||
"dhcp_found": "Some active DHCP servers found on the network. It is not safe to enable the built-in DHCP server.",
|
||||
"dhcp_not_found": "It is safe to enable the built-in DHCP server - we didn't find any active DHCP servers on the network. However, we encourage you to re-check it manually as our automatic test currently doesn't give 100% guarantee.",
|
||||
"dhcp_found": "An active DHCP server is found on the network. It is not safe to enable the built-in DHCP server.",
|
||||
"dhcp_leases": "DHCP leases",
|
||||
"dhcp_leases_not_found": "No DHCP leases found",
|
||||
"dhcp_config_saved": "Saved DHCP server config",
|
||||
@@ -28,7 +32,11 @@
|
||||
"dhcp_ip_addresses": "IP addresses",
|
||||
"dhcp_table_hostname": "Hostname",
|
||||
"dhcp_table_expires": "Expires",
|
||||
"dhcp_warning": "If you want to enable the built-in DHCP server, make sure that there is no other active DHCP server. Otherwise, it can break the internet for connected devices!",
|
||||
"dhcp_warning": "If you want to enable DHCP server anyway, make sure that there is no other active DHCP server in your network. Otherwise, it can break the Internet for connected devices!",
|
||||
"dhcp_error": "We could not determine whether there is another DHCP server in the network.",
|
||||
"dhcp_static_ip_error": "In order to use DHCP server a static IP address must be set. We failed to determine if this network interface is configured using static IP address. Please set a static IP address manually.",
|
||||
"dhcp_dynamic_ip_found": "Your system uses dynamic IP address configuration for interface <0>{{interfaceName}}</0>. In order to use DHCP server a static IP address must be set. Your current IP address is <0>{{ipAddress}}</0>. We will automatically set this IP address as static if you press Enable DHCP button.",
|
||||
"error_details": "Error details",
|
||||
"back": "Back",
|
||||
"dashboard": "Dashboard",
|
||||
"settings": "Settings",
|
||||
@@ -42,6 +50,7 @@
|
||||
"copyright": "Copyright",
|
||||
"homepage": "Homepage",
|
||||
"report_an_issue": "Report an issue",
|
||||
"privacy_policy": "Privacy policy",
|
||||
"enable_protection": "Enable protection",
|
||||
"enabled_protection": "Enabled protection",
|
||||
"disable_protection": "Disable protection",
|
||||
@@ -74,12 +83,12 @@
|
||||
"use_adguard_parental": "Use AdGuard parental control web service",
|
||||
"use_adguard_parental_hint": "AdGuard Home will check if domain contains adult materials. It uses the same privacy-friendly API as the browsing security web service.",
|
||||
"enforce_safe_search": "Enforce safe search",
|
||||
"enforce_save_search_hint": "AdGuard Home can enforce safe search in the following search engines: Google, Youtube, Bing, and Yandex.",
|
||||
"enforce_save_search_hint": "AdGuard Home can enforce safe search in the following search engines: Google, Youtube, Bing, DuckDuckGo and Yandex.",
|
||||
"no_servers_specified": "No servers specified",
|
||||
"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,9 +127,9 @@
|
||||
"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)",
|
||||
"all_filters_up_to_date_toast": "All filters are already up-to-date",
|
||||
"updated_upstream_dns_toast": "Updated the upstream DNS servers",
|
||||
@@ -211,11 +220,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 +232,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 +248,16 @@
|
||||
"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",
|
||||
"down": "Down",
|
||||
"fix": "Fix"
|
||||
}
|
||||
@@ -1,132 +1,139 @@
|
||||
{
|
||||
"example_upstream_reserved": "puede especificar el DNS de subida <0>para un dominio espec\u00edfico<\/0>",
|
||||
"upstream_parallel": "Usar consultas paralelas para acelerar la resoluci\u00f3n al consultar simult\u00e1neamente a todos los servidores de subida",
|
||||
"bootstrap_dns": "Servidores DNS de arranque",
|
||||
"bootstrap_dns_desc": "Los servidores DNS de arranque se utilizan para resolver las direcciones IP de los resolutores DoH\/DoT que especifique como DNS de subida.",
|
||||
"url_added_successfully": "URL a\u00f1adida correctamente",
|
||||
"check_dhcp_servers": "Compruebe si hay servidores DHCP",
|
||||
"save_config": "Guardar config",
|
||||
"save_config": "Guardar configuraci\u00f3n",
|
||||
"enabled_dhcp": "Servidor DHCP habilitado",
|
||||
"disabled_dhcp": "Servidor DHCP deshabilitado",
|
||||
"dhcp_title": "Servidor DHCP (experimental)",
|
||||
"dhcp_description": "Si su enrutador no proporciona la configuraci\u00f3n DHCP, puede utilizar el propio servidor DHCP incorporado de AdGuard.",
|
||||
"dhcp_description": "Si su router no proporciona la configuraci\u00f3n DHCP, puede utilizar el propio servidor DHCP incorporado de AdGuard.",
|
||||
"dhcp_enable": "Habilitar servidor DHCP",
|
||||
"dhcp_disable": "Deshabilitar el servidor DHCP",
|
||||
"dhcp_not_found": "No se han encontrado servidores DHCP activos en la red. Es seguro habilitar el servidor DHCP incorporado.",
|
||||
"dhcp_found": "Se encontraron servidores DHCP activos encontrados en la red. No es seguro habilitar el servidor DHCP incorporado.",
|
||||
"dhcp_leases": "concesi\u00f3nes DHCP",
|
||||
"dhcp_found": "Algunos servidores DHCP activos se encuentran en la red. No es seguro habilitar el servidor DHCP incorporado.",
|
||||
"dhcp_leases": "Concesi\u00f3nes DHCP",
|
||||
"dhcp_leases_not_found": "No se encontraron concesi\u00f3nes DHCP",
|
||||
"dhcp_config_saved": "Configuraci\u00f3n del servidor DHCP guardada",
|
||||
"form_error_required": "Campo obligatorio",
|
||||
"form_error_ip_format": "Formato IPv4 no v\u00e1lido",
|
||||
"form_error_positive": "Debe ser mayor que 0",
|
||||
"dhcp_form_gateway_input": "IP de acceso",
|
||||
"dhcp_form_gateway_input": "IP de puerta de enlace",
|
||||
"dhcp_form_subnet_input": "M\u00e1scara de subred",
|
||||
"dhcp_form_range_title": "Rango de direcciones IP",
|
||||
"dhcp_form_range_start": "Inicio de rango",
|
||||
"dhcp_form_range_end": "Final de rango",
|
||||
"dhcp_form_lease_title": "Tiempo de concesi\u00f3n DHCP (en segundos)",
|
||||
"dhcp_form_lease_input": "duraci\u00f3n de la concesi\u00f3n",
|
||||
"dhcp_form_lease_input": "Duraci\u00f3n de la concesi\u00f3n",
|
||||
"dhcp_interface_select": "Seleccione la interfaz DHCP",
|
||||
"dhcp_hardware_address": "Direcci\u00f3n de hardware",
|
||||
"dhcp_hardware_address": "Direcci\u00f3n MAC",
|
||||
"dhcp_ip_addresses": "Direcciones IP",
|
||||
"dhcp_table_hostname": "Hostname",
|
||||
"dhcp_table_hostname": "Nombre del host",
|
||||
"dhcp_table_expires": "Expira",
|
||||
"dhcp_warning": "Si desea habilitar el servidor DHCP incorporado, aseg\u00farese de que no hay otro servidor DHCP activo. \u00a1De lo contrario, puede dejar sin Internet a los dispositivos conectados!",
|
||||
"back": "Atr\u00e1s",
|
||||
"dashboard": "Tablero de rendimiento",
|
||||
"settings": "Ajustes",
|
||||
"dashboard": "Panel de control",
|
||||
"settings": "Configuraci\u00f3n",
|
||||
"filters": "Filtros",
|
||||
"query_log": "Log de consulta",
|
||||
"faq": "FAQ",
|
||||
"query_log": "Registro de consultas",
|
||||
"faq": "Preguntas frecuentes",
|
||||
"version": "versi\u00f3n",
|
||||
"address": "direcci\u00f3n",
|
||||
"on": "Activado",
|
||||
"off": "Desactivado",
|
||||
"copyright": "Derechos de autor",
|
||||
"copyright": "Copyright",
|
||||
"homepage": "P\u00e1gina de inicio",
|
||||
"report_an_issue": "Reportar un error",
|
||||
"enable_protection": "Activar la protecci\u00f3n",
|
||||
"enabled_protection": "Protecci\u00f3n activada",
|
||||
"disable_protection": "Desactivar protecci\u00f3n",
|
||||
"disabled_protection": "Protecci\u00f3n desactivada",
|
||||
"enable_protection": "Habilitar protecci\u00f3n",
|
||||
"enabled_protection": "Protecci\u00f3n habilitada",
|
||||
"disable_protection": "Deshabilitar protecci\u00f3n",
|
||||
"disabled_protection": "Protecci\u00f3n deshabilitada",
|
||||
"refresh_statics": "Restablecer estad\u00edsticas",
|
||||
"dns_query": "Consultas DNS",
|
||||
"blocked_by": "Bloqueado por filtros",
|
||||
"stats_malware_phishing": "Malware\/phishing bloqueado",
|
||||
"stats_adult": "Contenido para adultos bloqueado",
|
||||
"stats_query_domain": "Dominios m\u00e1s solicitados",
|
||||
"stats_adult": "Sitios para adultos bloqueado",
|
||||
"stats_query_domain": "Dominios m\u00e1s consultados",
|
||||
"for_last_24_hours": "en las \u00faltimas 24 horas",
|
||||
"no_domains_found": "Dominios no encontrados",
|
||||
"requests_count": "N\u00famero de solicitudes",
|
||||
"top_blocked_domains": "Dominios m\u00e1s bloqueados",
|
||||
"top_clients": "Clientes m\u00e1s populares",
|
||||
"top_clients": "Clientes m\u00e1s frecuentes",
|
||||
"no_clients_found": "No hay clientes",
|
||||
"general_statistics": "Estad\u00edsticas generales",
|
||||
"number_of_dns_query_24_hours": "Una serie de consultas DNS procesadas durante las \u00faltimas 24 horas",
|
||||
"number_of_dns_query_blocked_24_hours": "El n\u00famero de solicitudes de DNS bloqueadas por los filtros de publicidad y listas de bloqueo de hosts",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "Un n\u00famero de solicitudes de DNS bloqueadas por el m\u00f3dulo de navegaci\u00f3n segura de AdGuard",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "Un n\u00famero de sitios para adultos bloqueados",
|
||||
"enforced_save_search": "B\u00fasqueda segura forzada",
|
||||
"number_of_dns_query_to_safe_search": "Una serie de solicitudes de DNS a los motores de b\u00fasqueda para los que se aplic\u00f3 la B\u00fasqueda Segura",
|
||||
"number_of_dns_query_24_hours": "N\u00famero de consultas DNS procesadas durante las \u00faltimas 24 horas",
|
||||
"number_of_dns_query_blocked_24_hours": "N\u00famero de peticiones DNS bloqueadas por los filtros de publicidad y listas de bloqueo de hosts",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "N\u00famero de peticiones DNS bloqueadas por el m\u00f3dulo de navegaci\u00f3n segura de AdGuard",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "N\u00famero de sitios para adultos bloqueado",
|
||||
"enforced_save_search": "B\u00fasquedas seguras forzadas",
|
||||
"number_of_dns_query_to_safe_search": "N\u00famero de peticiones DNS a los motores de b\u00fasqueda para los que se aplic\u00f3 la b\u00fasqueda segura forzada",
|
||||
"average_processing_time": "Tiempo promedio de procesamiento",
|
||||
"average_processing_time_hint": "Tiempo promedio en milisegundos al procesar una solicitud DNS",
|
||||
"average_processing_time_hint": "Tiempo promedio en milisegundos al procesar una petici\u00f3n DNS",
|
||||
"block_domain_use_filters_and_hosts": "Bloquear dominios usando filtros y archivos hosts",
|
||||
"filters_block_toggle_hint": "Puede configurar las reglas de bloqueo en los ajustes <a href='#filters'>Filtros<\/a>.",
|
||||
"use_adguard_browsing_sec": "Usar el servicio web de Seguridad de navegaci\u00f3n de AdGuard",
|
||||
"use_adguard_browsing_sec_hint": "AdGuard Home comprobar\u00e1 si el dominio est\u00e1 en la lista negra del servicio web de seguridad de navegaci\u00f3n. Utilizar\u00e1 una API de b\u00fasqueda amigable con la privacidad para realizar la comprobaci\u00f3n: s\u00f3lo se env\u00eda al servidor un prefijo corto del hash del nombre de dominio SHA256.",
|
||||
"use_adguard_parental": "Usar Control Parental de AdGuard ",
|
||||
"filters_block_toggle_hint": "Puede configurar las reglas de bloqueo en la configuraci\u00f3n de <a href='#filters'>filtros<\/a>.",
|
||||
"use_adguard_browsing_sec": "Usar el servicio web de seguridad de navegaci\u00f3n de AdGuard",
|
||||
"use_adguard_browsing_sec_hint": "AdGuard Home comprobar\u00e1 si el dominio est\u00e1 en la lista negra del servicio web de seguridad de navegaci\u00f3n. Utilizar\u00e1 una API de b\u00fasqueda amigable con la privacidad para realizar la comprobaci\u00f3n: solo se env\u00eda al servidor un prefijo corto de hash del nombre de dominio SHA256.",
|
||||
"use_adguard_parental": "Usar el control parental de AdGuard",
|
||||
"use_adguard_parental_hint": "AdGuard Home comprobar\u00e1 si el dominio contiene materiales para adultos. Utiliza la misma API amigable con la privacidad que el servicio web de seguridad de navegaci\u00f3n.",
|
||||
"enforce_safe_search": "Forzar b\u00fasqueda segura",
|
||||
"enforce_save_search_hint": "AdGuard Home puede forzar la b\u00fasqueda segura en los siguientes motores de b\u00fasqueda: Google, Youtube, Bing y Yandex.",
|
||||
"enforce_save_search_hint": "AdGuard Home puede forzar la b\u00fasqueda segura en los siguientes motores de b\u00fasqueda: Google, YouTube, Bing y Yandex.",
|
||||
"no_servers_specified": "No hay servidores especificados",
|
||||
"no_settings": "No hay ajustes",
|
||||
"general_settings": "Ajustes generales",
|
||||
"upstream_dns": "Servidores DNS upstream",
|
||||
"upstream_dns_hint": "Si mantiene este campo vac\u00edo, AdGuard Home utilizar\u00e1 <a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a> como upstream. Utilice el prefijo tls:\/\/ para DNS sobre servidores TLS.",
|
||||
"test_upstream_btn": "Probar upstream",
|
||||
"no_settings": "Sin configuraci\u00f3n",
|
||||
"general_settings": "Configuraci\u00f3n general",
|
||||
"upstream_dns": "Servidores DNS de subida",
|
||||
"upstream_dns_hint": "Si mantiene este campo vac\u00edo, AdGuard Home utilizar\u00e1 <a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a> como DNS de subida. Utilice el prefijo tls:\/\/ para los servidores DNS mediante TLS.",
|
||||
"test_upstream_btn": "Probar DNS de subida",
|
||||
"apply_btn": "Aplicar",
|
||||
"disabled_filtering_toast": "Desactivar filtrado",
|
||||
"enabled_filtering_toast": "Filtrado activado",
|
||||
"disabled_safe_browsing_toast": "Navegaci\u00f3n segura desactivada",
|
||||
"enabled_safe_browsing_toast": "Navegaci\u00f3n segura activada",
|
||||
"disabled_parental_toast": "Control parental desactivado",
|
||||
"enabled_parental_toast": "Control parental activado",
|
||||
"disabled_safe_search_toast": "B\u00fasqueda segura desactivada",
|
||||
"enabled_save_search_toast": "B\u00fasqueda segura activada",
|
||||
"enabled_table_header": "Activado",
|
||||
"disabled_filtering_toast": "Filtrado deshabilitado",
|
||||
"enabled_filtering_toast": "Filtrado habilitado",
|
||||
"disabled_safe_browsing_toast": "B\u00fasqueda segura deshabilitada",
|
||||
"enabled_safe_browsing_toast": "B\u00fasqueda segura habilitada",
|
||||
"disabled_parental_toast": "Control parental deshabilitado",
|
||||
"enabled_parental_toast": "Control parental habilitado",
|
||||
"disabled_safe_search_toast": "B\u00fasqueda segura deshabilitada",
|
||||
"enabled_save_search_toast": "B\u00fasqueda segura habilitada",
|
||||
"enabled_table_header": "Habilitado",
|
||||
"name_table_header": "Nombre",
|
||||
"filter_url_table_header": "Filtro URL",
|
||||
"filter_url_table_header": "URL del filtro",
|
||||
"rules_count_table_header": "N\u00famero de reglas",
|
||||
"last_time_updated_table_header": "\u00daltima actualizaci\u00f3n",
|
||||
"actions_table_header": "Acciones",
|
||||
"delete_table_action": "Eliminar",
|
||||
"filters_and_hosts": "Filtros y listas de bloqueo de hosts",
|
||||
"filters_and_hosts_hint": "AdGuard Home entiende reglas b\u00e1sicas de bloqueo y la sintaxis de los archivos de hosts.",
|
||||
"no_filters_added": "No hay filtros agregados",
|
||||
"add_filter_btn": "Agregar filtro",
|
||||
"no_filters_added": "No hay filtros a\u00f1adidos",
|
||||
"add_filter_btn": "A\u00f1adir filtro",
|
||||
"cancel_btn": "Cancelar",
|
||||
"enter_name_hint": "Ingresar nombre",
|
||||
"enter_url_hint": "Ingresar URL",
|
||||
"check_updates_btn": "Revisar si hay actualizaciones",
|
||||
"enter_name_hint": "Ingrese el nombre",
|
||||
"enter_url_hint": "Ingrese la URL",
|
||||
"check_updates_btn": "Buscar actualizaciones",
|
||||
"new_filter_btn": "Nueva suscripci\u00f3n de filtro",
|
||||
"enter_valid_filter_url": "Ingrese una URL v\u00e1lida para suscribirse o un archivo de hosts.",
|
||||
"custom_filter_rules": "Personalizar reglas del filtrado",
|
||||
"custom_filter_rules_hint": "Introduzca una regla en una l\u00ednea. Puede utilizar reglas de bloqueo de anuncios o sintaxis de archivos de hosts.",
|
||||
"custom_filter_rules_hint": "Ingrese una regla en una l\u00ednea. Puede utilizar reglas de bloqueo de anuncios o la sintaxis de archivos de hosts.",
|
||||
"examples_title": "Ejemplos",
|
||||
"example_meaning_filter_block": "bloquear acceso al dominio ejemplo.org\ny a todos sus subdominios",
|
||||
"example_meaning_filter_whitelist": "desbloquear el acceso al dominio ejemplo.org y a sus subdominios",
|
||||
"example_meaning_filter_block": "bloquea el acceso al dominio ejemplo.org\ny a todos sus subdominios",
|
||||
"example_meaning_filter_whitelist": "desbloquea el acceso al dominio ejemplo.org y a todos sus subdominios",
|
||||
"example_meaning_host_block": "AdGuard Home regresar\u00e1 la direcci\u00f3n 127.0.0.1 para el dominio ejemplo.org (pero no para sus subdominios).",
|
||||
"example_comment": "! Aqu\u00ed va un comentario",
|
||||
"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_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",
|
||||
"dns_test_ok_toast": "Servidores DNS especificados funcionan correctamente",
|
||||
"example_regex_meaning": "bloquear el acceso a los dominios que coincidan con la expresi\u00f3n regular especificada",
|
||||
"example_upstream_regular": "DNS regular (mediante UDP)",
|
||||
"example_upstream_dot": "cifrado <0>DNS mediante TLS<\/0>",
|
||||
"example_upstream_doh": "cifrado <0>DNS mediante HTTPS<\/0>",
|
||||
"example_upstream_sdns": "puedes usar <0>DNS Stamps<\/0> para <1>DNSCrypt<\/1> o resolutores <2>DNS mediante HTTPS<\/2>",
|
||||
"example_upstream_tcp": "DNS regular (mediante TCP)",
|
||||
"all_filters_up_to_date_toast": "Todos los filtros ya est\u00e1n actualizados",
|
||||
"updated_upstream_dns_toast": "Servidores DNS de subida actualizados",
|
||||
"dns_test_ok_toast": "Los servidores DNS especificados funcionan correctamente",
|
||||
"dns_test_not_ok_toast": "Servidor \"{{key}}\": no puede ser usado, por favor, revise si lo ha escrito correctamente",
|
||||
"unblock_btn": "Desbloquear",
|
||||
"block_btn": "Bloquear",
|
||||
"time_table_header": "Tiempo",
|
||||
"domain_name_table_header": "Nombre de dominio",
|
||||
"time_table_header": "Hora",
|
||||
"domain_name_table_header": "Nombre del dominio",
|
||||
"type_table_header": "Tipo",
|
||||
"response_table_header": "Respuesta",
|
||||
"client_table_header": "Cliente",
|
||||
@@ -134,26 +141,116 @@
|
||||
"show_all_filter_type": "Mostrar todo",
|
||||
"show_filtered_type": "Mostrar filtrados",
|
||||
"no_logs_found": "No se han encontrado registros",
|
||||
"disabled_log_btn": "Desactivar registro",
|
||||
"download_log_file_btn": "Descargar el archivo de registro",
|
||||
"refresh_btn": "Refrescar",
|
||||
"enabled_log_btn": "Activar registro",
|
||||
"last_dns_queries": "\u00daltimas 500 solicitudes de DNS",
|
||||
"previous_btn": "Anterior",
|
||||
"disabled_log_btn": "Deshabilitar registro",
|
||||
"download_log_file_btn": "Descargar archivo de registro",
|
||||
"refresh_btn": "Actualizar",
|
||||
"enabled_log_btn": "Habilitar registro",
|
||||
"last_dns_queries": "\u00daltimas 5000 consultas DNS",
|
||||
"previous_btn": "Atr\u00e1s",
|
||||
"next_btn": "Siguiente",
|
||||
"loading_table_status": "Cargando...",
|
||||
"page_table_footer_text": "P\u00e1gina",
|
||||
"of_table_footer_text": "de",
|
||||
"rows_table_footer_text": "filas",
|
||||
"updated_custom_filtering_toast": "Actualizadas las reglas de filtrado personalizadas",
|
||||
"rule_removed_from_custom_filtering_toast": "Regla eliminada de las reglas de filtrado personalizadas",
|
||||
"rule_added_to_custom_filtering_toast": "Regla a\u00f1adida a las reglas de filtrado personalizadas",
|
||||
"query_log_disabled_toast": "Log de consulta desactivado",
|
||||
"query_log_enabled_toast": "Log de consulta activado",
|
||||
"updated_custom_filtering_toast": "Reglas de filtrado personalizado actualizadas",
|
||||
"rule_removed_from_custom_filtering_toast": "Regla eliminada de las reglas de filtrado personalizado",
|
||||
"rule_added_to_custom_filtering_toast": "Regla a\u00f1adida a las reglas de filtrado personalizado",
|
||||
"query_log_disabled_toast": "Registro de consultas deshabilitado",
|
||||
"query_log_enabled_toast": "Registro de consultas habilitado",
|
||||
"source_label": "Fuente",
|
||||
"found_in_known_domain_db": "Encontrado en la base de datos de dominios conocidos.",
|
||||
"category_label": "Categor\u00eda",
|
||||
"rule_label": "Regla",
|
||||
"filter_label": "Filtro",
|
||||
"unknown_filter": "Filtro desconocido {{filterId}}"
|
||||
"unknown_filter": "Filtro desconocido {{filterId}}",
|
||||
"install_welcome_title": "\u00a1Bienvenido a AdGuard Home!",
|
||||
"install_welcome_desc": "AdGuard Home es un servidor DNS de bloqueo de anuncios y rastreadores en toda la red. Su prop\u00f3sito es permitirle controlar toda su red y todos sus dispositivos, y no requiere el uso de un programa del lado del cliente.",
|
||||
"install_settings_title": "Interfaz web de administraci\u00f3n",
|
||||
"install_settings_listen": "Interfaz de escucha",
|
||||
"install_settings_port": "Puerto",
|
||||
"install_settings_interface_link": "Su interfaz web de administraci\u00f3n de AdGuard Home estar\u00e1 disponible en las siguientes direcciones:",
|
||||
"form_error_port": "Ingrese un valor de puerto v\u00e1lido",
|
||||
"install_settings_dns": "Servidor DNS",
|
||||
"install_settings_dns_desc": "Deber\u00e1 configurar sus dispositivos o router para usar el servidor DNS en las siguientes direcciones:",
|
||||
"install_settings_all_interfaces": "Todas las interfaces",
|
||||
"install_auth_title": "Autenticaci\u00f3n",
|
||||
"install_auth_desc": "Se recomienda encarecidamente configurar la autenticaci\u00f3n por contrase\u00f1a para la interfaz web del administraci\u00f3n de AdGuard Home. Incluso si solo es accesible en su red local, es importante que est\u00e9 protegido contra el acceso no autorizado.",
|
||||
"install_auth_username": "Usuario",
|
||||
"install_auth_password": "Contrase\u00f1a",
|
||||
"install_auth_confirm": "Confirmar contrase\u00f1a",
|
||||
"install_auth_username_enter": "Ingrese su nombre de usuario",
|
||||
"install_auth_password_enter": "Ingrese su contrase\u00f1a",
|
||||
"install_step": "Paso",
|
||||
"install_devices_title": "Configure sus dispositivos",
|
||||
"install_devices_desc": "Para que AdGuard Home comience a funcionar, necesita configurar sus dispositivos para usarlo.",
|
||||
"install_submit_title": "\u00a1Felicitaciones!",
|
||||
"install_submit_desc": "El proceso de configuraci\u00f3n ha finalizado y est\u00e1 listo para comenzar a usar AdGuard Home.",
|
||||
"install_devices_router": "Router",
|
||||
"install_devices_router_desc": "Esta configuraci\u00f3n cubrir\u00e1 autom\u00e1ticamente todos los dispositivos conectados a su router dom\u00e9stico y no necesitar\u00e1 configurar cada uno de ellos manualmente.",
|
||||
"install_devices_address": "El servidor DNS de AdGuard Home est\u00e1 escuchando las siguientes direcciones",
|
||||
"install_devices_router_list_1": "Abra las preferencias de su router. Por lo general, puede acceder a \u00e9l desde su navegador a trav\u00e9s de una URL (como http:\/\/192.168.0.1\/ o http:\/\/192.168.1.1\/). Se le puede pedir que ingrese la contrase\u00f1a. Si no lo recuerda, a menudo puede restablecer la contrase\u00f1a presionando un bot\u00f3n en el router. Algunos routers requieren una aplicaci\u00f3n espec\u00edfica, que en ese caso ya deber\u00eda estar instalada en su computadora\/tel\u00e9fono.",
|
||||
"install_devices_router_list_2": "Busque la configuraci\u00f3n de DHCP\/DNS. Busque las letras DNS junto a un campo que permita ingresar dos o tres grupos de n\u00fameros, cada uno dividido en cuatro grupos de uno a tres d\u00edgitos.",
|
||||
"install_devices_router_list_3": "Ingrese las direcciones de su servidor AdGuard Home all\u00ed.",
|
||||
"install_devices_windows_list_1": "Abra el Panel de control a trav\u00e9s del men\u00fa Inicio o en el buscador de Windows.",
|
||||
"install_devices_windows_list_2": "Vaya a la categor\u00eda Red e Internet, luego al Centro de redes y recursos compartidos.",
|
||||
"install_devices_windows_list_3": "En el lado izquierdo de la pantalla, busque Cambiar la configuraci\u00f3n del adaptador y haga clic en \u00e9l.",
|
||||
"install_devices_windows_list_4": "Seleccione su conexi\u00f3n activa, luego haga clic derecho sobre ella y elija Propiedades.",
|
||||
"install_devices_windows_list_5": "Busque el Protocolo de Internet versi\u00f3n 4 (TCP\/IP) en la lista, selecci\u00f3nelo y luego haga clic en Propiedades nuevamente.",
|
||||
"install_devices_windows_list_6": "Elija Usar las siguientes direcciones de servidor DNS e ingrese las direcciones de su servidor AdGuard Home.",
|
||||
"install_devices_macos_list_1": "Haga clic en el icono de Apple y vaya a Preferencias del sistema.",
|
||||
"install_devices_macos_list_2": "Haga clic en Red.",
|
||||
"install_devices_macos_list_3": "Seleccione la primera conexi\u00f3n de la lista y haga clic en Avanzado.",
|
||||
"install_devices_macos_list_4": "Seleccione la pesta\u00f1a DNS e ingrese las direcciones de su servidor AdGuard Home.",
|
||||
"install_devices_android_list_1": "En la pantalla de inicio del men\u00fa Android, pulse en Configuraci\u00f3n.",
|
||||
"install_devices_android_list_2": "Pulse Wi-Fi en el men\u00fa. Aparecer\u00e1 la pantalla que lista todas las redes disponibles (es imposible configurar DNS personalizados para la conexi\u00f3n m\u00f3vil).",
|
||||
"install_devices_android_list_3": "Mantenga presionada la red a la que est\u00e1 conectado y pulse Modificar red.",
|
||||
"install_devices_android_list_4": "En algunos dispositivos, es posible que deba marcar la casilla Avanzado para ver m\u00e1s configuraciones. Para ajustar la configuraci\u00f3n DNS de su Android, deber\u00e1 cambiar la configuraci\u00f3n de IP de DHCP a Est\u00e1tica.",
|
||||
"install_devices_android_list_5": "Cambie los valores de DNS 1 y DNS 2 a las direcciones de su servidor AdGuard Home.",
|
||||
"install_devices_ios_list_1": "En la pantalla de inicio, pulse en Configuraci\u00f3n.",
|
||||
"install_devices_ios_list_2": "Elija Wi-Fi en el men\u00fa de la izquierda (es imposible configurar DNS para redes m\u00f3viles).",
|
||||
"install_devices_ios_list_3": "Pulse sobre el nombre de la red activa en ese momento.",
|
||||
"install_devices_ios_list_4": "En ese campo DNS ingrese las direcciones de su servidor AdGuard Home.",
|
||||
"get_started": "Comenzar",
|
||||
"next": "Siguiente",
|
||||
"open_dashboard": "Abrir panel de control",
|
||||
"install_saved": "Guardado correctamente",
|
||||
"encryption_title": "Cifrado",
|
||||
"encryption_desc": "Soporte para cifrado (HTTPS\/TLS) tanto para DNS como para la interfaz web de administraci\u00f3n",
|
||||
"encryption_config_saved": "Configuraci\u00f3n de cifrado guardado",
|
||||
"encryption_server": "Nombre del servidor",
|
||||
"encryption_server_enter": "Ingrese su nombre de dominio",
|
||||
"encryption_server_desc": "Para utilizar HTTPS, debe ingresar el nombre del servidor que coincida con su certificado SSL.",
|
||||
"encryption_redirect": "Redireccionar a HTTPS autom\u00e1ticamente",
|
||||
"encryption_redirect_desc": "Si est\u00e1 marcado, AdGuard Home redireccionar\u00e1 autom\u00e1ticamente de HTTP a las direcciones HTTPS.",
|
||||
"encryption_https": "Puerto HTTPS",
|
||||
"encryption_https_desc": "Si el puerto HTTPS est\u00e1 configurado, la interfaz de administraci\u00f3n de AdGuard Home ser\u00e1 accesible a trav\u00e9s de HTTPS, y tambi\u00e9n proporcionar\u00e1 DNS mediante HTTPS en la ubicaci\u00f3n '\/dns-query'.",
|
||||
"encryption_dot": "Puerto para DNS mediante TLS",
|
||||
"encryption_dot_desc": "Si este puerto est\u00e1 configurado, AdGuard Home ejecutar\u00e1 un servidor DNS mediante TLS en este puerto.",
|
||||
"encryption_certificates": "Certificados",
|
||||
"encryption_certificates_desc": "Para utilizar el cifrado, debe proporcionar una cadena de certificados SSL v\u00e1lida para su dominio. Puede obtener un certificado gratuito en <0>{{link}}<\/0> o puede comprarlo en una de las autoridades de certificaci\u00f3n de confianza.",
|
||||
"encryption_certificates_input": "Copie\/pegue aqu\u00ed sus certificados codificados PEM.",
|
||||
"encryption_status": "Estado",
|
||||
"encryption_expire": "Expira",
|
||||
"encryption_key": "Clave privada",
|
||||
"encryption_key_input": "Copie\/pegue aqu\u00ed su clave privada codificada PEM para su certificado.",
|
||||
"encryption_enable": "Habilitar el cifrado (HTTPS, DNS mediante HTTPS y DNS mediante TLS)",
|
||||
"encryption_enable_desc": "Si el cifrado est\u00e1 habilitado, la interfaz de administraci\u00f3n de AdGuard Home funcionar\u00e1 a trav\u00e9s de HTTPS, y el servidor DNS escuchar\u00e1 las peticiones DNS mediante HTTPS y DNS mediante TLS.",
|
||||
"encryption_chain_valid": "La cadena de certificado es v\u00e1lida",
|
||||
"encryption_chain_invalid": "La cadena de certificado no es v\u00e1lida",
|
||||
"encryption_key_valid": "Esta es una clave privada {{type}} v\u00e1lida",
|
||||
"encryption_key_invalid": "Esta es una clave privada {{type}} no v\u00e1lida",
|
||||
"encryption_subject": "Asunto",
|
||||
"encryption_issuer": "Emisor",
|
||||
"encryption_hostnames": "Nombres de hosts",
|
||||
"encryption_reset": "\u00bfEst\u00e1 seguro de que desea restablecer la configuraci\u00f3n de cifrado?",
|
||||
"topline_expiring_certificate": "Su certificado SSL est\u00e1 a punto de expirar. Actualice la <0>configuraci\u00f3n del cifrado<\/0>.",
|
||||
"topline_expired_certificate": "Su certificado SSL ha expirado. Actualice la <0>configuraci\u00f3n del cifrado<\/0>.",
|
||||
"form_error_port_range": "Ingrese el valor del puerto en el rango de 80 - 65535",
|
||||
"form_error_port_unsafe": "Este es un puerto inseguro",
|
||||
"form_error_equal": "No deber\u00eda ser igual",
|
||||
"form_error_password": "La contrase\u00f1a no coincide",
|
||||
"reset_settings": "Restablecer configuraci\u00f3n",
|
||||
"update_announcement": "\u00a1AdGuard Home {{version}} ya est\u00e1 disponible! <0>Haga clic aqu\u00ed<\/0> para m\u00e1s informaci\u00f3n.",
|
||||
"setup_guide": "Gu\u00eda de configuraci\u00f3n",
|
||||
"dns_addresses": "Direcciones DNS"
|
||||
}
|
||||
@@ -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,10 @@
|
||||
{
|
||||
"url_added_successfully": "Url adicionada com sucesso",
|
||||
"check_dhcp_servers": "Verifique se h\u00e1 servidores DHCP",
|
||||
"example_upstream_reserved": "Voc\u00ea pode especificar um DNS upstream <0>para um dom\u00ednio(s) especifico<\/0>",
|
||||
"upstream_parallel": "Usar consultas paralelas para acelerar a resolu\u00e7\u00e3o consultando simultaneamente todos os servidores upstream",
|
||||
"bootstrap_dns": "Servidores DNS de inicializa\u00e7\u00e3o",
|
||||
"bootstrap_dns_desc": "Servidores DNS de inicializa\u00e7\u00e3o s\u00e3o usados para resolver endere\u00e7os IP dos resolvedores DoH\/DoT que voc\u00ea especifica como upstreams.",
|
||||
"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 +32,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",
|
||||
@@ -78,7 +83,7 @@
|
||||
"no_settings": "N\u00e3o configurado",
|
||||
"general_settings": "Configura\u00e7\u00f5es gerais",
|
||||
"upstream_dns": "Servidores DNS upstream",
|
||||
"upstream_dns_hint": "Se voc\u00ea deixar este campo vazio, o AdGuard Home ir\u00e1 usar o<a href='https:\/\/1.1.1.1\/' target='_blank'>DNS da Cloudflare<\/a> como upstream. Use o prefixo tls:\/\/ para servidores DNS com TLS.",
|
||||
"upstream_dns_hint": "Se voc\u00ea deixar este campo vazio, o AdGuard Home ir\u00e1 usar o<a href='https:\/\/1.1.1.1\/' target='_blank'>DNS da Cloudflare<\/a> como upstream.",
|
||||
"test_upstream_btn": "Testar upstreams",
|
||||
"apply_btn": "Aplicar",
|
||||
"disabled_filtering_toast": "Filtragem desativada",
|
||||
@@ -115,10 +120,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": "<0>DNS-sobre-TLS<\/0> criptografado",
|
||||
"example_upstream_doh": "<0>DNS-sobre-HTTPS<\/0> criptografado",
|
||||
"example_upstream_sdns": "Voc\u00ea pode usar <0>DNS Stamps<\/0>para o <1>DNSCrypt<\/1>ou usar os 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 +162,95 @@
|
||||
"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": "Suporte a criptografia (HTTPS\/TLS) para DNS e interface de administra\u00e7\u00e3o web",
|
||||
"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": "Para usar o protocolo HTTPS, voc\u00ea precisa digitar o nome do servidor que corresponde ao seu certificado SSL.",
|
||||
"encryption_redirect": "Redirecionar automaticamente para HTTPS",
|
||||
"encryption_redirect_desc": "Se marcado, o AdGuard Home ir\u00e1 redirecionar automaticamente os endere\u00e7os HTTP para HTTPS.",
|
||||
"encryption_https": "Porta HTTPS",
|
||||
"encryption_https_desc": "Se a porta HTTPS estiver configurada, a interface administrativa do AdGuard Home ser\u00e1 acess\u00edvel via HTTPS e tamb\u00e9m fornecer\u00e1 o DNS-sobre-HTTPS no local '\/dns-query'.",
|
||||
"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": "Para usar criptografia, voc\u00ea precisa fornecer uma cadeia de certificados SSL v\u00e1lida para seu dom\u00ednio. Voc\u00ea pode obter um certificado gratuito em <0> {{link}}<\/0> ou pode compr\u00e1-lo de uma das autoridades de certifica\u00e7\u00e3o confi\u00e1veis.",
|
||||
"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": "Se a criptografia estiver ativada, a interface administrativa do AdGuard Home funcionar\u00e1 em HTTPS, o servidor DNS ir\u00e1 capturar as solicita\u00e7\u00f5es por meio do DNS-sobre-HTTPS e DNS-sobre-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.",
|
||||
"setup_guide": "Guia de configura\u00e7\u00e3o",
|
||||
"dns_addresses": "Endere\u00e7os DNS"
|
||||
}
|
||||
@@ -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,8 @@
|
||||
{
|
||||
"example_upstream_reserved": "\u60a8\u53ef\u660e\u78ba\u6307\u5b9a<0>\u7528\u65bc\u7279\u5b9a\u7684\u7db2\u57df<\/0>\u4e4bDNS\u4e0a\u6e38",
|
||||
"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 +83,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 +120,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 <1>DNSCrypt<\/1> \u6216 <2>DNS-over-HTTPS<\/2> \u89e3\u6790\u5668\u4e4b <0>DNS \u6233\u8a18<\/0>",
|
||||
"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 +214,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';
|
||||
|
||||
@@ -139,6 +139,36 @@ export const toggleProtection = status => async (dispatch) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getVersionRequest = createAction('GET_VERSION_REQUEST');
|
||||
export const getVersionFailure = createAction('GET_VERSION_FAILURE');
|
||||
export const getVersionSuccess = createAction('GET_VERSION_SUCCESS');
|
||||
|
||||
export const getVersion = () => async (dispatch) => {
|
||||
dispatch(getVersionRequest());
|
||||
try {
|
||||
const newVersion = await apiClient.getGlobalVersion();
|
||||
dispatch(getVersionSuccess(newVersion));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(getVersionFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const getClientsRequest = createAction('GET_CLIENTS_REQUEST');
|
||||
export const getClientsFailure = createAction('GET_CLIENTS_FAILURE');
|
||||
export const getClientsSuccess = createAction('GET_CLIENTS_SUCCESS');
|
||||
|
||||
export const getClients = () => async (dispatch) => {
|
||||
dispatch(getClientsRequest());
|
||||
try {
|
||||
const clients = await apiClient.getGlobalClients();
|
||||
dispatch(getClientsSuccess(clients));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(getClientsFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const dnsStatusRequest = createAction('DNS_STATUS_REQUEST');
|
||||
export const dnsStatusFailure = createAction('DNS_STATUS_FAILURE');
|
||||
export const dnsStatusSuccess = createAction('DNS_STATUS_SUCCESS');
|
||||
@@ -148,6 +178,8 @@ export const getDnsStatus = () => async (dispatch) => {
|
||||
try {
|
||||
const dnsStatus = await apiClient.getGlobalStatus();
|
||||
dispatch(dnsStatusSuccess(dnsStatus));
|
||||
dispatch(getVersion());
|
||||
dispatch(getClients());
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(initSettingsFailure());
|
||||
@@ -205,21 +237,6 @@ export const getStats = () => async (dispatch) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getVersionRequest = createAction('GET_VERSION_REQUEST');
|
||||
export const getVersionFailure = createAction('GET_VERSION_FAILURE');
|
||||
export const getVersionSuccess = createAction('GET_VERSION_SUCCESS');
|
||||
|
||||
export const getVersion = () => async (dispatch) => {
|
||||
dispatch(getVersionRequest());
|
||||
try {
|
||||
const newVersion = await apiClient.getGlobalVersion();
|
||||
dispatch(getVersionSuccess(newVersion));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(getVersionFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const getTopStatsRequest = createAction('GET_TOP_STATS_REQUEST');
|
||||
export const getTopStatsFailure = createAction('GET_TOP_STATS_FAILURE');
|
||||
export const getTopStatsSuccess = createAction('GET_TOP_STATS_SUCCESS');
|
||||
@@ -452,10 +469,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 +493,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') {
|
||||
|
||||
@@ -44,3 +44,18 @@ export const setAllSettings = values => async (dispatch) => {
|
||||
dispatch(prevStep());
|
||||
}
|
||||
};
|
||||
|
||||
export const checkConfigRequest = createAction('CHECK_CONFIG_REQUEST');
|
||||
export const checkConfigFailure = createAction('CHECK_CONFIG_FAILURE');
|
||||
export const checkConfigSuccess = createAction('CHECK_CONFIG_SUCCESS');
|
||||
|
||||
export const checkConfig = values => async (dispatch) => {
|
||||
dispatch(checkConfigRequest());
|
||||
try {
|
||||
const check = await apiClient.checkConfig(values);
|
||||
dispatch(checkConfigSuccess(check));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(checkConfigFailure());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -34,11 +34,12 @@ 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' };
|
||||
GLOBAL_DISABLE_PROTECTION = { path: 'disable_protection', method: 'POST' };
|
||||
GLOBAL_CLIENTS = { path: 'clients', method: 'GET' }
|
||||
|
||||
restartGlobalFiltering() {
|
||||
const { path, method } = this.GLOBAL_RESTART;
|
||||
@@ -110,7 +111,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 +120,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);
|
||||
}
|
||||
@@ -139,13 +140,18 @@ export default class Api {
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
getGlobalClients() {
|
||||
const { path, method } = this.GLOBAL_CLIENTS;
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
// Filtering
|
||||
FILTERING_STATUS = { path: 'filtering/status', method: 'GET' };
|
||||
FILTERING_ENABLE = { path: 'filtering/enable', method: 'POST' };
|
||||
FILTERING_DISABLE = { path: 'filtering/disable', method: 'POST' };
|
||||
FILTERING_ADD_FILTER = { path: 'filtering/add_url', method: 'PUT' };
|
||||
FILTERING_REMOVE_FILTER = { path: 'filtering/remove_url', method: 'DELETE' };
|
||||
FILTERING_SET_RULES = { path: 'filtering/set_rules', method: 'PUT' };
|
||||
FILTERING_ADD_FILTER = { path: 'filtering/add_url', method: 'POST' };
|
||||
FILTERING_REMOVE_FILTER = { path: 'filtering/remove_url', method: 'POST' };
|
||||
FILTERING_SET_RULES = { path: 'filtering/set_rules', method: 'POST' };
|
||||
FILTERING_ENABLE_FILTER = { path: 'filtering/enable_url', method: 'POST' };
|
||||
FILTERING_DISABLE_FILTER = { path: 'filtering/disable_url', method: 'POST' };
|
||||
FILTERING_REFRESH = { path: 'filtering/refresh', method: 'POST' };
|
||||
@@ -344,6 +350,7 @@ export default class Api {
|
||||
// Installation
|
||||
INSTALL_GET_ADDRESSES = { path: 'install/get_addresses', method: 'GET' };
|
||||
INSTALL_CONFIGURE = { path: 'install/configure', method: 'POST' };
|
||||
INSTALL_CHECK_CONFIG = { path: 'install/check_config', method: 'POST' };
|
||||
|
||||
getDefaultAddresses() {
|
||||
const { path, method } = this.INSTALL_GET_ADDRESSES;
|
||||
@@ -359,6 +366,15 @@ export default class Api {
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
|
||||
checkConfig(config) {
|
||||
const { path, method } = this.INSTALL_CHECK_CONFIG;
|
||||
const parameters = {
|
||||
data: config,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
};
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
|
||||
// DNS-over-HTTPS and DNS-over-TLS
|
||||
TLS_STATUS = { path: 'tls/status', method: 'GET' };
|
||||
TLS_CONFIG = { path: 'tls/configure', method: 'POST' };
|
||||
|
||||
@@ -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';
|
||||
@@ -24,7 +25,6 @@ import i18n from '../../i18n';
|
||||
class App extends Component {
|
||||
componentDidMount() {
|
||||
this.props.getDnsStatus();
|
||||
this.props.getVersion();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
@@ -86,6 +86,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>
|
||||
@@ -103,7 +104,6 @@ App.propTypes = {
|
||||
dashboard: PropTypes.object,
|
||||
isCoreRunning: PropTypes.bool,
|
||||
error: PropTypes.string,
|
||||
getVersion: PropTypes.func,
|
||||
changeLanguage: PropTypes.func,
|
||||
encryption: PropTypes.object,
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Trans, withNamespaces } from 'react-i18next';
|
||||
import Card from '../ui/Card';
|
||||
import Cell from '../ui/Cell';
|
||||
|
||||
import { getPercent } from '../../helpers/helpers';
|
||||
import { getPercent, getClientName } from '../../helpers/helpers';
|
||||
import { STATUS_COLORS } from '../../helpers/constants';
|
||||
|
||||
class Clients extends Component {
|
||||
@@ -23,7 +23,24 @@ class Clients extends Component {
|
||||
columns = [{
|
||||
Header: 'IP',
|
||||
accessor: 'ip',
|
||||
Cell: ({ value }) => (<div className="logs__row logs__row--overflow"><span className="logs__text" title={value}>{value}</span></div>),
|
||||
Cell: ({ value }) => {
|
||||
const clientName = getClientName(this.props.clients, value);
|
||||
let client;
|
||||
|
||||
if (clientName) {
|
||||
client = <span>{clientName} <small>({value})</small></span>;
|
||||
} else {
|
||||
client = value;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="logs__row logs__row--overflow">
|
||||
<span className="logs__text" title={value}>
|
||||
{client}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
sortMethod: (a, b) => parseInt(a.replace(/\./g, ''), 10) - parseInt(b.replace(/\./g, ''), 10),
|
||||
}, {
|
||||
Header: <Trans>requests_count</Trans>,
|
||||
@@ -61,6 +78,7 @@ Clients.propTypes = {
|
||||
topClients: PropTypes.object.isRequired,
|
||||
dnsQueries: PropTypes.number.isRequired,
|
||||
refreshButton: PropTypes.node.isRequired,
|
||||
clients: PropTypes.array.isRequired,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ class Dashboard extends Component {
|
||||
dashboard.processing ||
|
||||
dashboard.processingStats ||
|
||||
dashboard.processingStatsHistory ||
|
||||
dashboard.processingClients ||
|
||||
dashboard.processingTopStats;
|
||||
|
||||
const refreshFullButton = <button type="button" className="btn btn-outline-primary btn-sm" onClick={() => this.getAllStats()}><Trans>refresh_statics</Trans></button>;
|
||||
@@ -94,6 +95,7 @@ class Dashboard extends Component {
|
||||
dnsQueries={dashboard.stats.dns_queries}
|
||||
refreshButton={refreshButton}
|
||||
topClients={dashboard.topStats.top_clients}
|
||||
clients={dashboard.clients}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-lg-6">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -110,6 +117,10 @@
|
||||
.nav-version {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1280px) {
|
||||
@@ -120,6 +131,10 @@
|
||||
.nav-version {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.dns-status {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -6,7 +6,7 @@ import escapeRegExp from 'lodash/escapeRegExp';
|
||||
import endsWith from 'lodash/endsWith';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
|
||||
import { formatTime } from '../../helpers/helpers';
|
||||
import { formatTime, getClientName } from '../../helpers/helpers';
|
||||
import { getTrackerData } from '../../helpers/trackers/trackers';
|
||||
import PageTitle from '../ui/PageTitle';
|
||||
import Card from '../ui/Card';
|
||||
@@ -86,7 +86,7 @@ class Logs extends Component {
|
||||
}
|
||||
|
||||
renderLogs(logs) {
|
||||
const { t } = this.props;
|
||||
const { t, dashboard } = this.props;
|
||||
const columns = [{
|
||||
Header: t('time_table_header'),
|
||||
accessor: 'time',
|
||||
@@ -196,11 +196,19 @@ class Logs extends Component {
|
||||
Cell: (row) => {
|
||||
const { reason } = row.original;
|
||||
const isFiltered = row ? reason.indexOf('Filtered') === 0 : false;
|
||||
const clientName = getClientName(dashboard.clients, row.value);
|
||||
let client;
|
||||
|
||||
if (clientName) {
|
||||
client = <span>{clientName} <small>({row.value})</small></span>;
|
||||
} else {
|
||||
client = row.value;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="logs__row">
|
||||
{row.value}
|
||||
{client}
|
||||
</div>
|
||||
{this.renderBlockingButton(isFiltered, row.original.domain)}
|
||||
</Fragment>
|
||||
@@ -315,9 +323,18 @@ class Logs extends Component {
|
||||
</div>
|
||||
</PageTitle>
|
||||
<Card>
|
||||
{queryLogEnabled && queryLogs.getLogsProcessing && <Loading />}
|
||||
{queryLogEnabled && !queryLogs.getLogsProcessing &&
|
||||
this.renderLogs(queryLogs.logs)}
|
||||
{
|
||||
queryLogEnabled
|
||||
&& queryLogs.getLogsProcessing
|
||||
&& dashboard.processingClients
|
||||
&& <Loading />
|
||||
}
|
||||
{
|
||||
queryLogEnabled
|
||||
&& !queryLogs.getLogsProcessing
|
||||
&& !dashboard.processingClients
|
||||
&& this.renderLogs(queryLogs.logs)
|
||||
}
|
||||
</Card>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
@@ -78,7 +78,8 @@ let Interface = (props) => {
|
||||
</div>
|
||||
{interfaceValue &&
|
||||
<div className="col-sm-12 col-md-6">
|
||||
{renderInterfaceValues(interfaces[interfaceValue])}
|
||||
{interfaces[interfaceValue] &&
|
||||
renderInterfaceValues(interfaces[interfaceValue])}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,32 +1,49 @@
|
||||
import React from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactTable from 'react-table';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
|
||||
const columns = [{
|
||||
Header: 'MAC',
|
||||
accessor: 'mac',
|
||||
}, {
|
||||
Header: 'IP',
|
||||
accessor: 'ip',
|
||||
}, {
|
||||
Header: <Trans>dhcp_table_hostname</Trans>,
|
||||
accessor: 'hostname',
|
||||
}, {
|
||||
Header: <Trans>dhcp_table_expires</Trans>,
|
||||
accessor: 'expires',
|
||||
}];
|
||||
class Leases extends Component {
|
||||
cellWrap = ({ value }) => (
|
||||
<div className="logs__row logs__row--overflow">
|
||||
<span className="logs__text" title={value}>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const Leases = props => (
|
||||
<ReactTable
|
||||
data={props.leases || []}
|
||||
columns={columns}
|
||||
showPagination={false}
|
||||
noDataText={ props.t('dhcp_leases_not_found') }
|
||||
minRows={6}
|
||||
className="-striped -highlight card-table-overflow"
|
||||
/>
|
||||
);
|
||||
render() {
|
||||
const { leases, t } = this.props;
|
||||
return (
|
||||
<ReactTable
|
||||
data={leases || []}
|
||||
columns={[
|
||||
{
|
||||
Header: 'MAC',
|
||||
accessor: 'mac',
|
||||
Cell: this.cellWrap,
|
||||
}, {
|
||||
Header: 'IP',
|
||||
accessor: 'ip',
|
||||
Cell: this.cellWrap,
|
||||
}, {
|
||||
Header: <Trans>dhcp_table_hostname</Trans>,
|
||||
accessor: 'hostname',
|
||||
Cell: this.cellWrap,
|
||||
}, {
|
||||
Header: <Trans>dhcp_table_expires</Trans>,
|
||||
accessor: 'expires',
|
||||
Cell: this.cellWrap,
|
||||
},
|
||||
]}
|
||||
showPagination={false}
|
||||
noDataText={t('dhcp_leases_not_found')}
|
||||
minRows={6}
|
||||
className="-striped -highlight card-table-overflow"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Leases.propTypes = {
|
||||
leases: PropTypes.array,
|
||||
|
||||
@@ -3,10 +3,12 @@ import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
|
||||
import { DHCP_STATUS_RESPONSE } from '../../../helpers/constants';
|
||||
import Form from './Form';
|
||||
import Leases from './Leases';
|
||||
import Interface from './Interface';
|
||||
import Card from '../../ui/Card';
|
||||
import Accordion from '../../ui/Accordion';
|
||||
|
||||
class Dhcp extends Component {
|
||||
handleFormSubmit = (values) => {
|
||||
@@ -19,11 +21,12 @@ class Dhcp extends Component {
|
||||
|
||||
getToggleDhcpButton = () => {
|
||||
const {
|
||||
config, active, processingDhcp, processingConfig,
|
||||
config, check, processingDhcp, processingConfig,
|
||||
} = this.props.dhcp;
|
||||
const activeDhcpFound = active && active.found;
|
||||
const otherDhcpFound =
|
||||
check && check.otherServer && check.otherServer.found === DHCP_STATUS_RESPONSE.YES;
|
||||
const filledConfig = Object.keys(config).every((key) => {
|
||||
if (key === 'enabled') {
|
||||
if (key === 'enabled' || key === 'icmp_timeout_msec') {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -50,7 +53,8 @@ class Dhcp extends Component {
|
||||
onClick={() => this.handleToggle(config)}
|
||||
disabled={
|
||||
!filledConfig
|
||||
|| activeDhcpFound
|
||||
|| !check
|
||||
|| otherDhcpFound
|
||||
|| processingDhcp
|
||||
|| processingConfig
|
||||
}
|
||||
@@ -60,33 +64,89 @@ class Dhcp extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
getActiveDhcpMessage = () => {
|
||||
const { active } = this.props.dhcp;
|
||||
|
||||
if (active) {
|
||||
if (active.error) {
|
||||
return (
|
||||
<div className="text-danger mb-2">
|
||||
{active.error}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
getActiveDhcpMessage = (t, check) => {
|
||||
const { found } = check.otherServer;
|
||||
|
||||
if (found === DHCP_STATUS_RESPONSE.ERROR) {
|
||||
return (
|
||||
<div className="mb-2">
|
||||
{active.found ? (
|
||||
<div className="text-danger">
|
||||
<Trans>dhcp_found</Trans>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-secondary">
|
||||
<Trans>dhcp_not_found</Trans>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-danger mb-2">
|
||||
<Trans>dhcp_error</Trans>
|
||||
<div className="mt-2 mb-2">
|
||||
<Accordion label={t('error_details')}>
|
||||
<span>{check.otherServer.error}</span>
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-2">
|
||||
{found === DHCP_STATUS_RESPONSE.YES ? (
|
||||
<div className="text-danger">
|
||||
<Trans>dhcp_found</Trans>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-secondary">
|
||||
<Trans>dhcp_not_found</Trans>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getDhcpWarning = (check) => {
|
||||
if (check.otherServer.found === DHCP_STATUS_RESPONSE.NO) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="text-danger">
|
||||
<Trans>dhcp_warning</Trans>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getStaticIpWarning = (t, check, interfaceName) => {
|
||||
if (check.staticIP.static === DHCP_STATUS_RESPONSE.ERROR) {
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="text-danger mb-2">
|
||||
<Trans>dhcp_static_ip_error</Trans>
|
||||
<div className="mt-2 mb-2">
|
||||
<Accordion label={t('error_details')}>
|
||||
<span>{check.staticIP.error}</span>
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
<hr className="mt-4 mb-4"/>
|
||||
</Fragment>
|
||||
);
|
||||
} else if (
|
||||
check.staticIP.static === DHCP_STATUS_RESPONSE.NO
|
||||
&& check.staticIP.ip
|
||||
&& interfaceName
|
||||
) {
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="text-secondary mb-2">
|
||||
<Trans
|
||||
components={[
|
||||
<strong key="0">example</strong>,
|
||||
]}
|
||||
values={{
|
||||
interfaceName,
|
||||
ipAddress: check.staticIP.ip,
|
||||
}}
|
||||
>
|
||||
dhcp_dynamic_ip_found
|
||||
</Trans>
|
||||
</div>
|
||||
<hr className="mt-4 mb-4"/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -131,17 +191,21 @@ class Dhcp extends Component {
|
||||
this.props.findActiveDhcp(dhcp.config.interface_name)
|
||||
}
|
||||
disabled={
|
||||
!dhcp.config.interface_name
|
||||
dhcp.config.enabled
|
||||
|| !dhcp.config.interface_name
|
||||
|| dhcp.processingConfig
|
||||
}
|
||||
>
|
||||
<Trans>check_dhcp_servers</Trans>
|
||||
</button>
|
||||
</div>
|
||||
{this.getActiveDhcpMessage()}
|
||||
<div className="text-danger">
|
||||
<Trans>dhcp_warning</Trans>
|
||||
</div>
|
||||
{!enabled && dhcp.check &&
|
||||
<Fragment>
|
||||
{this.getStaticIpWarning(t, dhcp.check, interface_name)}
|
||||
{this.getActiveDhcpMessage(t, dhcp.check)}
|
||||
{this.getDhcpWarning(dhcp.check)}
|
||||
</Fragment>
|
||||
}
|
||||
</Fragment>
|
||||
}
|
||||
</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">
|
||||
@@ -251,7 +261,7 @@ let Form = (props) => {
|
||||
component="textarea"
|
||||
type="text"
|
||||
className="form-control form-control--textarea"
|
||||
placeholder="Copy/paste your PEM-encoded private key for your cerficate here."
|
||||
placeholder={t('encryption_key_input')}
|
||||
onChange={handleChange}
|
||||
disabled={!isEnabled}
|
||||
/>
|
||||
@@ -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,
|
||||
|
||||
@@ -9,7 +9,9 @@ import Card from '../../ui/Card';
|
||||
|
||||
class Encryption extends Component {
|
||||
componentDidMount() {
|
||||
this.props.validateTlsConfig(this.props.encryption);
|
||||
if (this.props.encryption.enabled) {
|
||||
this.props.validateTlsConfig(this.props.encryption);
|
||||
}
|
||||
}
|
||||
|
||||
handleFormSubmit = (values) => {
|
||||
|
||||
@@ -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/AdGuardHome/wiki/Configuration#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);
|
||||
32
client/src/components/ui/Accordion.css
Normal file
32
client/src/components/ui/Accordion.css
Normal file
@@ -0,0 +1,32 @@
|
||||
.accordion {
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.accordion__label {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding-left: 25px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.accordion__label:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
left: 0;
|
||||
width: 17px;
|
||||
height: 10px;
|
||||
background-image: url("./svg/chevron-down.svg");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: 100%;
|
||||
}
|
||||
|
||||
.accordion__label--open:after {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.accordion__content {
|
||||
padding-top: 5px;
|
||||
}
|
||||
43
client/src/components/ui/Accordion.js
Normal file
43
client/src/components/ui/Accordion.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './Accordion.css';
|
||||
|
||||
class Accordion extends Component {
|
||||
state = {
|
||||
isOpen: false,
|
||||
}
|
||||
|
||||
handleClick = () => {
|
||||
this.setState(prevState => ({ isOpen: !prevState.isOpen }));
|
||||
};
|
||||
|
||||
render() {
|
||||
const accordionClass = this.state.isOpen
|
||||
? 'accordion__label accordion__label--open'
|
||||
: 'accordion__label';
|
||||
|
||||
return (
|
||||
<div className="accordion">
|
||||
<div
|
||||
className={accordionClass}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
{this.props.label}
|
||||
</div>
|
||||
{this.state.isOpen && (
|
||||
<div className="accordion__content">
|
||||
{this.props.children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Accordion.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default Accordion;
|
||||
@@ -91,6 +91,10 @@
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.checkbox__label-text--long {
|
||||
max-width: initial;
|
||||
}
|
||||
|
||||
.checkbox__label-title {
|
||||
display: block;
|
||||
line-height: 1.5;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
import { REPOSITORY, LANGUAGES } from '../../helpers/constants';
|
||||
import { REPOSITORY, LANGUAGES, PRIVACY_POLICY_LINK } from '../../helpers/constants';
|
||||
import i18n from '../../i18n';
|
||||
|
||||
import './Footer.css';
|
||||
@@ -30,6 +30,9 @@ class Footer extends Component {
|
||||
<a href={REPOSITORY.URL} className="footer__link" target="_blank" rel="noopener noreferrer">
|
||||
<Trans>homepage</Trans>
|
||||
</a>
|
||||
<a href={PRIVACY_POLICY_LINK} className="footer__link" target="_blank" rel="noopener noreferrer">
|
||||
<Trans>privacy_policy</Trans>
|
||||
</a>
|
||||
<a href={`${REPOSITORY.URL}/issues/new`} className="btn btn-outline-primary btn-sm footer__link footer__link--report" target="_blank" rel="noopener noreferrer">
|
||||
<Trans>report_an_issue</Trans>
|
||||
</a>
|
||||
|
||||
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);
|
||||
@@ -22,6 +22,8 @@ export const REPOSITORY = {
|
||||
TRACKERS_DB: 'https://github.com/AdguardTeam/AdGuardHome/tree/master/client/src/helpers/trackers/adguard.json',
|
||||
};
|
||||
|
||||
export const PRIVACY_POLICY_LINK = 'https://adguard.com/privacy/home.html';
|
||||
|
||||
export const LANGUAGES = [
|
||||
{
|
||||
key: 'en',
|
||||
@@ -47,6 +49,10 @@ export const LANGUAGES = [
|
||||
key: 'vi',
|
||||
name: 'Tiếng Việt',
|
||||
},
|
||||
{
|
||||
key: 'bg',
|
||||
name: 'Български',
|
||||
},
|
||||
{
|
||||
key: 'ru',
|
||||
name: 'Русский',
|
||||
@@ -59,6 +65,10 @@ export const LANGUAGES = [
|
||||
key: 'zh-tw',
|
||||
name: '正體中文',
|
||||
},
|
||||
{
|
||||
key: 'zh-cn',
|
||||
name: '简体中文',
|
||||
},
|
||||
];
|
||||
|
||||
export const INSTALL_FIRST_STEP = 1;
|
||||
@@ -147,3 +157,11 @@ export const UNSAFE_PORTS = [
|
||||
6668,
|
||||
6669,
|
||||
];
|
||||
|
||||
export const ALL_INTERFACES_IP = '0.0.0.0';
|
||||
|
||||
export const DHCP_STATUS_RESPONSE = {
|
||||
YES: 'yes',
|
||||
NO: 'no',
|
||||
ERROR: 'error',
|
||||
};
|
||||
|
||||
@@ -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,10 @@ 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);
|
||||
|
||||
export const getClientName = (clients, ip) => {
|
||||
const client = clients.find(item => ip === item.ip);
|
||||
return (client && client.name) || '';
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,12 +2,13 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getIpList, getDnsAddress, getWebAddress } from '../../helpers/helpers';
|
||||
import { ALL_INTERFACES_IP } from '../../helpers/constants';
|
||||
|
||||
const AddressList = (props) => {
|
||||
let webAddress = getWebAddress(props.address, props.port);
|
||||
let dnsAddress = getDnsAddress(props.address, props.port);
|
||||
|
||||
if (props.address === '0.0.0.0') {
|
||||
if (props.address === ALL_INTERFACES_IP) {
|
||||
return getIpList(props.interfaces).map((ip) => {
|
||||
webAddress = getWebAddress(ip, props.port);
|
||||
dnsAddress = getDnsAddress(ip, props.port);
|
||||
|
||||
@@ -25,13 +25,22 @@ class Controls extends Component {
|
||||
}
|
||||
|
||||
renderNextButton(step) {
|
||||
const {
|
||||
nextStep,
|
||||
invalid,
|
||||
pristine,
|
||||
install,
|
||||
ip,
|
||||
port,
|
||||
} = this.props;
|
||||
|
||||
switch (step) {
|
||||
case 1:
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-success btn-lg setup__button"
|
||||
onClick={this.props.nextStep}
|
||||
onClick={nextStep}
|
||||
>
|
||||
<Trans>get_started</Trans>
|
||||
</button>
|
||||
@@ -43,9 +52,11 @@ class Controls extends Component {
|
||||
type="submit"
|
||||
className="btn btn-success btn-lg setup__button"
|
||||
disabled={
|
||||
this.props.invalid
|
||||
|| this.props.pristine
|
||||
|| this.props.install.processingSubmit
|
||||
invalid
|
||||
|| pristine
|
||||
|| install.processingSubmit
|
||||
|| install.dns.status
|
||||
|| install.web.status
|
||||
}
|
||||
>
|
||||
<Trans>next</Trans>
|
||||
@@ -56,7 +67,7 @@ class Controls extends Component {
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-success btn-lg setup__button"
|
||||
onClick={this.props.nextStep}
|
||||
onClick={nextStep}
|
||||
>
|
||||
<Trans>next</Trans>
|
||||
</button>
|
||||
@@ -66,7 +77,8 @@ class Controls extends Component {
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-success btn-lg setup__button"
|
||||
onClick={() => this.props.openDashboard(this.props.address)}
|
||||
onClick={() =>
|
||||
this.props.openDashboard(ip, port)}
|
||||
>
|
||||
<Trans>open_dashboard</Trans>
|
||||
</button>
|
||||
@@ -98,7 +110,8 @@ Controls.propTypes = {
|
||||
submitting: PropTypes.bool,
|
||||
invalid: PropTypes.bool,
|
||||
pristine: PropTypes.bool,
|
||||
address: PropTypes.string,
|
||||
ip: PropTypes.string,
|
||||
port: PropTypes.number,
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Field, reduxForm, formValueSelector } from 'redux-form';
|
||||
@@ -9,6 +9,7 @@ import Controls from './Controls';
|
||||
import AddressList from './AddressList';
|
||||
import renderField from './renderField';
|
||||
import { getInterfaceIp } from '../../helpers/helpers';
|
||||
import { ALL_INTERFACES_IP } from '../../helpers/constants';
|
||||
|
||||
const required = (value) => {
|
||||
if (value || value === 0) {
|
||||
@@ -29,10 +30,25 @@ const toNumber = value => value && parseInt(value, 10);
|
||||
const renderInterfaces = (interfaces => (
|
||||
Object.keys(interfaces).map((item) => {
|
||||
const option = interfaces[item];
|
||||
const { name } = option;
|
||||
const {
|
||||
name,
|
||||
ip_addresses,
|
||||
flags,
|
||||
} = option;
|
||||
|
||||
if (option.ip_addresses && option.ip_addresses.length > 0) {
|
||||
if (option && ip_addresses && ip_addresses.length > 0) {
|
||||
const ip = getInterfaceIp(option);
|
||||
const isDown = flags && flags.includes('down');
|
||||
|
||||
if (isDown) {
|
||||
return (
|
||||
<option value={ip} key={name} disabled>
|
||||
<Fragment>
|
||||
{name} - {ip} (<Trans>down</Trans>)
|
||||
</Fragment>
|
||||
</option>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<option value={ip} key={name}>
|
||||
@@ -45,141 +61,191 @@ const renderInterfaces = (interfaces => (
|
||||
})
|
||||
));
|
||||
|
||||
let Settings = (props) => {
|
||||
const {
|
||||
handleSubmit,
|
||||
webIp,
|
||||
webPort,
|
||||
dnsIp,
|
||||
dnsPort,
|
||||
interfaces,
|
||||
invalid,
|
||||
webWarning,
|
||||
dnsWarning,
|
||||
} = props;
|
||||
class Settings extends Component {
|
||||
componentDidMount() {
|
||||
const { web, dns } = this.props.config;
|
||||
|
||||
return (
|
||||
<form className="setup__step" onSubmit={handleSubmit}>
|
||||
<div className="setup__group">
|
||||
<div className="setup__subtitle">
|
||||
<Trans>install_settings_title</Trans>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-8">
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<Trans>install_settings_listen</Trans>
|
||||
</label>
|
||||
<Field
|
||||
name="web.ip"
|
||||
component="select"
|
||||
className="form-control custom-select"
|
||||
>
|
||||
<option value="0.0.0.0">
|
||||
<Trans>install_settings_all_interfaces</Trans>
|
||||
</option>
|
||||
{renderInterfaces(interfaces)}
|
||||
</Field>
|
||||
this.props.validateForm({
|
||||
web,
|
||||
dns,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
handleSubmit,
|
||||
handleChange,
|
||||
handleAutofix,
|
||||
webIp,
|
||||
webPort,
|
||||
dnsIp,
|
||||
dnsPort,
|
||||
interfaces,
|
||||
invalid,
|
||||
config,
|
||||
} = this.props;
|
||||
const {
|
||||
status: webStatus,
|
||||
can_autofix: isWebFixAvailable,
|
||||
} = config.web;
|
||||
const {
|
||||
status: dnsStatus,
|
||||
can_autofix: isDnsFixAvailable,
|
||||
} = config.dns;
|
||||
|
||||
return (
|
||||
<form className="setup__step" onSubmit={handleSubmit}>
|
||||
<div className="setup__group">
|
||||
<div className="setup__subtitle">
|
||||
<Trans>install_settings_title</Trans>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-8">
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<Trans>install_settings_listen</Trans>
|
||||
</label>
|
||||
<Field
|
||||
name="web.ip"
|
||||
component="select"
|
||||
className="form-control custom-select"
|
||||
onChange={handleChange}
|
||||
>
|
||||
<option value={ALL_INTERFACES_IP}>
|
||||
<Trans>install_settings_all_interfaces</Trans>
|
||||
</option>
|
||||
{renderInterfaces(interfaces)}
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-4">
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<Trans>install_settings_port</Trans>
|
||||
</label>
|
||||
<Field
|
||||
name="web.port"
|
||||
component={renderField}
|
||||
type="number"
|
||||
className="form-control"
|
||||
placeholder="80"
|
||||
validate={[port, required]}
|
||||
normalize={toNumber}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12">
|
||||
{webStatus &&
|
||||
<div className="setup__error text-danger">
|
||||
{webStatus}
|
||||
{isWebFixAvailable &&
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-sm ml-2"
|
||||
onClick={() => handleAutofix('web', webIp, webPort)}
|
||||
>
|
||||
<Trans>fix</Trans>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-4">
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<Trans>install_settings_port</Trans>
|
||||
</label>
|
||||
<Field
|
||||
name="web.port"
|
||||
component={renderField}
|
||||
type="number"
|
||||
className="form-control"
|
||||
placeholder="80"
|
||||
validate={[port, required]}
|
||||
normalize={toNumber}
|
||||
<div className="setup__desc">
|
||||
<Trans>install_settings_interface_link</Trans>
|
||||
<div className="mt-1">
|
||||
<AddressList
|
||||
interfaces={interfaces}
|
||||
address={webIp}
|
||||
port={webPort}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="setup__desc">
|
||||
<Trans>install_settings_interface_link</Trans>
|
||||
<div className="mt-1">
|
||||
<AddressList
|
||||
interfaces={interfaces}
|
||||
address={webIp}
|
||||
port={webPort}
|
||||
/>
|
||||
<div className="setup__group">
|
||||
<div className="setup__subtitle">
|
||||
<Trans>install_settings_dns</Trans>
|
||||
</div>
|
||||
{webWarning &&
|
||||
<div className="text-danger mt-2">
|
||||
{webWarning}
|
||||
<div className="row">
|
||||
<div className="col-8">
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<Trans>install_settings_listen</Trans>
|
||||
</label>
|
||||
<Field
|
||||
name="dns.ip"
|
||||
component="select"
|
||||
className="form-control custom-select"
|
||||
onChange={handleChange}
|
||||
>
|
||||
<option value={ALL_INTERFACES_IP}>
|
||||
<Trans>install_settings_all_interfaces</Trans>
|
||||
</option>
|
||||
{renderInterfaces(interfaces)}
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className="setup__group">
|
||||
<div className="setup__subtitle">
|
||||
<Trans>install_settings_dns</Trans>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-8">
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<Trans>install_settings_listen</Trans>
|
||||
</label>
|
||||
<Field
|
||||
name="dns.ip"
|
||||
component="select"
|
||||
className="form-control custom-select"
|
||||
>
|
||||
<option value="0.0.0.0">
|
||||
<Trans>install_settings_all_interfaces</Trans>
|
||||
</option>
|
||||
{renderInterfaces(interfaces)}
|
||||
</Field>
|
||||
<div className="col-4">
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<Trans>install_settings_port</Trans>
|
||||
</label>
|
||||
<Field
|
||||
name="dns.port"
|
||||
component={renderField}
|
||||
type="number"
|
||||
className="form-control"
|
||||
placeholder="80"
|
||||
validate={[port, required]}
|
||||
normalize={toNumber}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12">
|
||||
{dnsStatus &&
|
||||
<div className="setup__error text-danger">
|
||||
{dnsStatus}
|
||||
{isDnsFixAvailable &&
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-sm ml-2"
|
||||
onClick={() => handleAutofix('dns', dnsIp, dnsPort)}
|
||||
>
|
||||
<Trans>fix</Trans>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-4">
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<Trans>install_settings_port</Trans>
|
||||
</label>
|
||||
<Field
|
||||
name="dns.port"
|
||||
component={renderField}
|
||||
type="number"
|
||||
className="form-control"
|
||||
placeholder="80"
|
||||
validate={[port, required]}
|
||||
normalize={toNumber}
|
||||
<div className="setup__desc">
|
||||
<Trans>install_settings_dns_desc</Trans>
|
||||
<div className="mt-1">
|
||||
<AddressList
|
||||
interfaces={interfaces}
|
||||
address={dnsIp}
|
||||
port={dnsPort}
|
||||
isDns={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="setup__desc">
|
||||
<Trans>install_settings_dns_desc</Trans>
|
||||
<div className="mt-1">
|
||||
<AddressList
|
||||
interfaces={interfaces}
|
||||
address={dnsIp}
|
||||
port={dnsPort}
|
||||
isDns={true}
|
||||
/>
|
||||
</div>
|
||||
{dnsWarning &&
|
||||
<div className="text-danger mt-2">
|
||||
{dnsWarning}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<Controls invalid={invalid} />
|
||||
</form>
|
||||
);
|
||||
};
|
||||
<Controls invalid={invalid} />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Settings.propTypes = {
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
handleChange: PropTypes.func,
|
||||
handleAutofix: PropTypes.func,
|
||||
validateForm: PropTypes.func,
|
||||
webIp: PropTypes.string.isRequired,
|
||||
dnsIp: PropTypes.string.isRequired,
|
||||
config: PropTypes.object.isRequired,
|
||||
webPort: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
@@ -188,8 +254,6 @@ Settings.propTypes = {
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]),
|
||||
webWarning: PropTypes.string.isRequired,
|
||||
dnsWarning: PropTypes.string.isRequired,
|
||||
interfaces: PropTypes.object.isRequired,
|
||||
invalid: PropTypes.bool.isRequired,
|
||||
initialValues: PropTypes.object,
|
||||
@@ -197,7 +261,7 @@ Settings.propTypes = {
|
||||
|
||||
const selector = formValueSelector('install');
|
||||
|
||||
Settings = connect((state) => {
|
||||
const SettingsForm = connect((state) => {
|
||||
const webIp = selector(state, 'web.ip');
|
||||
const webPort = selector(state, 'web.port');
|
||||
const dnsIp = selector(state, 'dns.ip');
|
||||
@@ -218,4 +282,4 @@ export default flow([
|
||||
destroyOnUnmount: false,
|
||||
forceUnregisterOnUnmount: true,
|
||||
}),
|
||||
])(Settings);
|
||||
])(SettingsForm);
|
||||
|
||||
@@ -115,3 +115,7 @@
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.setup__error {
|
||||
margin: -5px 0 5px;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { Trans, withNamespaces } from 'react-i18next';
|
||||
import flow from 'lodash/flow';
|
||||
|
||||
import Controls from './Controls';
|
||||
import { getWebAddress } from '../../helpers/helpers';
|
||||
|
||||
let Submit = props => (
|
||||
<div className="setup__step">
|
||||
@@ -20,7 +19,8 @@ let Submit = props => (
|
||||
</div>
|
||||
<Controls
|
||||
openDashboard={props.openDashboard}
|
||||
address={getWebAddress(props.webIp, props.webPort)}
|
||||
ip={props.webIp}
|
||||
port={props.webPort}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
import * as actionCreators from '../../actions/install';
|
||||
import { INSTALL_FIRST_STEP, INSTALL_TOTAL_STEPS } from '../../helpers/constants';
|
||||
import { getWebAddress } from '../../helpers/helpers';
|
||||
import {
|
||||
INSTALL_FIRST_STEP,
|
||||
INSTALL_TOTAL_STEPS,
|
||||
ALL_INTERFACES_IP,
|
||||
DEBOUNCE_TIMEOUT,
|
||||
} from '../../helpers/constants';
|
||||
|
||||
import Loading from '../../components/ui/Loading';
|
||||
import Greeting from './Greeting';
|
||||
@@ -29,7 +36,37 @@ class Setup extends Component {
|
||||
this.props.setAllSettings(values);
|
||||
};
|
||||
|
||||
openDashboard = (address) => {
|
||||
handleFormChange = debounce((values) => {
|
||||
if (values && values.web.port && values.dns.port) {
|
||||
this.props.checkConfig(values);
|
||||
}
|
||||
}, DEBOUNCE_TIMEOUT);
|
||||
|
||||
handleAutofix = (type, ip, port) => {
|
||||
const data = {
|
||||
ip,
|
||||
port,
|
||||
autofix: true,
|
||||
};
|
||||
|
||||
if (type === 'web') {
|
||||
this.props.checkConfig({
|
||||
web: { ...data },
|
||||
});
|
||||
} else {
|
||||
this.props.checkConfig({
|
||||
dns: { ...data },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
openDashboard = (ip, port) => {
|
||||
let address = getWebAddress(ip, port);
|
||||
|
||||
if (ip === ALL_INTERFACES_IP) {
|
||||
address = getWebAddress(window.location.hostname, port);
|
||||
}
|
||||
|
||||
window.location.replace(address);
|
||||
}
|
||||
|
||||
@@ -52,11 +89,13 @@ class Setup extends Component {
|
||||
case 2:
|
||||
return (
|
||||
<Settings
|
||||
config={config}
|
||||
initialValues={config}
|
||||
interfaces={interfaces}
|
||||
webWarning={config.web.warning}
|
||||
dnsWarning={config.dns.warning}
|
||||
onSubmit={this.nextStep}
|
||||
onChange={this.handleFormChange}
|
||||
validateForm={this.handleFormChange}
|
||||
handleAutofix={this.handleAutofix}
|
||||
/>
|
||||
);
|
||||
case 3:
|
||||
@@ -105,6 +144,7 @@ class Setup extends Component {
|
||||
Setup.propTypes = {
|
||||
getDefaultAddresses: PropTypes.func.isRequired,
|
||||
setAllSettings: PropTypes.func.isRequired,
|
||||
checkConfig: PropTypes.func.isRequired,
|
||||
nextStep: PropTypes.func.isRequired,
|
||||
prevStep: PropTypes.func.isRequired,
|
||||
install: PropTypes.object.isRequired,
|
||||
|
||||
@@ -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,
|
||||
@@ -163,6 +167,17 @@ const dashboard = handleActions({
|
||||
const newState = { ...state, language: payload };
|
||||
return newState;
|
||||
},
|
||||
|
||||
[actions.getClientsRequest]: state => ({ ...state, processingClients: true }),
|
||||
[actions.getClientsFailure]: state => ({ ...state, processingClients: false }),
|
||||
[actions.getClientsSuccess]: (state, { payload }) => {
|
||||
const newState = {
|
||||
...state,
|
||||
clients: payload,
|
||||
processingClients: false,
|
||||
};
|
||||
return newState;
|
||||
},
|
||||
}, {
|
||||
processing: true,
|
||||
isCoreRunning: false,
|
||||
@@ -171,10 +186,17 @@ const dashboard = handleActions({
|
||||
logStatusProcessing: false,
|
||||
processingVersion: true,
|
||||
processingFiltering: true,
|
||||
upstreamDns: [],
|
||||
processingClients: true,
|
||||
upstreamDns: '',
|
||||
bootstrapDns: '',
|
||||
allServers: false,
|
||||
protectionEnabled: false,
|
||||
processingProtection: false,
|
||||
httpPort: 80,
|
||||
dnsPort: 53,
|
||||
dnsAddresses: [],
|
||||
dnsVersion: '',
|
||||
clients: [],
|
||||
});
|
||||
|
||||
const queryLogs = handleActions({
|
||||
@@ -270,18 +292,31 @@ const dhcp = handleActions({
|
||||
|
||||
[actions.findActiveDhcpRequest]: state => ({ ...state, processingStatus: true }),
|
||||
[actions.findActiveDhcpFailure]: state => ({ ...state, processingStatus: false }),
|
||||
[actions.findActiveDhcpSuccess]: (state, { payload }) => ({
|
||||
...state,
|
||||
active: payload,
|
||||
processingStatus: false,
|
||||
}),
|
||||
[actions.findActiveDhcpSuccess]: (state, { payload }) => {
|
||||
const {
|
||||
other_server: otherServer,
|
||||
static_ip: staticIP,
|
||||
} = payload;
|
||||
|
||||
const newState = {
|
||||
...state,
|
||||
check: {
|
||||
otherServer,
|
||||
staticIP,
|
||||
},
|
||||
processingStatus: false,
|
||||
};
|
||||
return newState;
|
||||
},
|
||||
|
||||
[actions.toggleDhcpRequest]: state => ({ ...state, processingDhcp: true }),
|
||||
[actions.toggleDhcpFailure]: state => ({ ...state, processingDhcp: false }),
|
||||
[actions.toggleDhcpSuccess]: (state) => {
|
||||
const { config } = state;
|
||||
const newConfig = { ...config, enabled: !config.enabled };
|
||||
const newState = { ...state, config: newConfig, processingDhcp: false };
|
||||
const newState = {
|
||||
...state, config: newConfig, check: null, processingDhcp: false,
|
||||
};
|
||||
return newState;
|
||||
},
|
||||
|
||||
@@ -302,7 +337,7 @@ const dhcp = handleActions({
|
||||
config: {
|
||||
enabled: false,
|
||||
},
|
||||
active: null,
|
||||
check: null,
|
||||
leases: [],
|
||||
});
|
||||
|
||||
|
||||
@@ -10,10 +10,13 @@ const install = handleActions({
|
||||
[actions.getDefaultAddressesRequest]: state => ({ ...state, processingDefault: true }),
|
||||
[actions.getDefaultAddressesFailure]: state => ({ ...state, processingDefault: false }),
|
||||
[actions.getDefaultAddressesSuccess]: (state, { payload }) => {
|
||||
const values = payload;
|
||||
values.web.ip = state.web.ip;
|
||||
values.dns.ip = state.dns.ip;
|
||||
const newState = { ...state, ...values, processingDefault: false };
|
||||
const { interfaces } = payload;
|
||||
const web = { ...state.web, port: payload.web_port };
|
||||
const dns = { ...state.dns, port: payload.dns_port };
|
||||
|
||||
const newState = {
|
||||
...state, web, dns, interfaces, processingDefault: false,
|
||||
};
|
||||
return newState;
|
||||
},
|
||||
|
||||
@@ -23,19 +26,34 @@ const install = handleActions({
|
||||
[actions.setAllSettingsRequest]: state => ({ ...state, processingSubmit: true }),
|
||||
[actions.setAllSettingsFailure]: state => ({ ...state, processingSubmit: false }),
|
||||
[actions.setAllSettingsSuccess]: state => ({ ...state, processingSubmit: false }),
|
||||
|
||||
[actions.checkConfigRequest]: state => ({ ...state, processingCheck: true }),
|
||||
[actions.checkConfigFailure]: state => ({ ...state, processingCheck: false }),
|
||||
[actions.checkConfigSuccess]: (state, { payload }) => {
|
||||
const web = { ...state.web, ...payload.web };
|
||||
const dns = { ...state.dns, ...payload.dns };
|
||||
|
||||
const newState = {
|
||||
...state, web, dns, processingCheck: false,
|
||||
};
|
||||
return newState;
|
||||
},
|
||||
}, {
|
||||
step: INSTALL_FIRST_STEP,
|
||||
processingDefault: true,
|
||||
processingSubmit: false,
|
||||
processingCheck: false,
|
||||
web: {
|
||||
ip: '0.0.0.0',
|
||||
port: 80,
|
||||
warning: '',
|
||||
status: '',
|
||||
can_autofix: false,
|
||||
},
|
||||
dns: {
|
||||
ip: '0.0.0.0',
|
||||
port: 53,
|
||||
warning: '',
|
||||
status: '',
|
||||
can_autofix: false,
|
||||
},
|
||||
interfaces: {},
|
||||
});
|
||||
|
||||
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 },
|
||||
]),
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
92
clients.go
Normal file
92
clients.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// Client information
|
||||
type Client struct {
|
||||
IP string
|
||||
Name string
|
||||
//Source source // Hosts file / User settings / DHCP
|
||||
}
|
||||
|
||||
type clientJSON struct {
|
||||
IP string `json:"ip"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
var clients []Client
|
||||
var clientsFilled bool
|
||||
|
||||
// Parse system 'hosts' file and fill clients array
|
||||
func fillClientInfo() {
|
||||
hostsFn := "/etc/hosts"
|
||||
if runtime.GOOS == "windows" {
|
||||
hostsFn = os.ExpandEnv("$SystemRoot\\system32\\drivers\\etc\\hosts")
|
||||
}
|
||||
|
||||
d, e := ioutil.ReadFile(hostsFn)
|
||||
if e != nil {
|
||||
log.Info("Can't read file %s: %v", hostsFn, e)
|
||||
return
|
||||
}
|
||||
|
||||
lines := strings.Split(string(d), "\n")
|
||||
for _, ln := range lines {
|
||||
ln = strings.TrimSpace(ln)
|
||||
if len(ln) == 0 || ln[0] == '#' {
|
||||
continue
|
||||
}
|
||||
|
||||
fields := strings.Fields(ln)
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
var c Client
|
||||
c.IP = fields[0]
|
||||
c.Name = fields[1]
|
||||
clients = append(clients, c)
|
||||
log.Tracef("%s -> %s", c.IP, c.Name)
|
||||
}
|
||||
|
||||
log.Info("Added %d client aliases from %s", len(clients), hostsFn)
|
||||
clientsFilled = true
|
||||
}
|
||||
|
||||
// respond with information about configured clients
|
||||
func handleGetClients(w http.ResponseWriter, r *http.Request) {
|
||||
log.Tracef("%s %v", r.Method, r.URL)
|
||||
|
||||
if !clientsFilled {
|
||||
fillClientInfo()
|
||||
}
|
||||
|
||||
data := []clientJSON{}
|
||||
for _, c := range clients {
|
||||
cj := clientJSON{
|
||||
IP: c.IP,
|
||||
Name: c.Name,
|
||||
}
|
||||
data = append(data, cj)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
e := json.NewEncoder(w).Encode(data)
|
||||
if e != nil {
|
||||
httpError(w, http.StatusInternalServerError, "Failed to encode to json: %v", e)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterClientsHandlers registers HTTP handlers
|
||||
func RegisterClientsHandlers() {
|
||||
http.HandleFunc("/control/clients", postInstall(optionalAuth(ensureGET(handleGetClients))))
|
||||
}
|
||||
74
config.go
74
config.go
@@ -4,13 +4,15 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -32,11 +34,13 @@ type configuration struct {
|
||||
ourWorkingDir string // Location of our directory, used to protect against CWD being somewhere else
|
||||
firstRun bool // if set to true, don't run any services except HTTP web inteface, and serve only first-run html
|
||||
|
||||
BindHost string `yaml:"bind_host"` // BindHost is the IP address of the HTTP server to bind to
|
||||
BindPort int `yaml:"bind_port"` // BindPort is the port the HTTP server
|
||||
AuthName string `yaml:"auth_name"` // AuthName is the basic auth username
|
||||
AuthPass string `yaml:"auth_pass"` // AuthPass is the basic auth password
|
||||
Language string `yaml:"language"` // two-letter ISO 639-1 language code
|
||||
BindHost string `yaml:"bind_host"` // BindHost is the IP address of the HTTP server to bind to
|
||||
BindPort int `yaml:"bind_port"` // BindPort is the port the HTTP server
|
||||
AuthName string `yaml:"auth_name"` // AuthName is the basic auth username
|
||||
AuthPass string `yaml:"auth_pass"` // AuthPass is the basic auth password
|
||||
Language string `yaml:"language"` // two-letter ISO 639-1 language code
|
||||
RlimitNoFile uint `yaml:"rlimit_nofile"` // Maximum number of opened fd's per process (0: default)
|
||||
|
||||
DNS dnsConfig `yaml:"dns"`
|
||||
TLS tlsConfig `yaml:"tls"`
|
||||
Filters []filter `yaml:"filters"`
|
||||
@@ -60,7 +64,8 @@ type dnsConfig struct {
|
||||
UpstreamDNS []string `yaml:"upstream_dns"`
|
||||
}
|
||||
|
||||
var defaultDNS = []string{"tls://1.1.1.1", "tls://1.0.0.1"}
|
||||
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
|
||||
@@ -87,7 +92,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
|
||||
@@ -114,7 +119,8 @@ var config = configuration{
|
||||
QueryLogEnabled: true,
|
||||
Ratelimit: 20,
|
||||
RefuseAny: true,
|
||||
BootstrapDNS: "8.8.8.8:53",
|
||||
BootstrapDNS: defaultBootstrap,
|
||||
AllServers: false,
|
||||
},
|
||||
UpstreamDNS: defaultDNS,
|
||||
},
|
||||
@@ -128,16 +134,36 @@ 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"},
|
||||
},
|
||||
DHCP: dhcpd.ServerConfig{
|
||||
LeaseDuration: 86400,
|
||||
ICMPTimeout: 1000,
|
||||
},
|
||||
SchemaVersion: currentSchemaVersion,
|
||||
}
|
||||
|
||||
// init initializes default configuration for the current OS&ARCH
|
||||
func init() {
|
||||
if runtime.GOARCH == "mips" || runtime.GOARCH == "mipsle" {
|
||||
// Use plain DNS on MIPS, encryption is too slow
|
||||
defaultDNS = []string{"1.1.1.1", "1.0.0.1"}
|
||||
// also change the default config
|
||||
config.DNS.UpstreamDNS = defaultDNS
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -152,7 +178,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
|
||||
}
|
||||
@@ -160,19 +186,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
|
||||
}
|
||||
|
||||
@@ -199,19 +225,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
|
||||
}
|
||||
|
||||
@@ -221,14 +247,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
|
||||
}
|
||||
|
||||
|
||||
921
control.go
921
control.go
File diff suppressed because it is too large
Load Diff
278
control_install.go
Normal file
278
control_install.go
Normal file
@@ -0,0 +1,278 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
type firstRunData struct {
|
||||
WebPort int `json:"web_port"`
|
||||
DNSPort int `json:"dns_port"`
|
||||
Interfaces map[string]interface{} `json:"interfaces"`
|
||||
}
|
||||
|
||||
// Get initial installation settings
|
||||
func handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) {
|
||||
log.Tracef("%s %v", r.Method, r.URL)
|
||||
data := firstRunData{}
|
||||
data.WebPort = 80
|
||||
data.DNSPort = 53
|
||||
|
||||
ifaces, err := getValidNetInterfacesForWeb()
|
||||
if err != nil {
|
||||
httpError(w, http.StatusInternalServerError, "Couldn't get interfaces: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
data.Interfaces = make(map[string]interface{})
|
||||
for _, iface := range ifaces {
|
||||
data.Interfaces[iface.Name] = iface
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
err = json.NewEncoder(w).Encode(data)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusInternalServerError, "Unable to marshal default addresses to json: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type checkConfigReqEnt struct {
|
||||
Port int `json:"port"`
|
||||
IP string `json:"ip"`
|
||||
Autofix bool `json:"autofix"`
|
||||
}
|
||||
type checkConfigReq struct {
|
||||
Web checkConfigReqEnt `json:"web"`
|
||||
DNS checkConfigReqEnt `json:"dns"`
|
||||
}
|
||||
|
||||
type checkConfigRespEnt struct {
|
||||
Status string `json:"status"`
|
||||
CanAutofix bool `json:"can_autofix"`
|
||||
}
|
||||
type checkConfigResp struct {
|
||||
Web checkConfigRespEnt `json:"web"`
|
||||
DNS checkConfigRespEnt `json:"dns"`
|
||||
}
|
||||
|
||||
// Check if ports are available, respond with results
|
||||
func handleInstallCheckConfig(w http.ResponseWriter, r *http.Request) {
|
||||
log.Tracef("%s %v", r.Method, r.URL)
|
||||
reqData := checkConfigReq{}
|
||||
respData := checkConfigResp{}
|
||||
err := json.NewDecoder(r.Body).Decode(&reqData)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "Failed to parse 'check_config' JSON data: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if reqData.Web.Port != 0 && reqData.Web.Port != config.BindPort {
|
||||
err = checkPortAvailable(reqData.Web.IP, reqData.Web.Port)
|
||||
if err != nil {
|
||||
respData.Web.Status = fmt.Sprintf("%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if reqData.DNS.Port != 0 {
|
||||
err = checkPacketPortAvailable(reqData.DNS.IP, reqData.DNS.Port)
|
||||
|
||||
if errorIsAddrInUse(err) {
|
||||
canAutofix := checkDNSStubListener()
|
||||
if canAutofix && reqData.DNS.Autofix {
|
||||
|
||||
err = disableDNSStubListener()
|
||||
if err != nil {
|
||||
log.Error("Couldn't disable DNSStubListener: %s", err)
|
||||
}
|
||||
|
||||
err = checkPacketPortAvailable(reqData.DNS.IP, reqData.DNS.Port)
|
||||
canAutofix = false
|
||||
}
|
||||
|
||||
respData.DNS.CanAutofix = canAutofix
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
err = checkPortAvailable(reqData.DNS.IP, reqData.DNS.Port)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
respData.DNS.Status = fmt.Sprintf("%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
err = json.NewEncoder(w).Encode(respData)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusInternalServerError, "Unable to marshal JSON: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check if DNSStubListener is active
|
||||
func checkDNSStubListener() bool {
|
||||
cmd := exec.Command("systemctl", "is-enabled", "systemd-resolved")
|
||||
log.Tracef("executing %s %v", cmd.Path, cmd.Args)
|
||||
_, err := cmd.Output()
|
||||
if err != nil || cmd.ProcessState.ExitCode() != 0 {
|
||||
log.Error("command %s has failed: %v code:%d",
|
||||
cmd.Path, err, cmd.ProcessState.ExitCode())
|
||||
return false
|
||||
}
|
||||
|
||||
cmd = exec.Command("grep", "-E", "#?DNSStubListener=yes", "/etc/systemd/resolved.conf")
|
||||
log.Tracef("executing %s %v", cmd.Path, cmd.Args)
|
||||
_, err = cmd.Output()
|
||||
if err != nil || cmd.ProcessState.ExitCode() != 0 {
|
||||
log.Error("command %s has failed: %v code:%d",
|
||||
cmd.Path, err, cmd.ProcessState.ExitCode())
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Deactivate DNSStubListener
|
||||
func disableDNSStubListener() error {
|
||||
cmd := exec.Command("sed", "-r", "-i.orig", "s/#?DNSStubListener=yes/DNSStubListener=no/g", "/etc/systemd/resolved.conf")
|
||||
log.Tracef("executing %s %v", cmd.Path, cmd.Args)
|
||||
_, err := cmd.Output()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cmd.ProcessState.ExitCode() != 0 {
|
||||
return fmt.Errorf("process %s exited with an error: %d",
|
||||
cmd.Path, cmd.ProcessState.ExitCode())
|
||||
}
|
||||
|
||||
cmd = exec.Command("systemctl", "reload-or-restart", "systemd-resolved")
|
||||
log.Tracef("executing %s %v", cmd.Path, cmd.Args)
|
||||
_, err = cmd.Output()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cmd.ProcessState.ExitCode() != 0 {
|
||||
return fmt.Errorf("process %s exited with an error: %d",
|
||||
cmd.Path, cmd.ProcessState.ExitCode())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type applyConfigReqEnt struct {
|
||||
IP string `json:"ip"`
|
||||
Port int `json:"port"`
|
||||
}
|
||||
type applyConfigReq struct {
|
||||
Web applyConfigReqEnt `json:"web"`
|
||||
DNS applyConfigReqEnt `json:"dns"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// Copy installation parameters between two configuration objects
|
||||
func copyInstallSettings(dst *configuration, src *configuration) {
|
||||
dst.BindHost = src.BindHost
|
||||
dst.BindPort = src.BindPort
|
||||
dst.DNS.BindHost = src.DNS.BindHost
|
||||
dst.DNS.Port = src.DNS.Port
|
||||
dst.AuthName = src.AuthName
|
||||
dst.AuthPass = src.AuthPass
|
||||
}
|
||||
|
||||
// Apply new configuration, start DNS server, restart Web server
|
||||
func handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
|
||||
log.Tracef("%s %v", r.Method, r.URL)
|
||||
newSettings := applyConfigReq{}
|
||||
err := json.NewDecoder(r.Body).Decode(&newSettings)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "Failed to parse 'configure' JSON: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if newSettings.Web.Port == 0 || newSettings.DNS.Port == 0 {
|
||||
httpError(w, http.StatusBadRequest, "port value can't be 0")
|
||||
return
|
||||
}
|
||||
|
||||
restartHTTP := true
|
||||
if config.BindHost == newSettings.Web.IP && config.BindPort == newSettings.Web.Port {
|
||||
// no need to rebind
|
||||
restartHTTP = false
|
||||
}
|
||||
|
||||
// validate that hosts and ports are bindable
|
||||
if restartHTTP {
|
||||
err = checkPortAvailable(newSettings.Web.IP, newSettings.Web.Port)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "Impossible to listen on IP:port %s due to %s",
|
||||
net.JoinHostPort(newSettings.Web.IP, strconv.Itoa(newSettings.Web.Port)), err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = checkPacketPortAvailable(newSettings.DNS.IP, newSettings.DNS.Port)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "%s", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = checkPortAvailable(newSettings.DNS.IP, newSettings.DNS.Port)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "%s", err)
|
||||
return
|
||||
}
|
||||
|
||||
var curConfig configuration
|
||||
copyInstallSettings(&curConfig, &config)
|
||||
|
||||
config.firstRun = false
|
||||
config.BindHost = newSettings.Web.IP
|
||||
config.BindPort = newSettings.Web.Port
|
||||
config.DNS.BindHost = newSettings.DNS.IP
|
||||
config.DNS.Port = newSettings.DNS.Port
|
||||
config.AuthName = newSettings.Username
|
||||
config.AuthPass = newSettings.Password
|
||||
|
||||
err = startDNSServer()
|
||||
if err != nil {
|
||||
config.firstRun = true
|
||||
copyInstallSettings(&config, &curConfig)
|
||||
httpError(w, http.StatusInternalServerError, "Couldn't start DNS server: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = config.write()
|
||||
if err != nil {
|
||||
config.firstRun = true
|
||||
copyInstallSettings(&config, &curConfig)
|
||||
httpError(w, http.StatusInternalServerError, "Couldn't write config: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
go refreshFiltersIfNecessary(false)
|
||||
|
||||
// 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 restartHTTP {
|
||||
go func() {
|
||||
httpServer.Shutdown(context.TODO())
|
||||
}()
|
||||
}
|
||||
|
||||
returnOK(w)
|
||||
}
|
||||
|
||||
func registerInstallHandlers() {
|
||||
http.HandleFunc("/control/install/get_addresses", preInstall(ensureGET(handleInstallGetAddresses)))
|
||||
http.HandleFunc("/control/install/check_config", preInstall(ensurePOST(handleInstallCheckConfig)))
|
||||
http.HandleFunc("/control/install/configure", preInstall(ensurePOST(handleInstallConfigure)))
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
205
dhcp.go
205
dhcp.go
@@ -2,21 +2,26 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/dhcpd"
|
||||
"github.com/hmage/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/file"
|
||||
"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 +48,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,24 +56,35 @@ 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)
|
||||
|
||||
staticIP, err := hasStaticIP(newconfig.InterfaceName)
|
||||
if !staticIP && err == nil {
|
||||
err = setStaticIP(newconfig.InterfaceName)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusInternalServerError, "Failed to configure static IP: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = dhcpServer.Start(&newconfig)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "Failed to start DHCP server: %s", err)
|
||||
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()
|
||||
@@ -127,11 +144,16 @@ func handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// Perform the following tasks:
|
||||
// . Search for another DHCP server running
|
||||
// . Check if a static IP is configured for the network interface
|
||||
// Respond with results
|
||||
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,17 +161,39 @@ 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
|
||||
}
|
||||
|
||||
found, err := dhcpd.CheckIfOtherDHCPServersPresent(interfaceName)
|
||||
result := map[string]interface{}{}
|
||||
if err != nil {
|
||||
result["error"] = err.Error()
|
||||
} else {
|
||||
result["found"] = found
|
||||
|
||||
othSrv := map[string]interface{}{}
|
||||
foundVal := "no"
|
||||
if found {
|
||||
foundVal = "yes"
|
||||
} else if err != nil {
|
||||
foundVal = "error"
|
||||
othSrv["error"] = err.Error()
|
||||
}
|
||||
othSrv["found"] = foundVal
|
||||
|
||||
staticIP := map[string]interface{}{}
|
||||
isStaticIP, err := hasStaticIP(interfaceName)
|
||||
staticIPStatus := "yes"
|
||||
if err != nil {
|
||||
staticIPStatus = "error"
|
||||
staticIP["error"] = err.Error()
|
||||
} else if !isStaticIP {
|
||||
staticIPStatus = "no"
|
||||
staticIP["ip"] = getFullIP(interfaceName)
|
||||
}
|
||||
staticIP["static"] = staticIPStatus
|
||||
|
||||
result := map[string]interface{}{}
|
||||
result["other_server"] = othSrv
|
||||
result["static_ip"] = staticIP
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
err = json.NewEncoder(w).Encode(result)
|
||||
if err != nil {
|
||||
@@ -158,6 +202,137 @@ func handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if network interface has a static IP configured
|
||||
func hasStaticIP(ifaceName string) (bool, error) {
|
||||
if runtime.GOOS == "windows" {
|
||||
return false, errors.New("Can't detect static IP: not supported on Windows")
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadFile("/etc/dhcpcd.conf")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
lines := strings.Split(string(body), "\n")
|
||||
nameLine := fmt.Sprintf("interface %s", ifaceName)
|
||||
withinInterfaceCtx := false
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
if withinInterfaceCtx && len(line) == 0 {
|
||||
// an empty line resets our state
|
||||
withinInterfaceCtx = false
|
||||
}
|
||||
|
||||
if len(line) == 0 || line[0] == '#' {
|
||||
continue
|
||||
}
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
if !withinInterfaceCtx {
|
||||
if line == nameLine {
|
||||
// we found our interface
|
||||
withinInterfaceCtx = true
|
||||
}
|
||||
|
||||
} else {
|
||||
if strings.HasPrefix(line, "interface ") {
|
||||
// we found another interface - reset our state
|
||||
withinInterfaceCtx = false
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(line, "static ip_address=") {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Get IP address with netmask
|
||||
func getFullIP(ifaceName string) string {
|
||||
cmd := exec.Command("ip", "-oneline", "-family", "inet", "address", "show", ifaceName)
|
||||
log.Tracef("executing %s %v", cmd.Path, cmd.Args)
|
||||
d, err := cmd.Output()
|
||||
if err != nil || cmd.ProcessState.ExitCode() != 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
fields := strings.Fields(string(d))
|
||||
if len(fields) < 4 {
|
||||
return ""
|
||||
}
|
||||
_, _, err = net.ParseCIDR(fields[3])
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return fields[3]
|
||||
}
|
||||
|
||||
// Get gateway IP address
|
||||
func getGatewayIP(ifaceName string) string {
|
||||
cmd := exec.Command("ip", "route", "show", "dev", ifaceName)
|
||||
log.Tracef("executing %s %v", cmd.Path, cmd.Args)
|
||||
d, err := cmd.Output()
|
||||
if err != nil || cmd.ProcessState.ExitCode() != 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
fields := strings.Fields(string(d))
|
||||
if len(fields) < 3 || fields[0] != "default" {
|
||||
return ""
|
||||
}
|
||||
|
||||
ip := net.ParseIP(fields[2])
|
||||
if ip == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return fields[2]
|
||||
}
|
||||
|
||||
// Set a static IP for network interface
|
||||
func setStaticIP(ifaceName string) error {
|
||||
ip := getFullIP(ifaceName)
|
||||
if len(ip) == 0 {
|
||||
return errors.New("Can't get IP address")
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadFile("/etc/dhcpcd.conf")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ip4, _, err := net.ParseCIDR(ip)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
add := fmt.Sprintf("\ninterface %s\nstatic ip_address=%s\n",
|
||||
ifaceName, ip)
|
||||
body = append(body, []byte(add)...)
|
||||
|
||||
gatewayIP := getGatewayIP(ifaceName)
|
||||
if len(gatewayIP) != 0 {
|
||||
add = fmt.Sprintf("static routers=%s\n",
|
||||
gatewayIP)
|
||||
body = append(body, []byte(add)...)
|
||||
}
|
||||
|
||||
add = fmt.Sprintf("static domain_name_servers=%s\n\n",
|
||||
ip4)
|
||||
body = append(body, []byte(add)...)
|
||||
|
||||
err = file.SafeWrite("/etc/dhcpcd.conf", body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func startDHCPServer() error {
|
||||
if !config.DHCP.Enabled {
|
||||
// not enabled, don't do anything
|
||||
|
||||
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
|
||||
@@ -1,6 +1,7 @@
|
||||
package dhcpd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
@@ -9,8 +10,9 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/hmage/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/krolaw/dhcp4"
|
||||
"golang.org/x/net/ipv4"
|
||||
)
|
||||
|
||||
// CheckIfOtherDHCPServersPresent sends a DHCP request to the specified network interface,
|
||||
@@ -32,13 +34,13 @@ func CheckIfOtherDHCPServersPresent(ifaceName string) (bool, error) {
|
||||
dst := "255.255.255.255:67"
|
||||
|
||||
// form a DHCP request packet, try to emulate existing client as much as possible
|
||||
xID := make([]byte, 8)
|
||||
xID := make([]byte, 4)
|
||||
n, err := rand.Read(xID)
|
||||
if n != 8 && err == nil {
|
||||
err = fmt.Errorf("Generated less than 8 bytes")
|
||||
if n != 4 && err == nil {
|
||||
err = fmt.Errorf("Generated less than 4 bytes")
|
||||
}
|
||||
if err != nil {
|
||||
return false, wrapErrPrint(err, "Couldn't generate 8 random bytes")
|
||||
return false, wrapErrPrint(err, "Couldn't generate random bytes")
|
||||
}
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
@@ -89,58 +91,63 @@ func CheckIfOtherDHCPServersPresent(ifaceName string) (bool, error) {
|
||||
|
||||
// bind to 0.0.0.0:68
|
||||
log.Tracef("Listening to udp4 %+v", udpAddr)
|
||||
c, err := net.ListenPacket("udp4", src)
|
||||
c, err := newBroadcastPacketConn(net.IPv4(0, 0, 0, 0), 68, ifaceName)
|
||||
if c != nil {
|
||||
defer c.Close()
|
||||
}
|
||||
// spew.Dump(c, err)
|
||||
// spew.Printf("net.ListenUDP returned %v, %v\n", c, err)
|
||||
if err != nil {
|
||||
return false, wrapErrPrint(err, "Couldn't listen to %s", src)
|
||||
return false, wrapErrPrint(err, "Couldn't listen on :68")
|
||||
}
|
||||
|
||||
// send to 255.255.255.255:67
|
||||
_, err = c.WriteTo(packet, dstAddr)
|
||||
cm := ipv4.ControlMessage{}
|
||||
_, err = c.WriteTo(packet, &cm, dstAddr)
|
||||
// spew.Dump(n, err)
|
||||
if err != nil {
|
||||
return false, wrapErrPrint(err, "Couldn't send a packet to %s", dst)
|
||||
}
|
||||
|
||||
// wait for answer
|
||||
log.Tracef("Waiting %v for an answer", defaultDiscoverTime)
|
||||
// TODO: replicate dhclient's behaviour of retrying several times with progressively bigger timeouts
|
||||
b := make([]byte, 1500)
|
||||
c.SetReadDeadline(time.Now().Add(defaultDiscoverTime))
|
||||
n, _, err = c.ReadFrom(b)
|
||||
if isTimeout(err) {
|
||||
// timed out -- no DHCP servers
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, wrapErrPrint(err, "Couldn't receive packet")
|
||||
}
|
||||
if n > 0 {
|
||||
b = b[:n]
|
||||
}
|
||||
// spew.Dump(n, fromAddr, err, b)
|
||||
for {
|
||||
// wait for answer
|
||||
log.Tracef("Waiting %v for an answer", defaultDiscoverTime)
|
||||
// TODO: replicate dhclient's behaviour of retrying several times with progressively bigger timeouts
|
||||
b := make([]byte, 1500)
|
||||
_ = c.SetReadDeadline(time.Now().Add(defaultDiscoverTime))
|
||||
n, _, _, err = c.ReadFrom(b)
|
||||
if isTimeout(err) {
|
||||
// timed out -- no DHCP servers
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, wrapErrPrint(err, "Couldn't receive packet")
|
||||
}
|
||||
// spew.Dump(n, fromAddr, err, b)
|
||||
|
||||
if n < 240 {
|
||||
// packet too small for dhcp
|
||||
return false, wrapErrPrint(err, "got packet that's too small for DHCP")
|
||||
}
|
||||
log.Tracef("Received packet (%v bytes)", n)
|
||||
|
||||
response := dhcp4.Packet(b[:n])
|
||||
if response.HLen() > 16 {
|
||||
// invalid size
|
||||
return false, wrapErrPrint(err, "got malformed packet with HLen() > 16")
|
||||
}
|
||||
if n < 240 {
|
||||
// packet too small for dhcp
|
||||
continue
|
||||
}
|
||||
|
||||
parsedOptions := response.ParseOptions()
|
||||
_, ok := parsedOptions[dhcp4.OptionDHCPMessageType]
|
||||
if !ok {
|
||||
return false, wrapErrPrint(err, "got malformed packet without DHCP message type")
|
||||
}
|
||||
response := dhcp4.Packet(b[:n])
|
||||
if response.OpCode() != dhcp4.BootReply ||
|
||||
response.HType() != 1 /*Ethernet*/ ||
|
||||
response.HLen() > 16 ||
|
||||
!bytes.Equal(response.CHAddr(), iface.HardwareAddr) ||
|
||||
!bytes.Equal(response.XId(), xID) {
|
||||
continue
|
||||
}
|
||||
|
||||
// that's a DHCP server there
|
||||
return true, nil
|
||||
parsedOptions := response.ParseOptions()
|
||||
if t := parsedOptions[dhcp4.OptionDHCPMessageType]; len(t) != 1 {
|
||||
continue //packet without DHCP message type
|
||||
}
|
||||
|
||||
log.Tracef("The packet is from an active DHCP server")
|
||||
// that's a DHCP server there
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
|
||||
45
dhcpd/os_linux.go
Normal file
45
dhcpd/os_linux.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package dhcpd
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/net/ipv4"
|
||||
)
|
||||
|
||||
// Create a socket for receiving broadcast packets
|
||||
func newBroadcastPacketConn(bindAddr net.IP, port int, ifname string) (*ipv4.PacketConn, error) {
|
||||
s, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, syscall.IPPROTO_UDP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := syscall.SetsockoptInt(s, syscall.SOL_SOCKET, syscall.SO_BROADCAST, 1); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := syscall.SetsockoptInt(s, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := syscall.SetsockoptString(s, syscall.SOL_SOCKET, syscall.SO_BINDTODEVICE, ifname); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addr := syscall.SockaddrInet4{Port: port}
|
||||
copy(addr.Addr[:], bindAddr.To4())
|
||||
err = syscall.Bind(s, &addr)
|
||||
if err != nil {
|
||||
syscall.Close(s)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f := os.NewFile(uintptr(s), "")
|
||||
c, err := net.FilePacketConn(f)
|
||||
f.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p := ipv4.NewPacketConn(c)
|
||||
return p, nil
|
||||
}
|
||||
44
dhcpd/os_unix.go
Normal file
44
dhcpd/os_unix.go
Normal file
@@ -0,0 +1,44 @@
|
||||
// +build aix darwin dragonfly freebsd netbsd openbsd solaris
|
||||
|
||||
package dhcpd
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/net/ipv4"
|
||||
)
|
||||
|
||||
// Create a socket for receiving broadcast packets
|
||||
func newBroadcastPacketConn(bindAddr net.IP, port int, ifname string) (*ipv4.PacketConn, error) {
|
||||
s, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, syscall.IPPROTO_UDP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := syscall.SetsockoptInt(s, syscall.SOL_SOCKET, syscall.SO_BROADCAST, 1); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := syscall.SetsockoptInt(s, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addr := syscall.SockaddrInet4{Port: port}
|
||||
copy(addr.Addr[:], bindAddr.To4())
|
||||
err = syscall.Bind(s, &addr)
|
||||
if err != nil {
|
||||
syscall.Close(s)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f := os.NewFile(uintptr(s), "")
|
||||
c, err := net.FilePacketConn(f)
|
||||
f.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p := ipv4.NewPacketConn(c)
|
||||
return p, nil
|
||||
}
|
||||
13
dhcpd/os_windows.go
Normal file
13
dhcpd/os_windows.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package dhcpd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
"golang.org/x/net/ipv4"
|
||||
)
|
||||
|
||||
// Create a socket for receiving broadcast packets
|
||||
func newBroadcastPacketConn(bindAddr net.IP, port int, ifname string) (*ipv4.PacketConn, error) {
|
||||
return nil, errors.New("newBroadcastPacketConn(): not supported on Windows")
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
27
dns.go
27
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"
|
||||
)
|
||||
|
||||
@@ -50,6 +50,11 @@ func generateServerConfig() dnsforward.ServerConfig {
|
||||
FilteringConfig: config.DNS.FilteringConfig,
|
||||
Filters: filters,
|
||||
}
|
||||
bindhost := config.DNS.BindHost
|
||||
if config.DNS.BindHost == "0.0.0.0" {
|
||||
bindhost = "127.0.0.1"
|
||||
}
|
||||
newconfig.ResolverAddress = fmt.Sprintf("%s:%d", bindhost, config.DNS.Port)
|
||||
|
||||
if config.TLS.Enabled {
|
||||
newconfig.TLSConfig = config.TLS.TLSConfig
|
||||
@@ -58,19 +63,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
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package dnsfilter
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -16,8 +17,9 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/bluele/gcache"
|
||||
"github.com/hmage/golibs/log"
|
||||
"golang.org/x/net/publicsuffix"
|
||||
)
|
||||
|
||||
@@ -45,10 +47,11 @@ const enableDelayedCompilation = true // flag for debugging, must be true in pro
|
||||
|
||||
// Config allows you to configure DNS filtering with New() or just change variables directly.
|
||||
type Config struct {
|
||||
ParentalSensitivity int `yaml:"parental_sensitivity"` // must be either 3, 10, 13 or 17
|
||||
ParentalEnabled bool `yaml:"parental_enabled"`
|
||||
SafeSearchEnabled bool `yaml:"safesearch_enabled"`
|
||||
SafeBrowsingEnabled bool `yaml:"safebrowsing_enabled"`
|
||||
ParentalSensitivity int `yaml:"parental_sensitivity"` // must be either 3, 10, 13 or 17
|
||||
ParentalEnabled bool `yaml:"parental_enabled"`
|
||||
SafeSearchEnabled bool `yaml:"safesearch_enabled"`
|
||||
SafeBrowsingEnabled bool `yaml:"safebrowsing_enabled"`
|
||||
ResolverAddress string // DNS server address
|
||||
}
|
||||
|
||||
type privateConfig struct {
|
||||
@@ -180,6 +183,10 @@ func (d *Dnsfilter) CheckHost(host string) (Result, error) {
|
||||
return Result{Reason: NotFilteredNotFound}, nil
|
||||
}
|
||||
host = strings.ToLower(host)
|
||||
// prevent recursion
|
||||
if host == d.parentalServer || host == d.safeBrowsingServer {
|
||||
return Result{}, nil
|
||||
}
|
||||
|
||||
// try filter lists first
|
||||
result, err := d.matchHost(host)
|
||||
@@ -600,6 +607,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 +620,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
|
||||
}
|
||||
|
||||
@@ -638,15 +651,17 @@ func (d *Dnsfilter) checkSafeSearch(host string) (Result, error) {
|
||||
return Result{}, err
|
||||
}
|
||||
|
||||
res.IP = addrs[0]
|
||||
// The next bug may occurs: LookupIP returns DNS64 mapped ipv4 address with zero-prefix
|
||||
for _, i := range addrs {
|
||||
if ipv4 := i.To4(); ipv4 != nil && len(i) == net.IPv6len {
|
||||
if ipv4 := i.To4(); ipv4 != nil {
|
||||
res.IP = ipv4
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(res.IP) == 0 {
|
||||
return Result{}, fmt.Errorf("no ipv4 addresses in safe search response for %s", safeHost)
|
||||
}
|
||||
|
||||
// Cache result
|
||||
err = safeSearchCache.Set(host, res)
|
||||
if err != nil {
|
||||
@@ -656,10 +671,11 @@ func (d *Dnsfilter) checkSafeSearch(host string) (Result, error) {
|
||||
}
|
||||
|
||||
func (d *Dnsfilter) checkSafeBrowsing(host string) (Result, error) {
|
||||
// prevent recursion -- checking the host of safebrowsing server makes no sense
|
||||
if host == d.safeBrowsingServer {
|
||||
return Result{}, nil
|
||||
if log.GetLevel() >= log.DEBUG {
|
||||
timer := log.StartTimer()
|
||||
defer timer.LogElapsed("SafeBrowsing HTTP lookup for %s", host)
|
||||
}
|
||||
|
||||
format := func(hashparam string) string {
|
||||
url := fmt.Sprintf(defaultSafebrowsingURL, d.safeBrowsingServer, hashparam)
|
||||
return url
|
||||
@@ -697,10 +713,11 @@ func (d *Dnsfilter) checkSafeBrowsing(host string) (Result, error) {
|
||||
}
|
||||
|
||||
func (d *Dnsfilter) checkParental(host string) (Result, error) {
|
||||
// prevent recursion -- checking the host of parental safety server makes no sense
|
||||
if host == d.parentalServer {
|
||||
return Result{}, nil
|
||||
if log.GetLevel() >= log.DEBUG {
|
||||
timer := log.StartTimer()
|
||||
defer timer.LogElapsed("Parental HTTP lookup for %s", host)
|
||||
}
|
||||
|
||||
format := func(hashparam string) string {
|
||||
url := fmt.Sprintf(defaultParentalURL, d.parentalServer, hashparam, d.ParentalSensitivity)
|
||||
return url
|
||||
@@ -754,6 +771,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 {
|
||||
@@ -954,6 +972,51 @@ func (d *Dnsfilter) matchHost(host string) (Result, error) {
|
||||
// lifecycle helper functions
|
||||
//
|
||||
|
||||
type dialFunctionType func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
|
||||
// Connect to a remote server resolving hostname using our own DNS server
|
||||
func createCustomDialContext(resolverAddr string) dialFunctionType {
|
||||
return func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
log.Tracef("network:%v addr:%v", network, addr)
|
||||
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dialer := &net.Dialer{
|
||||
Timeout: time.Minute * 5,
|
||||
}
|
||||
|
||||
if net.ParseIP(host) != nil {
|
||||
con, err := dialer.DialContext(ctx, network, addr)
|
||||
return con, err
|
||||
}
|
||||
|
||||
r := upstream.NewResolver(resolverAddr, 30*time.Second)
|
||||
addrs, e := r.LookupIPAddr(ctx, host)
|
||||
log.Tracef("LookupIPAddr: %s: %v", host, addrs)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
|
||||
var firstErr error
|
||||
firstErr = nil
|
||||
for _, a := range addrs {
|
||||
addr = fmt.Sprintf("%s:%s", a.String(), port)
|
||||
con, err := dialer.DialContext(ctx, network, addr)
|
||||
if err != nil {
|
||||
if firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
continue
|
||||
}
|
||||
return con, err
|
||||
}
|
||||
return nil, firstErr
|
||||
}
|
||||
}
|
||||
|
||||
// New creates properly initialized DNS Filter that is ready to be used
|
||||
func New(c *Config) *Dnsfilter {
|
||||
d := new(Dnsfilter)
|
||||
@@ -973,6 +1036,9 @@ func New(c *Config) *Dnsfilter {
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
}
|
||||
if c != nil && len(c.ResolverAddress) != 0 {
|
||||
d.transport.DialContext = createCustomDialContext(c.ResolverAddress)
|
||||
}
|
||||
d.client = http.Client{
|
||||
Transport: d.transport,
|
||||
Timeout: defaultHTTPTimeout,
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -14,6 +14,10 @@ var safeSearchDomains = map[string]string{
|
||||
|
||||
"www.bing.com": "strict.bing.com",
|
||||
|
||||
"duckduckgo.com": "safe.duckduckgo.com",
|
||||
"www.duckduckgo.com": "safe.duckduckgo.com",
|
||||
"start.duckduckgo.com": "safe.duckduckgo.com",
|
||||
|
||||
"www.google.com": "forcesafesearch.google.com",
|
||||
"www.google.ad": "forcesafesearch.google.com",
|
||||
"www.google.ae": "forcesafesearch.google.com",
|
||||
|
||||
@@ -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 != "" {
|
||||
@@ -171,7 +175,10 @@ func (s *Server) startInternal(config *ServerConfig) error {
|
||||
if err != nil {
|
||||
return errorx.Decorate(err, "Failed to parse TLS keypair")
|
||||
}
|
||||
proxyConfig.TLSConfig = &tls.Config{Certificates: []tls.Certificate{keypair}}
|
||||
proxyConfig.TLSConfig = &tls.Config{
|
||||
Certificates: []tls.Certificate{keypair},
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
}
|
||||
|
||||
if proxyConfig.UDPListenAddr == nil {
|
||||
|
||||
@@ -101,7 +101,11 @@ func TestDotServer(t *testing.T) {
|
||||
// Add our self-signed generated config to roots
|
||||
roots := x509.NewCertPool()
|
||||
roots.AppendCertsFromPEM(certPem)
|
||||
tlsConfig := &tls.Config{ServerName: tlsServerName, RootCAs: roots}
|
||||
tlsConfig := &tls.Config{
|
||||
ServerName: tlsServerName,
|
||||
RootCAs: roots,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
|
||||
// Create a DNS-over-TLS client connection
|
||||
addr := s.dnsProxy.Addr(proxy.ProtoTLS)
|
||||
@@ -459,7 +463,7 @@ func createServerTLSConfig(t *testing.T) (*tls.Config, []byte, []byte) {
|
||||
t.Fatalf("failed to create certificate: %s", err)
|
||||
}
|
||||
|
||||
return &tls.Config{Certificates: []tls.Certificate{cert}, ServerName: tlsServerName}, certPem, keyPem
|
||||
return &tls.Config{Certificates: []tls.Certificate{cert}, ServerName: tlsServerName, MinVersion: tls.VersionTLS12}, certPem, keyPem
|
||||
}
|
||||
|
||||
func createDataDir(t *testing.T) string {
|
||||
|
||||
@@ -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()
|
||||
|
||||
229
filter.go
229
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,88 @@ 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
|
||||
if config.firstRun {
|
||||
return 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.Name = f.Name
|
||||
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 +269,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 +289,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 +319,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 +345,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 +354,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 +372,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{}
|
||||
|
||||
12
go.mod
12
go.mod
@@ -1,13 +1,16 @@
|
||||
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/NYTimes/gziphandler v1.1.1
|
||||
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,7 +18,8 @@ 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/stretchr/testify v1.2.2
|
||||
github.com/sparrc/go-ping v0.0.0-20181106165434-ef3ab45e41b0
|
||||
github.com/stretchr/testify v1.3.0
|
||||
go.uber.org/goleak v0.10.0
|
||||
golang.org/x/net v0.0.0-20190119204137-ed066c81e75e
|
||||
golang.org/x/sys v0.0.0-20190122071731-054c452bb702
|
||||
|
||||
21
go.sum
21
go.sum
@@ -1,5 +1,10 @@
|
||||
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/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
|
||||
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
|
||||
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 +17,9 @@ 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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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 +32,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,12 +59,17 @@ 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=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
go.uber.org/goleak v0.10.0 h1:G3eWbSNIskeRqtsN/1uI5B+eP73y3JUuBsv9AZjehb4=
|
||||
go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
|
||||
|
||||
118
helpers.go
118
helpers.go
@@ -2,10 +2,10 @@ package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -15,30 +15,14 @@ import (
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"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 +32,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)
|
||||
}
|
||||
}
|
||||
@@ -60,12 +50,19 @@ func ensureGET(handler func(http.ResponseWriter, *http.Request)) func(http.Respo
|
||||
return ensure("GET", handler)
|
||||
}
|
||||
|
||||
func ensurePUT(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
|
||||
return ensure("PUT", handler)
|
||||
// Bridge between http.Handler object and Go function
|
||||
type httpHandler struct {
|
||||
handler func(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
func ensureDELETE(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
|
||||
return ensure("DELETE", handler)
|
||||
func (h *httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.handler(w, r)
|
||||
}
|
||||
|
||||
func ensureGETHandler(handler func(http.ResponseWriter, *http.Request)) http.Handler {
|
||||
h := httpHandler{}
|
||||
h.handler = ensure("GET", handler)
|
||||
return &h
|
||||
}
|
||||
|
||||
func optionalAuth(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
|
||||
@@ -308,6 +305,77 @@ func checkPacketPortAvailable(host string, port int) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Connect to a remote server resolving hostname using our own DNS server
|
||||
func customDialContext(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
log.Tracef("network:%v addr:%v", network, addr)
|
||||
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dialer := &net.Dialer{
|
||||
Timeout: time.Minute * 5,
|
||||
}
|
||||
|
||||
if net.ParseIP(host) != nil {
|
||||
con, err := dialer.DialContext(ctx, network, addr)
|
||||
return con, err
|
||||
}
|
||||
|
||||
bindhost := config.DNS.BindHost
|
||||
if config.DNS.BindHost == "0.0.0.0" {
|
||||
bindhost = "127.0.0.1"
|
||||
}
|
||||
resolverAddr := fmt.Sprintf("%s:%d", bindhost, config.DNS.Port)
|
||||
r := upstream.NewResolver(resolverAddr, 30*time.Second)
|
||||
addrs, e := r.LookupIPAddr(ctx, host)
|
||||
log.Tracef("LookupIPAddr: %s: %v", host, addrs)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
|
||||
var firstErr error
|
||||
firstErr = nil
|
||||
for _, a := range addrs {
|
||||
addr = fmt.Sprintf("%s:%s", a.String(), port)
|
||||
con, err := dialer.DialContext(ctx, network, addr)
|
||||
if err != nil {
|
||||
if firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
continue
|
||||
}
|
||||
return con, err
|
||||
}
|
||||
return nil, firstErr
|
||||
}
|
||||
|
||||
// check if error is "address already in use"
|
||||
func errorIsAddrInUse(err error) bool {
|
||||
errOpError, ok := err.(*net.OpError)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
errSyscallError, ok := errOpError.Err.(*os.SyscallError)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
errErrno, ok := errSyscallError.Err.(syscall.Errno)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
const WSAEADDRINUSE = 10048
|
||||
return errErrno == WSAEADDRINUSE
|
||||
}
|
||||
|
||||
return errErrno == syscall.EADDRINUSE
|
||||
}
|
||||
|
||||
// ---------------------
|
||||
// debug logging helpers
|
||||
// ---------------------
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -2,7 +2,7 @@ swagger: '2.0'
|
||||
info:
|
||||
title: 'AdGuard Home'
|
||||
description: 'AdGuard Home REST API. Admin web interface is built on top of this REST API.'
|
||||
version: 0.93.0
|
||||
version: 0.95.0
|
||||
schemes:
|
||||
- http
|
||||
basePath: /control
|
||||
@@ -39,6 +39,9 @@ tags:
|
||||
-
|
||||
name: dhcp
|
||||
description: 'Built-in DHCP server controls'
|
||||
-
|
||||
name: clients
|
||||
description: 'Clients list operations'
|
||||
-
|
||||
name: install
|
||||
description: 'First-time install configuration handlers'
|
||||
@@ -87,26 +90,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 +114,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.'
|
||||
@@ -679,6 +671,22 @@ paths:
|
||||
application/json:
|
||||
enabled: false
|
||||
|
||||
# --------------------------------------------------
|
||||
# Clients list methods
|
||||
# --------------------------------------------------
|
||||
|
||||
/clients:
|
||||
get:
|
||||
tags:
|
||||
- clients
|
||||
operationId: clientsStatus
|
||||
summary: 'Get information about configured clients'
|
||||
responses:
|
||||
200:
|
||||
description: OK
|
||||
schema:
|
||||
$ref: "#/definitions/Clients"
|
||||
|
||||
# --------------------------------------------------
|
||||
# I18N methods
|
||||
# --------------------------------------------------
|
||||
@@ -731,6 +739,26 @@ paths:
|
||||
description: OK
|
||||
schema:
|
||||
$ref: "#/definitions/AddressesInfo"
|
||||
/install/check_config:
|
||||
post:
|
||||
tags:
|
||||
- install
|
||||
operationId: installCheckConfig
|
||||
summary: "Checks configuration"
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "Configuration to be checked"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/CheckConfigRequest"
|
||||
responses:
|
||||
200:
|
||||
description: OK
|
||||
schema:
|
||||
$ref: "#/definitions/CheckConfigResponse"
|
||||
400:
|
||||
description: "Failed to parse JSON or cannot listen on the specified address"
|
||||
/install/configure:
|
||||
post:
|
||||
tags:
|
||||
@@ -798,6 +826,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"
|
||||
@@ -1087,14 +1142,33 @@ definitions:
|
||||
DhcpSearchResult:
|
||||
type: "object"
|
||||
description: "Information about a DHCP server discovered in the current network"
|
||||
required:
|
||||
- "found"
|
||||
properties:
|
||||
other_server:
|
||||
$ref: "#/definitions/DhcpSearchResultOtherServer"
|
||||
static_ip:
|
||||
$ref: "#/definitions/DhcpSearchResultStaticIP"
|
||||
DhcpSearchResultOtherServer:
|
||||
type: "object"
|
||||
properties:
|
||||
found:
|
||||
type: "boolean"
|
||||
gateway_ip:
|
||||
type: "string"
|
||||
example: "192.168.1.1"
|
||||
description: "yes|no|error"
|
||||
example: "no"
|
||||
error:
|
||||
type: "string"
|
||||
description: "Set if found=error"
|
||||
example: ""
|
||||
DhcpSearchResultStaticIP:
|
||||
type: "object"
|
||||
properties:
|
||||
static:
|
||||
type: "string"
|
||||
description: "yes|no|error"
|
||||
example: "yes"
|
||||
ip:
|
||||
type: "string"
|
||||
description: "Set if static=no"
|
||||
example: ""
|
||||
DnsAnswer:
|
||||
type: "object"
|
||||
description: "DNS answer section"
|
||||
@@ -1247,6 +1321,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"
|
||||
@@ -1276,27 +1354,82 @@ definitions:
|
||||
properties:
|
||||
ip:
|
||||
type: "string"
|
||||
example: "127.0.01"
|
||||
example: "127.0.0.1"
|
||||
port:
|
||||
type: "integer"
|
||||
format: "int32"
|
||||
example: 53
|
||||
warning:
|
||||
type: "string"
|
||||
example: "Cannot bind to this port"
|
||||
AddressesInfo:
|
||||
type: "object"
|
||||
description: "AdGuard Home addresses configuration"
|
||||
properties:
|
||||
dns:
|
||||
$ref: "#/definitions/AddressInfo"
|
||||
web:
|
||||
$ref: "#/definitions/AddressInfo"
|
||||
dns_port:
|
||||
type: "integer"
|
||||
format: "int32"
|
||||
example: 53
|
||||
web_port:
|
||||
type: "integer"
|
||||
format: "int32"
|
||||
example: 80
|
||||
interfaces:
|
||||
type: "object"
|
||||
description: "Network interfaces dictionary (key is the interface name)"
|
||||
additionalProperties:
|
||||
$ref: "#/definitions/NetInterface"
|
||||
Client:
|
||||
type: "object"
|
||||
description: "Client information"
|
||||
properties:
|
||||
ip:
|
||||
type: "string"
|
||||
description: "IP address"
|
||||
example: "127.0.0.1"
|
||||
name:
|
||||
type: "string"
|
||||
description: "Name"
|
||||
example: "localhost"
|
||||
Clients:
|
||||
type: "array"
|
||||
items:
|
||||
$ref: "#/definitions/Client"
|
||||
description: "Clients array"
|
||||
CheckConfigRequest:
|
||||
type: "object"
|
||||
description: "Configuration to be checked"
|
||||
properties:
|
||||
dns:
|
||||
$ref: "#/definitions/CheckConfigRequestInfo"
|
||||
web:
|
||||
$ref: "#/definitions/CheckConfigRequestInfo"
|
||||
CheckConfigRequestInfo:
|
||||
type: "object"
|
||||
properties:
|
||||
ip:
|
||||
type: "string"
|
||||
example: "127.0.0.1"
|
||||
port:
|
||||
type: "integer"
|
||||
format: "int32"
|
||||
example: 53
|
||||
autofix:
|
||||
type: "boolean"
|
||||
example: false
|
||||
CheckConfigResponse:
|
||||
type: "object"
|
||||
properties:
|
||||
dns:
|
||||
$ref: "#/definitions/CheckConfigResponseInfo"
|
||||
web:
|
||||
$ref: "#/definitions/CheckConfigResponseInfo"
|
||||
CheckConfigResponseInfo:
|
||||
type: "object"
|
||||
properties:
|
||||
status:
|
||||
type: "string"
|
||||
example: ""
|
||||
can_autofix:
|
||||
type: "boolean"
|
||||
example: false
|
||||
InitialConfiguration:
|
||||
type: "object"
|
||||
description: "AdGuard Home initial configuration (for the first-install wizard)"
|
||||
|
||||
27
os_unix.go
Normal file
27
os_unix.go
Normal file
@@ -0,0 +1,27 @@
|
||||
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// Set user-specified limit of how many fd's we can use
|
||||
// https://github.com/AdguardTeam/AdGuardHome/issues/659
|
||||
func setRlimit(val uint) {
|
||||
var rlim syscall.Rlimit
|
||||
rlim.Max = uint64(val)
|
||||
rlim.Cur = uint64(val)
|
||||
err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rlim)
|
||||
if err != nil {
|
||||
log.Error("Setrlimit() failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the current user has root (administrator) rights
|
||||
func haveAdminRights() (bool, error) {
|
||||
return os.Getuid() == 0, nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user