mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-07-16 13:17:19 +02:00
Compare commits
246 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f39512b4c0 | |||
| 7ce62ccaec | |||
| f7d3db06c6 | |||
| 0ca37dc707 | |||
| 2bcba7b578 | |||
| 829e93c079 | |||
| 4896563e3c | |||
| 0c096d5f07 | |||
| ab8f072388 | |||
| 32219e7d32 | |||
| d292e03d1b | |||
| 5dd6336953 | |||
| 854a244ebb | |||
| 125b4b6077 | |||
| 46e8d4fad7 | |||
| e5389ffecb | |||
| 46509be8a0 | |||
| d3d2ed539f | |||
| 8496adc638 | |||
| e1d078a2c3 | |||
| 0dee7518c4 | |||
| 774f07dd7f | |||
| c271896551 | |||
| 82d887f52d | |||
| 6e27f877ff | |||
| 39a2cab051 | |||
| 72d2f4e7e3 | |||
| 19bc44a7f3 | |||
| 59dc74ffbb | |||
| 12c8ab696f | |||
| 28f32bd7e5 | |||
| 6b43639be5 | |||
| 6be80e4827 | |||
| 437fb1b16d | |||
| 61b6431b6e | |||
| 7ccecdd9f7 | |||
| e43b2b5530 | |||
| 2cd8b7e021 | |||
| d6768c4c39 | |||
| 59a895bfe2 | |||
| cacd957594 | |||
| 2cd063ebd6 | |||
| 9ed8e49a08 | |||
| 66cb7cc21d | |||
| 4bf09120ff | |||
| be0769e433 | |||
| 7b476e38be | |||
| 0a7d3445f4 | |||
| 76d2e2c226 | |||
| 3007cb86ec | |||
| fa3af372ab | |||
| 48a780fc3e | |||
| 28df551195 | |||
| e65a71b2ae | |||
| dc61fd2554 | |||
| a4edf266f0 | |||
| 7af59ee589 | |||
| 3f3c1d6d78 | |||
| ab1d7fd796 | |||
| 6c2996a921 | |||
| de32dd8ba4 | |||
| d43e50ee2d | |||
| aec2596262 | |||
| 78a7c87ecc | |||
| 1d3f8757bc | |||
| c0c69d0739 | |||
| 1aa991298a | |||
| f3a3227f21 | |||
| a4c1983657 | |||
| cc28b92935 | |||
| eaa907a647 | |||
| de951fd895 | |||
| 3f211d3cc2 | |||
| 2f46d512c6 | |||
| 12148ec231 | |||
| 9fe6af684f | |||
| 472bb05e95 | |||
| 50bfed706d | |||
| 350d8355b1 | |||
| 03781d4cec | |||
| 67e4afc06e | |||
| 32482809b7 | |||
| c315d21be9 | |||
| 48b2031269 | |||
| 41139b3343 | |||
| d5e6c7b13f | |||
| 60d6734e1f | |||
| e684c7d8c4 | |||
| ce35383341 | |||
| 5553490b27 | |||
| eaf39f48a0 | |||
| a5ddbdcb42 | |||
| 0c99d27be5 | |||
| b9eb89c02e | |||
| 53f8d006f0 | |||
| 929de49c7b | |||
| 542c4f7daf | |||
| c941f9c621 | |||
| 25eae187db | |||
| 726a25a7ea | |||
| a46bb152af | |||
| bbfa7c6c22 | |||
| 1cd54a48e9 | |||
| 2d950eecdf | |||
| b143e46eb0 | |||
| 8fda856e24 | |||
| 54e63ccf9b | |||
| ee53db1e35 | |||
| fc502b920b | |||
| 20eae82f11 | |||
| d2fc530316 | |||
| 7ac5555a84 | |||
| 15d397d8a6 | |||
| b471adfb09 | |||
| d7a38363e6 | |||
| 90def8f9b5 | |||
| b126db453b | |||
| 601d357456 | |||
| 3a2024ebd7 | |||
| 6cd451acec | |||
| 3b6c12abd4 | |||
| d9dfc584e7 | |||
| 57fa68970a | |||
| fa14f1dadf | |||
| 9689607409 | |||
| d75f871541 | |||
| 45895067c6 | |||
| 521f06dcc1 | |||
| 5b6a3a4c6f | |||
| be497a68de | |||
| c872a3b3f6 | |||
| e0ae0f8e7b | |||
| ad4ca32873 | |||
| 24100c4cbe | |||
| e3a792d50d | |||
| 440d085c6d | |||
| 270ea9f6ca | |||
| 7a156d7d15 | |||
| 4c45e6cf3d | |||
| 704bc27dba | |||
| b267572b38 | |||
| 5cad0d6be1 | |||
| 56d8dc865f | |||
| d57c1d6d44 | |||
| 02fa7fbe2e | |||
| 07689954bf | |||
| a7ea20b117 | |||
| 43fecdf60f | |||
| 31239684c7 | |||
| 5528ac8bf1 | |||
| 411e23ecfe | |||
| 7bf231643b | |||
| 2326160f2f | |||
| 68fe7e8406 | |||
| c7bad63869 | |||
| 69319c6b41 | |||
| 9df381d3d1 | |||
| 0af7f64bca | |||
| f73cbde7a5 | |||
| 0645a738ad | |||
| d52cd11322 | |||
| d3d08022cc | |||
| 21c8b9f8e7 | |||
| 6c55d8f139 | |||
| ccdb2a3f70 | |||
| f5ef9b917e | |||
| a5443d5ca4 | |||
| 2c7d95bba2 | |||
| 8a2cdbfaa3 | |||
| c94be0df35 | |||
| 4b6a976747 | |||
| 0043fdf859 | |||
| 24e62e18fa | |||
| 663dbbb476 | |||
| 471427a439 | |||
| a777c4b00f | |||
| dcc4cdd316 | |||
| 9c22701940 | |||
| a77a924320 | |||
| 95dbf71939 | |||
| 8869e33a20 | |||
| c94e1b02d2 | |||
| 42d29b626b | |||
| b65a5ac283 | |||
| ba48ff5965 | |||
| b3a342bc44 | |||
| 9927803497 | |||
| f0c604a9f1 | |||
| 8a56389396 | |||
| 9f7bfc76db | |||
| a7a5501ea5 | |||
| c401c4ef87 | |||
| 8ffb42962a | |||
| aad04200cb | |||
| 4bfcacaf3c | |||
| 5b362412be | |||
| ccf07a7d1c | |||
| e4eb3b2ded | |||
| 77b62f8734 | |||
| 096e7ea429 | |||
| 3e6f6cc721 | |||
| 7dab688252 | |||
| 7cd1f7adda | |||
| 9a249c3029 | |||
| 0dfa377e08 | |||
| 14bc29751f | |||
| e6800fbc82 | |||
| 4f6c2032a1 | |||
| d1589bd9d6 | |||
| 85c95a6a3a | |||
| fa50cd4df4 | |||
| 018f6651c1 | |||
| 1a40767cb7 | |||
| 12512a60da | |||
| b0114dfaeb | |||
| fb20d443c1 | |||
| 262dcb1dff | |||
| 8b08cc8a6e | |||
| 930a5ad439 | |||
| 8852f60ccb | |||
| 2e1b3f9d07 | |||
| 6d3c82d38d | |||
| cad71997aa | |||
| 82900eeca6 | |||
| 84fca06c62 | |||
| 64f2dcb25b | |||
| 4c2d21a8f8 | |||
| 4172fc09d0 | |||
| d9b699501d | |||
| 71b1b324db | |||
| 35c890048b | |||
| bac6810956 | |||
| 997ec342e0 | |||
| e385547461 | |||
| 83b551fb2d | |||
| 45f827a2c5 | |||
| 3218b5fac1 | |||
| df514d15a5 | |||
| 50b0e5a4b0 | |||
| 6428ac23a0 | |||
| 790cb773e2 | |||
| 9dab097268 | |||
| f13f61592c | |||
| 2f42fc055d | |||
| 1cdce73070 | |||
| 5f9ac5889b |
@@ -0,0 +1,2 @@
|
|||||||
|
Dockerfile
|
||||||
|
.git/
|
||||||
@@ -9,18 +9,18 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: ["windows-latest", "ubuntu-latest", "macOS-latest"]
|
os: ["windows-latest", "ubuntu-latest", "macOS-latest"]
|
||||||
go: ["1.19.x"]
|
go: ["1.20.x"]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
- uses: WillAbides/setup-go-faster@v1.7.0
|
- uses: WillAbides/setup-go-faster@v1.8.0
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go }}
|
go-version: ${{ matrix.go }}
|
||||||
- run: "go test -race ./..."
|
- run: "go test -race ./..."
|
||||||
- uses: dominikh/staticcheck-action@v1.2.0
|
- uses: dominikh/staticcheck-action@v1.2.0
|
||||||
with:
|
with:
|
||||||
version: "2022.1.1"
|
version: "2023.1.2"
|
||||||
install-go: false
|
install-go: false
|
||||||
cache-key: ${{ matrix.go }}
|
cache-key: ${{ matrix.go }}
|
||||||
|
|||||||
+3
-1
@@ -1,3 +1,5 @@
|
|||||||
|
|
||||||
dist/
|
dist/
|
||||||
gon.hcl
|
gon.hcl
|
||||||
|
|
||||||
|
/Build
|
||||||
|
.DS_Store
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ builds:
|
|||||||
- -trimpath
|
- -trimpath
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w
|
- -s -w
|
||||||
|
- -X main.version={{.Version}}
|
||||||
|
- -X main.commit={{.Commit}}
|
||||||
goos:
|
goos:
|
||||||
- darwin
|
- darwin
|
||||||
goarch:
|
goarch:
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
before:
|
||||||
|
hooks:
|
||||||
|
- go mod tidy
|
||||||
|
builds:
|
||||||
|
- id: ctrld
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
flags:
|
||||||
|
- -trimpath
|
||||||
|
ldflags:
|
||||||
|
- -s -w
|
||||||
|
- -X main.version={{.Version}}
|
||||||
|
- -X main.commit={{.Commit}}
|
||||||
|
goos:
|
||||||
|
- darwin
|
||||||
|
- linux
|
||||||
|
- windows
|
||||||
|
goarch:
|
||||||
|
- 386
|
||||||
|
- arm
|
||||||
|
- amd64
|
||||||
|
- arm64
|
||||||
|
tags:
|
||||||
|
- qf
|
||||||
|
main: ./cmd/ctrld
|
||||||
|
hooks:
|
||||||
|
post: /bin/sh ./scripts/upx.sh {{ .Path }}
|
||||||
|
archives:
|
||||||
|
- format_overrides:
|
||||||
|
- goos: windows
|
||||||
|
format: zip
|
||||||
|
strip_parent_binary_folder: true
|
||||||
|
wrap_in_directory: true
|
||||||
|
files:
|
||||||
|
- README.md
|
||||||
|
checksum:
|
||||||
|
name_template: 'checksums.txt'
|
||||||
|
snapshot:
|
||||||
|
name_template: "{{ incpatch .Version }}-next"
|
||||||
|
changelog:
|
||||||
|
sort: asc
|
||||||
|
filters:
|
||||||
|
exclude:
|
||||||
|
- '^docs:'
|
||||||
@@ -9,13 +9,17 @@ builds:
|
|||||||
- -trimpath
|
- -trimpath
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w
|
- -s -w
|
||||||
|
- -X main.version={{.Version}}
|
||||||
|
- -X main.commit={{.Commit}}
|
||||||
goos:
|
goos:
|
||||||
- linux
|
- linux
|
||||||
|
- freebsd
|
||||||
- windows
|
- windows
|
||||||
goarch:
|
goarch:
|
||||||
- 386
|
- 386
|
||||||
- arm
|
- arm
|
||||||
- mips
|
- mips
|
||||||
|
- mipsle
|
||||||
- amd64
|
- amd64
|
||||||
- arm64
|
- arm64
|
||||||
goarm:
|
goarm:
|
||||||
@@ -25,6 +29,12 @@ builds:
|
|||||||
gomips:
|
gomips:
|
||||||
- softfloat
|
- softfloat
|
||||||
main: ./cmd/ctrld
|
main: ./cmd/ctrld
|
||||||
|
hooks:
|
||||||
|
post: /bin/sh ./scripts/upx.sh {{ .Path }}
|
||||||
|
ignore:
|
||||||
|
- goos: freebsd
|
||||||
|
goarch: arm
|
||||||
|
goarm: 5
|
||||||
archives:
|
archives:
|
||||||
- format_overrides:
|
- format_overrides:
|
||||||
- goos: windows
|
- goos: windows
|
||||||
|
|||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
# Using Debian bullseye for building regular image.
|
||||||
|
# Using scratch image for minimal image size.
|
||||||
|
# The final image has:
|
||||||
|
#
|
||||||
|
# - Timezone info file.
|
||||||
|
# - CA certs file.
|
||||||
|
# - /etc/{passwd,group} file.
|
||||||
|
# - Non-cgo ctrld binary.
|
||||||
|
#
|
||||||
|
# CI_COMMIT_TAG is used to set the version of ctrld binary.
|
||||||
|
FROM golang:bullseye as base
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y upx-ucl
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
ARG tag=master
|
||||||
|
ENV CI_COMMIT_TAG=$tag
|
||||||
|
RUN CTRLD_NO_QF=yes CGO_ENABLED=0 ./scripts/build.sh
|
||||||
|
|
||||||
|
FROM scratch
|
||||||
|
|
||||||
|
COPY --from=base /usr/share/zoneinfo /usr/share/zoneinfo
|
||||||
|
COPY --from=base /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||||
|
COPY --from=base /etc/passwd /etc/passwd
|
||||||
|
COPY --from=base /etc/group /etc/group
|
||||||
|
|
||||||
|
COPY --from=base /app/ctrld-linux-*-nocgo ctrld
|
||||||
|
|
||||||
|
ENTRYPOINT ["./ctrld", "run"]
|
||||||
@@ -1,9 +1,19 @@
|
|||||||
# ctrld
|
# ctrld
|
||||||
|
|
||||||
|

|
||||||
|
[](https://pkg.go.dev/github.com/Control-D-Inc/ctrld)
|
||||||
|
[](https://goreportcard.com/report/github.com/Control-D-Inc/ctrld)
|
||||||
|
|
||||||
A highly configurable DNS forwarding proxy with support for:
|
A highly configurable DNS forwarding proxy with support for:
|
||||||
- Multiple listeners for incoming queries
|
- Multiple listeners for incoming queries
|
||||||
- Multiple upstreams with fallbacks
|
- Multiple upstreams with fallbacks
|
||||||
- Multiple network policy driven DNS query steering
|
- Multiple network policy driven DNS query steering
|
||||||
- Policy driven domain based "split horizon" DNS with wildcard support
|
- Policy driven domain based "split horizon" DNS with wildcard support
|
||||||
|
- Integrations with common router vendors and firmware
|
||||||
|
- LAN client discovery via DHCP, mDNS, and ARP
|
||||||
|
|
||||||
|
## TLDR
|
||||||
|
Proxy legacy DNS traffic to secure DNS upstreams in highly configurable ways.
|
||||||
|
|
||||||
All DNS protocols are supported, including:
|
All DNS protocols are supported, including:
|
||||||
- `UDP 53`
|
- `UDP 53`
|
||||||
@@ -12,23 +22,46 @@ All DNS protocols are supported, including:
|
|||||||
- `DNS-over-HTTP/3` (DOH3)
|
- `DNS-over-HTTP/3` (DOH3)
|
||||||
- `DNS-over-QUIC`
|
- `DNS-over-QUIC`
|
||||||
|
|
||||||
## Use Cases
|
# Use Cases
|
||||||
1. Use secure DNS protocols on networks and devices that don't natively support them (legacy routers, legacy OSes, TVs, smart toasters).
|
1. Use secure DNS protocols on networks and devices that don't natively support them (legacy routers, legacy OSes, TVs, smart toasters).
|
||||||
2. Create source IP based DNS routing policies with variable secure DNS upstreams. Subnet 1 (admin) uses upstream resolver A, while Subnet 2 (employee) uses upstream resolver B.
|
2. Create source IP based DNS routing policies with variable secure DNS upstreams. Subnet 1 (admin) uses upstream resolver A, while Subnet 2 (employee) uses upstream resolver B.
|
||||||
3. Create destination IP based DNS routing policies with variable secure DNS upstreams. Listener 1 uses upstream resolver C, while Listener 2 uses upstream resolver D.
|
3. Create destination IP based DNS routing policies with variable secure DNS upstreams. Listener 1 uses upstream resolver C, while Listener 2 uses upstream resolver D.
|
||||||
4. Create domain level "split horizon" DNS routing policies to send internal domains (*.company.int) to a local DNS server, while everything else goes to another upstream.
|
4. Create domain level "split horizon" DNS routing policies to send internal domains (*.company.int) to a local DNS server, while everything else goes to another upstream.
|
||||||
|
5. Deploy on a router and create LAN client specific DNS routing policies from a web GUI (When using ControlD.com).
|
||||||
|
|
||||||
|
|
||||||
## OS Support
|
## OS Support
|
||||||
- Windows (386, amd64, arm)
|
- Windows (386, amd64, arm)
|
||||||
- Mac (amd64, arm64)
|
- Mac (amd64, arm64)
|
||||||
- Linux (386, amd64, arm, mips)
|
- Linux (386, amd64, arm, mips)
|
||||||
|
- FreeBSD
|
||||||
|
- Common routers (See Router Mode below)
|
||||||
|
|
||||||
## Download
|
# Install
|
||||||
Download pre-compiled binaries from the [Releases](https://github.com/Control-D-Inc/ctrld/releases) section.
|
There are several ways to download and install `ctrld.
|
||||||
|
|
||||||
|
## Quick Install
|
||||||
|
The simplest way to download and install `ctrld` is to use the following installer command on any UNIX-like platform:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sh -c 'sh -c "$(curl -sL https://api.controld.com/dl)"'
|
||||||
|
```
|
||||||
|
|
||||||
|
Windows user and prefer Powershell (who doesn't)? No problem, execute this command instead in administrative cmd:
|
||||||
|
```shell
|
||||||
|
powershell -Command "(Invoke-WebRequest -Uri 'https://api.controld.com/dl' -UseBasicParsing).Content | Set-Content 'ctrld_install.bat'" && ctrld_install.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
Or you can pull and run a Docker container from [Docker Hub](https://hub.docker.com/r/controldns/ctrld)
|
||||||
|
```
|
||||||
|
$ docker pull controldns/ctrld
|
||||||
|
```
|
||||||
|
|
||||||
|
## Download Manually
|
||||||
|
Alternatively, if you know what you're doing you can download pre-compiled binaries from the [Releases](https://github.com/Control-D-Inc/ctrld/releases) section for the appropriate platform.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
`ctrld` requires `go1.19+`:
|
Lastly, you can build `ctrld` from source which requires `go1.19+`:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
$ go build ./cmd/ctrld
|
$ go build ./cmd/ctrld
|
||||||
@@ -40,6 +73,17 @@ or
|
|||||||
$ go install github.com/Control-D-Inc/ctrld/cmd/ctrld@latest
|
$ go install github.com/Control-D-Inc/ctrld/cmd/ctrld@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
```
|
||||||
|
$ docker build -t controldns/ctrld .
|
||||||
|
$ docker run -d --name=ctrld -p 53:53/tcp -p 53:53/udp controldns/ctrld --cd=RESOLVER_ID_GOES_HERE -vv
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
The cli is self documenting, so free free to run `--help` on any sub-command to get specific usages.
|
||||||
|
|
||||||
## Arguments
|
## Arguments
|
||||||
```
|
```
|
||||||
__ .__ .___
|
__ .__ .___
|
||||||
@@ -55,18 +99,23 @@ Usage:
|
|||||||
Available Commands:
|
Available Commands:
|
||||||
run Run the DNS proxy server
|
run Run the DNS proxy server
|
||||||
service Manage ctrld service
|
service Manage ctrld service
|
||||||
start Quick start service and configure DNS on default interface
|
start Quick start service and configure DNS on interface
|
||||||
stop Quick stop service and remove DNS from default interface
|
stop Quick stop service and remove DNS from interface
|
||||||
|
restart Restart the ctrld service
|
||||||
|
status Show status of the ctrld service
|
||||||
|
uninstall Stop and uninstall the ctrld service
|
||||||
|
clients Manage clients
|
||||||
|
|
||||||
Flags:
|
Flags:
|
||||||
-h, --help help for ctrld
|
-h, --help help for ctrld
|
||||||
|
-s, --silent do not write any log output
|
||||||
-v, --verbose count verbose log output, "-v" basic logging, "-vv" debug level logging
|
-v, --verbose count verbose log output, "-v" basic logging, "-vv" debug level logging
|
||||||
--version version for ctrld
|
--version version for ctrld
|
||||||
|
|
||||||
Use "ctrld [command] --help" for more information about a command.
|
Use "ctrld [command] --help" for more information about a command.
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Basic Run Mode
|
||||||
To start the server with default configuration, simply run: `./ctrld run`. This will create a generic `ctrld.toml` file in the **working directory** and start the application in foreground.
|
To start the server with default configuration, simply run: `./ctrld run`. This will create a generic `ctrld.toml` file in the **working directory** and start the application in foreground.
|
||||||
1. Start the server
|
1. Start the server
|
||||||
```
|
```
|
||||||
@@ -80,40 +129,33 @@ To start the server with default configuration, simply run: `./ctrld run`. This
|
|||||||
147.185.34.1
|
147.185.34.1
|
||||||
```
|
```
|
||||||
|
|
||||||
If `verify.controld.com` resolves, you're successfully using the default Control D upstream.
|
If `verify.controld.com` resolves, you're successfully using the default Control D upstream. From here, you can start editing the config file and go nuts with it. To enforce a new config, restart the server.
|
||||||
|
|
||||||
### Service Mode
|
## Service Mode
|
||||||
To run the application in service mode, simply run: `./ctrld start` as system/root user. This will create a generic `ctrld.toml` file in the **user home** directory, start the system service, and configure the listener on the default interface. Service will start on OS boot.
|
To run the application in service mode on any Windows, MacOS, Linux distibution or supported router, simply run: `./ctrld start` as system/root user. This will create a generic `ctrld.toml` file in the **user home** directory (on Windows) or `/etc/controld/` (almost everywhere else), start the system service, and configure the listener on the default network interface. Service will start on OS boot.
|
||||||
|
|
||||||
In order to stop the service, and restore your DNS to original state, simply run `./ctrld stop`.
|
When Control D upstreams are used, `ctrld` willl [relay your network topology](https://docs.controld.com/docs/device-clients) to Control D (LAN IPs, MAC addresses, and hostnames), and you will be able to see your LAN devices in the web panel, view analytics and apply unique profiles to them.
|
||||||
|
|
||||||
For granular control of the service, run the `service` command. Each sub-command has its own help section so you can see what arguments you can supply.
|
In order to stop the service, and restore your DNS to original state, simply run `./ctrld stop`. If you wish to stop and uninstall the service permanently, run `./ctrld uninstall`.
|
||||||
|
|
||||||
```
|
|
||||||
Manage ctrld service
|
|
||||||
|
|
||||||
Usage:
|
### Supported Routers
|
||||||
ctrld service [command]
|
You can run `ctrld` on any supported router, which will function similarly to the Service Mode mentioned above. The list of supported routers and firmware includes:
|
||||||
|
- Asus Merlin
|
||||||
|
- DD-WRT
|
||||||
|
- Firewalla
|
||||||
|
- FreshTomato
|
||||||
|
- GL.iNet
|
||||||
|
- OpenWRT
|
||||||
|
- pfSense / OPNsense
|
||||||
|
- Synology
|
||||||
|
- Ubiquiti (UniFi, EdgeOS)
|
||||||
|
|
||||||
Available Commands:
|
`ctrld` will attempt to interface with dnsmasq whenever possible and set itself as the upstream, while running on port 5354. On FreeBSD based OSes, `ctrld` will terminate dnsmasq and unbound in order to be able to listen on port 53 directly.
|
||||||
interfaces Manage network interfaces
|
|
||||||
restart Restart the ctrld service
|
|
||||||
start Start the ctrld service
|
|
||||||
status Show status of the ctrld service
|
|
||||||
stop Stop the ctrld service
|
|
||||||
uninstall Uninstall the ctrld service
|
|
||||||
|
|
||||||
Flags:
|
|
||||||
-h, --help help for service
|
|
||||||
|
|
||||||
Global Flags:
|
|
||||||
-v, --verbose count verbose log output, "-v" basic logging, "-vv" debug level logging
|
|
||||||
|
|
||||||
Use "ctrld service [command] --help" for more information about a command.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Control D Auto Configuration
|
### Control D Auto Configuration
|
||||||
Application can be started with a specific resolver config, instead of the default one. Simply supply your resolver ID with a `--cd` flag, when using the `run` (foreground) or `start` (service) modes.
|
Application can be started with a specific resolver config, instead of the default one. Simply supply your Resolver ID with a `--cd` flag, when using the `run` (foreground) or `start` (service) modes.
|
||||||
|
|
||||||
The following command will start the application in foreground mode, using the free "p2" resolver, which blocks Ads & Trackers.
|
The following command will start the application in foreground mode, using the free "p2" resolver, which blocks Ads & Trackers.
|
||||||
|
|
||||||
@@ -121,22 +163,22 @@ The following command will start the application in foreground mode, using the f
|
|||||||
./ctrld run --cd p2
|
./ctrld run --cd p2
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternatively, you can use your own personal Control D Device resolver, and start the application in service mode. Your resolver ID is the part after the slash of your DNS-over-HTTPS resolver. ie. https://dns.controld.com/abcd1234
|
Alternatively, you can use your own personal Control D Device resolver, and start the application in service mode. Your resolver ID is displayed on the "Show Resolvers" screen for the relevant Control D Device.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
./ctrld start --cd abcd1234
|
./ctrld start --cd abcd1234
|
||||||
```
|
```
|
||||||
|
|
||||||
Once you run the above command, the following things will happen:
|
Once you run the above commands (in service mode only), the following things will happen:
|
||||||
- You resolver configuration will be fetched from the API, and config file templated with the resolver data
|
- You resolver configuration will be fetched from the API, and config file templated with the resolver data
|
||||||
- Application will start as a service, and keep running (even after reboot) until you run the `stop` or `service uninstall` sub-commands
|
- Application will start as a service, and keep running (even after reboot) until you run the `stop` or `uninstall` sub-commands
|
||||||
- Your default network interface will be updated to use the listener started by the service
|
- Your default network interface will be updated to use the listener started by the service
|
||||||
- All OS DNS queries will be sent to the listener
|
- All OS DNS queries will be sent to the listener
|
||||||
|
|
||||||
## Configuration
|
# Configuration
|
||||||
See [Configuration Docs](docs/config.md).
|
See [Configuration Docs](docs/config.md).
|
||||||
|
|
||||||
### Example
|
## Example
|
||||||
- Start `listener.0` on 127.0.0.1:53
|
- Start `listener.0` on 127.0.0.1:53
|
||||||
- Accept queries from any source address
|
- Accept queries from any source address
|
||||||
- Send all queries to `upstream.0` via DoH protocol
|
- Send all queries to `upstream.0` via DoH protocol
|
||||||
@@ -178,17 +220,17 @@ See [Configuration Docs](docs/config.md).
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Advanced
|
## Advanced Configuration
|
||||||
The above is the most basic example, which will work out of the box. If you're looking to do advanced configurations using policies, see [Configuration Docs](docs/config.md) for complete documentation of the config file.
|
The above is the most basic example, which will work out of the box. If you're looking to do advanced configurations using policies, see [Configuration Docs](docs/config.md) for complete documentation of the config file.
|
||||||
|
|
||||||
You can also supply configuration via launch argeuments, in [Ephemeral Mode](docs/ephemeral_mode.md).
|
You can also supply configuration via launch argeuments, in [Ephemeral Mode](docs/ephemeral_mode.md).
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
See [Contribution Guideline](./docs/contributing.md)
|
See [Contribution Guideline](./docs/contributing.md)
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
The following functionality is on the roadmap and will be available in future releases.
|
The following functionality is on the roadmap and will be available in future releases.
|
||||||
- Router self-installation
|
|
||||||
- Client hostname/MAC passthrough
|
|
||||||
- Prometheus metrics exporter
|
- Prometheus metrics exporter
|
||||||
|
- DNS intercept mode
|
||||||
|
- Direct listener mode
|
||||||
|
- Support for more routers (let us know which ones)
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package ctrld
|
||||||
|
|
||||||
|
// ClientInfoCtxKey is the context key to store client info.
|
||||||
|
type ClientInfoCtxKey struct{}
|
||||||
|
|
||||||
|
// ClientInfo represents ctrld's clients information.
|
||||||
|
type ClientInfo struct {
|
||||||
|
Mac string
|
||||||
|
IP string
|
||||||
|
Hostname string
|
||||||
|
}
|
||||||
|
|
||||||
|
// LeaseFileFormat specifies the format of DHCP lease file.
|
||||||
|
type LeaseFileFormat string
|
||||||
|
|
||||||
|
const (
|
||||||
|
Dnsmasq LeaseFileFormat = "dnsmasq"
|
||||||
|
IscDhcpd LeaseFileFormat = "isc-dhcpd"
|
||||||
|
)
|
||||||
+1702
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_writeConfigFile(t *testing.T) {
|
||||||
|
tmpdir := t.TempDir()
|
||||||
|
// simulate --config CLI flag by setting configPath manually.
|
||||||
|
configPath = filepath.Join(tmpdir, "ctrld.toml")
|
||||||
|
_, err := os.Stat(configPath)
|
||||||
|
assert.True(t, os.IsNotExist(err))
|
||||||
|
|
||||||
|
assert.NoError(t, writeConfigFile())
|
||||||
|
|
||||||
|
_, err = os.Stat(configPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// logConn wraps a net.Conn, override the Write behavior.
|
||||||
|
// runCmd uses this wrapper, so as long as startCmd finished,
|
||||||
|
// ctrld log won't be flushed with un-necessary write errors.
|
||||||
|
type logConn struct {
|
||||||
|
conn net.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lc *logConn) Read(b []byte) (n int, err error) {
|
||||||
|
return lc.conn.Read(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lc *logConn) Close() error {
|
||||||
|
return lc.conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lc *logConn) LocalAddr() net.Addr {
|
||||||
|
return lc.conn.LocalAddr()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lc *logConn) RemoteAddr() net.Addr {
|
||||||
|
return lc.conn.RemoteAddr()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lc *logConn) SetDeadline(t time.Time) error {
|
||||||
|
return lc.conn.SetDeadline(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lc *logConn) SetReadDeadline(t time.Time) error {
|
||||||
|
return lc.conn.SetReadDeadline(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lc *logConn) SetWriteDeadline(t time.Time) error {
|
||||||
|
return lc.conn.SetWriteDeadline(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lc *logConn) Write(b []byte) (int, error) {
|
||||||
|
// Write performs writes with underlying net.Conn, ignore any errors happen.
|
||||||
|
// "ctrld run" command use this wrapper to report errors to "ctrld start".
|
||||||
|
// If no error occurred, "ctrld start" may finish before "ctrld run" attempt
|
||||||
|
// to close the connection, so ignore errors conservatively here, prevent
|
||||||
|
// un-necessary error "write to closed connection" flushed to ctrld log.
|
||||||
|
_, _ = lc.conn.Write(b)
|
||||||
|
return len(b), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type controlClient struct {
|
||||||
|
c *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func newControlClient(addr string) *controlClient {
|
||||||
|
return &controlClient{c: &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||||
|
d := net.Dialer{}
|
||||||
|
return d.DialContext(ctx, "unix", addr)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Timeout: time.Second * 30,
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *controlClient) post(path string, data io.Reader) (*http.Response, error) {
|
||||||
|
return c.c.Post("http://unix"+path, contentTypeJson, data)
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
contentTypeJson = "application/json"
|
||||||
|
listClientsPath = "/clients"
|
||||||
|
startedPath = "/started"
|
||||||
|
)
|
||||||
|
|
||||||
|
type controlServer struct {
|
||||||
|
server *http.Server
|
||||||
|
mux *http.ServeMux
|
||||||
|
addr string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newControlServer(addr string) (*controlServer, error) {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
s := &controlServer{
|
||||||
|
server: &http.Server{Handler: mux},
|
||||||
|
mux: mux,
|
||||||
|
}
|
||||||
|
s.addr = addr
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *controlServer) start() error {
|
||||||
|
_ = os.Remove(s.addr)
|
||||||
|
unixListener, err := net.Listen("unix", s.addr)
|
||||||
|
if l, ok := unixListener.(*net.UnixListener); ok {
|
||||||
|
l.SetUnlinkOnClose(true)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
go s.server.Serve(unixListener)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *controlServer) stop() error {
|
||||||
|
_ = os.Remove(s.addr)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
|
||||||
|
defer cancel()
|
||||||
|
return s.server.Shutdown(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *controlServer) register(pattern string, handler http.Handler) {
|
||||||
|
s.mux.Handle(pattern, jsonResponse(handler))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *prog) registerControlServerHandler() {
|
||||||
|
p.cs.register(listClientsPath, http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
|
||||||
|
clients := p.ciTable.ListClients()
|
||||||
|
sort.Slice(clients, func(i, j int) bool {
|
||||||
|
return clients[i].IP.Less(clients[j].IP)
|
||||||
|
})
|
||||||
|
if err := json.NewEncoder(w).Encode(&clients); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
p.cs.register(startedPath, http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
|
||||||
|
select {
|
||||||
|
case <-p.onStartedDone:
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
case <-time.After(10 * time.Second):
|
||||||
|
w.WriteHeader(http.StatusRequestTimeout)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func jsonResponse(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestControlServer(t *testing.T) {
|
||||||
|
f, err := os.CreateTemp("", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.Remove(f.Name())
|
||||||
|
f.Close()
|
||||||
|
|
||||||
|
s, err := newControlServer(f.Name())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
pattern := "/ping"
|
||||||
|
respBody := []byte("pong")
|
||||||
|
s.register(pattern, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
_, _ = w.Write(respBody)
|
||||||
|
}))
|
||||||
|
if err := s.start(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := newControlClient(f.Name())
|
||||||
|
resp, err := c.post(pattern, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
t.Fatalf("unepxected response code: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
if ct := resp.Header.Get("content-type"); ct != contentTypeJson {
|
||||||
|
t.Fatalf("unexpected content type: %s", ct)
|
||||||
|
}
|
||||||
|
buf, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(buf, respBody) {
|
||||||
|
t.Errorf("unexpected response body, want: %q, got: %q", string(respBody), string(buf))
|
||||||
|
}
|
||||||
|
if err := s.stop(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package cli
|
||||||
|
|
||||||
//lint:ignore U1000 use in os_linux.go
|
//lint:ignore U1000 use in os_linux.go
|
||||||
type getDNS func(iface string) []string
|
type getDNS func(iface string) []string
|
||||||
@@ -0,0 +1,569 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"go4.org/mem"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
"tailscale.com/net/interfaces"
|
||||||
|
"tailscale.com/util/lineread"
|
||||||
|
|
||||||
|
"github.com/Control-D-Inc/ctrld"
|
||||||
|
"github.com/Control-D-Inc/ctrld/internal/dnscache"
|
||||||
|
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
staleTTL = 60 * time.Second
|
||||||
|
// EDNS0_OPTION_MAC is dnsmasq EDNS0 code for adding mac option.
|
||||||
|
// https://thekelleys.org.uk/gitweb/?p=dnsmasq.git;a=blob;f=src/dns-protocol.h;h=76ac66a8c28317e9c121a74ab5fd0e20f6237dc8;hb=HEAD#l81
|
||||||
|
// This is also dns.EDNS0LOCALSTART, but define our own constant here for clarification.
|
||||||
|
EDNS0_OPTION_MAC = 0xFDE9
|
||||||
|
)
|
||||||
|
|
||||||
|
var osUpstreamConfig = &ctrld.UpstreamConfig{
|
||||||
|
Name: "OS resolver",
|
||||||
|
Type: ctrld.ResolverTypeOS,
|
||||||
|
Timeout: 2000,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *prog) serveDNS(listenerNum string) error {
|
||||||
|
listenerConfig := p.cfg.Listener[listenerNum]
|
||||||
|
// make sure ip is allocated
|
||||||
|
if allocErr := p.allocateIP(listenerConfig.IP); allocErr != nil {
|
||||||
|
mainLog.Load().Error().Err(allocErr).Str("ip", listenerConfig.IP).Msg("serveUDP: failed to allocate listen ip")
|
||||||
|
return allocErr
|
||||||
|
}
|
||||||
|
var failoverRcodes []int
|
||||||
|
if listenerConfig.Policy != nil {
|
||||||
|
failoverRcodes = listenerConfig.Policy.FailoverRcodeNumbers
|
||||||
|
}
|
||||||
|
handler := dns.HandlerFunc(func(w dns.ResponseWriter, m *dns.Msg) {
|
||||||
|
p.sema.acquire()
|
||||||
|
defer p.sema.release()
|
||||||
|
q := m.Question[0]
|
||||||
|
domain := canonicalName(q.Name)
|
||||||
|
reqId := requestID()
|
||||||
|
remoteIP, _, _ := net.SplitHostPort(w.RemoteAddr().String())
|
||||||
|
mac := macFromMsg(m)
|
||||||
|
ci := p.getClientInfo(remoteIP, mac)
|
||||||
|
remoteAddr := spoofRemoteAddr(w.RemoteAddr(), ci)
|
||||||
|
fmtSrcToDest := fmtRemoteToLocal(listenerNum, remoteAddr.String(), w.LocalAddr().String())
|
||||||
|
t := time.Now()
|
||||||
|
ctx := context.WithValue(context.Background(), ctrld.ReqIdCtxKey{}, reqId)
|
||||||
|
ctrld.Log(ctx, mainLog.Load().Debug(), "%s received query: %s %s", fmtSrcToDest, dns.TypeToString[q.Qtype], domain)
|
||||||
|
upstreams, matched := p.upstreamFor(ctx, listenerNum, listenerConfig, remoteAddr, domain)
|
||||||
|
var answer *dns.Msg
|
||||||
|
if !matched && listenerConfig.Restricted {
|
||||||
|
answer = new(dns.Msg)
|
||||||
|
answer.SetRcode(m, dns.RcodeRefused)
|
||||||
|
} else {
|
||||||
|
answer = p.proxy(ctx, upstreams, failoverRcodes, m, ci)
|
||||||
|
rtt := time.Since(t)
|
||||||
|
ctrld.Log(ctx, mainLog.Load().Debug(), "received response of %d bytes in %s", answer.Len(), rtt)
|
||||||
|
}
|
||||||
|
if err := w.WriteMsg(answer); err != nil {
|
||||||
|
ctrld.Log(ctx, mainLog.Load().Error().Err(err), "serveUDP: failed to send DNS response to client")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
g, ctx := errgroup.WithContext(context.Background())
|
||||||
|
for _, proto := range []string{"udp", "tcp"} {
|
||||||
|
proto := proto
|
||||||
|
if needLocalIPv6Listener() {
|
||||||
|
g.Go(func() error {
|
||||||
|
s, errCh := runDNSServer(net.JoinHostPort("::1", strconv.Itoa(listenerConfig.Port)), proto, handler)
|
||||||
|
defer s.Shutdown()
|
||||||
|
select {
|
||||||
|
case <-p.stopCh:
|
||||||
|
case <-ctx.Done():
|
||||||
|
case err := <-errCh:
|
||||||
|
// Local ipv6 listener should not terminate ctrld.
|
||||||
|
// It's a workaround for a quirk on Windows.
|
||||||
|
mainLog.Load().Warn().Err(err).Msg("local ipv6 listener failed")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// When we spawn a listener on 127.0.0.1, also spawn listeners on the RFC1918
|
||||||
|
// addresses of the machine. So ctrld could receive queries from LAN clients.
|
||||||
|
if needRFC1918Listeners(listenerConfig) {
|
||||||
|
g.Go(func() error {
|
||||||
|
for _, addr := range rfc1918Addresses() {
|
||||||
|
func() {
|
||||||
|
listenAddr := net.JoinHostPort(addr, strconv.Itoa(listenerConfig.Port))
|
||||||
|
s, errCh := runDNSServer(listenAddr, proto, handler)
|
||||||
|
defer s.Shutdown()
|
||||||
|
select {
|
||||||
|
case <-p.stopCh:
|
||||||
|
case <-ctx.Done():
|
||||||
|
case err := <-errCh:
|
||||||
|
// RFC1918 listener should not terminate ctrld.
|
||||||
|
// It's a workaround for a quirk on system with systemd-resolved.
|
||||||
|
mainLog.Load().Warn().Err(err).Msgf("could not listen on %s: %s", proto, listenAddr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
g.Go(func() error {
|
||||||
|
s, errCh := runDNSServer(dnsListenAddress(listenerConfig), proto, handler)
|
||||||
|
defer s.Shutdown()
|
||||||
|
select {
|
||||||
|
case err := <-errCh:
|
||||||
|
return err
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
p.started <- struct{}{}
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-p.stopCh:
|
||||||
|
case <-ctx.Done():
|
||||||
|
case err := <-errCh:
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return g.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// upstreamFor returns the list of upstreams for resolving the given domain,
|
||||||
|
// matching by policies defined in the listener config. The second return value
|
||||||
|
// reports whether the domain matches the policy.
|
||||||
|
//
|
||||||
|
// Though domain policy has higher priority than network policy, it is still
|
||||||
|
// processed later, because policy logging want to know whether a network rule
|
||||||
|
// is disregarded in favor of the domain level rule.
|
||||||
|
func (p *prog) upstreamFor(ctx context.Context, defaultUpstreamNum string, lc *ctrld.ListenerConfig, addr net.Addr, domain string) ([]string, bool) {
|
||||||
|
upstreams := []string{"upstream." + defaultUpstreamNum}
|
||||||
|
matchedPolicy := "no policy"
|
||||||
|
matchedNetwork := "no network"
|
||||||
|
matchedRule := "no rule"
|
||||||
|
matched := false
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if !matched && lc.Restricted {
|
||||||
|
ctrld.Log(ctx, mainLog.Load().Info(), "query refused, %s does not match any network policy", addr.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if matched {
|
||||||
|
ctrld.Log(ctx, mainLog.Load().Info(), "%s, %s, %s -> %v", matchedPolicy, matchedNetwork, matchedRule, upstreams)
|
||||||
|
} else {
|
||||||
|
ctrld.Log(ctx, mainLog.Load().Info(), "no explicit policy matched, using default routing -> %v", upstreams)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if lc.Policy == nil {
|
||||||
|
return upstreams, false
|
||||||
|
}
|
||||||
|
|
||||||
|
do := func(policyUpstreams []string) {
|
||||||
|
upstreams = append([]string(nil), policyUpstreams...)
|
||||||
|
}
|
||||||
|
|
||||||
|
var networkTargets []string
|
||||||
|
var sourceIP net.IP
|
||||||
|
switch addr := addr.(type) {
|
||||||
|
case *net.UDPAddr:
|
||||||
|
sourceIP = addr.IP
|
||||||
|
case *net.TCPAddr:
|
||||||
|
sourceIP = addr.IP
|
||||||
|
}
|
||||||
|
|
||||||
|
networkRules:
|
||||||
|
for _, rule := range lc.Policy.Networks {
|
||||||
|
for source, targets := range rule {
|
||||||
|
networkNum := strings.TrimPrefix(source, "network.")
|
||||||
|
nc := p.cfg.Network[networkNum]
|
||||||
|
if nc == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, ipNet := range nc.IPNets {
|
||||||
|
if ipNet.Contains(sourceIP) {
|
||||||
|
matchedPolicy = lc.Policy.Name
|
||||||
|
matchedNetwork = source
|
||||||
|
networkTargets = targets
|
||||||
|
matched = true
|
||||||
|
break networkRules
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rule := range lc.Policy.Rules {
|
||||||
|
// There's only one entry per rule, config validation ensures this.
|
||||||
|
for source, targets := range rule {
|
||||||
|
if source == domain || wildcardMatches(source, domain) {
|
||||||
|
matchedPolicy = lc.Policy.Name
|
||||||
|
if len(networkTargets) > 0 {
|
||||||
|
matchedNetwork += " (unenforced)"
|
||||||
|
}
|
||||||
|
matchedRule = source
|
||||||
|
do(targets)
|
||||||
|
matched = true
|
||||||
|
return upstreams, matched
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if matched {
|
||||||
|
do(networkTargets)
|
||||||
|
}
|
||||||
|
|
||||||
|
return upstreams, matched
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *prog) proxy(ctx context.Context, upstreams []string, failoverRcodes []int, msg *dns.Msg, ci *ctrld.ClientInfo) *dns.Msg {
|
||||||
|
var staleAnswer *dns.Msg
|
||||||
|
serveStaleCache := p.cache != nil && p.cfg.Service.CacheServeStale
|
||||||
|
upstreamConfigs := p.upstreamConfigsFromUpstreamNumbers(upstreams)
|
||||||
|
if len(upstreamConfigs) == 0 {
|
||||||
|
upstreamConfigs = []*ctrld.UpstreamConfig{osUpstreamConfig}
|
||||||
|
upstreams = []string{"upstream.os"}
|
||||||
|
}
|
||||||
|
// Inverse query should not be cached: https://www.rfc-editor.org/rfc/rfc1035#section-7.4
|
||||||
|
if p.cache != nil && msg.Question[0].Qtype != dns.TypePTR {
|
||||||
|
for _, upstream := range upstreams {
|
||||||
|
cachedValue := p.cache.Get(dnscache.NewKey(msg, upstream))
|
||||||
|
if cachedValue == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
answer := cachedValue.Msg.Copy()
|
||||||
|
answer.SetRcode(msg, answer.Rcode)
|
||||||
|
now := time.Now()
|
||||||
|
if cachedValue.Expire.After(now) {
|
||||||
|
ctrld.Log(ctx, mainLog.Load().Debug(), "hit cached response")
|
||||||
|
setCachedAnswerTTL(answer, now, cachedValue.Expire)
|
||||||
|
return answer
|
||||||
|
}
|
||||||
|
staleAnswer = answer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolve1 := func(n int, upstreamConfig *ctrld.UpstreamConfig, msg *dns.Msg) (*dns.Msg, error) {
|
||||||
|
ctrld.Log(ctx, mainLog.Load().Debug(), "sending query to %s: %s", upstreams[n], upstreamConfig.Name)
|
||||||
|
dnsResolver, err := ctrld.NewResolver(upstreamConfig)
|
||||||
|
if err != nil {
|
||||||
|
ctrld.Log(ctx, mainLog.Load().Error().Err(err), "failed to create resolver")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resolveCtx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
if upstreamConfig.Timeout > 0 {
|
||||||
|
timeoutCtx, cancel := context.WithTimeout(resolveCtx, time.Millisecond*time.Duration(upstreamConfig.Timeout))
|
||||||
|
defer cancel()
|
||||||
|
resolveCtx = timeoutCtx
|
||||||
|
}
|
||||||
|
return dnsResolver.Resolve(resolveCtx, msg)
|
||||||
|
}
|
||||||
|
resolve := func(n int, upstreamConfig *ctrld.UpstreamConfig, msg *dns.Msg) *dns.Msg {
|
||||||
|
if upstreamConfig.UpstreamSendClientInfo() && ci != nil {
|
||||||
|
ctrld.Log(ctx, mainLog.Load().Debug(), "including client info with the request")
|
||||||
|
ctx = context.WithValue(ctx, ctrld.ClientInfoCtxKey{}, ci)
|
||||||
|
}
|
||||||
|
answer, err := resolve1(n, upstreamConfig, msg)
|
||||||
|
if err != nil {
|
||||||
|
ctrld.Log(ctx, mainLog.Load().Error().Err(err), "failed to resolve query")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return answer
|
||||||
|
}
|
||||||
|
for n, upstreamConfig := range upstreamConfigs {
|
||||||
|
if upstreamConfig == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
answer := resolve(n, upstreamConfig, msg)
|
||||||
|
if answer == nil {
|
||||||
|
if serveStaleCache && staleAnswer != nil {
|
||||||
|
ctrld.Log(ctx, mainLog.Load().Debug(), "serving stale cached response")
|
||||||
|
now := time.Now()
|
||||||
|
setCachedAnswerTTL(staleAnswer, now, now.Add(staleTTL))
|
||||||
|
return staleAnswer
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if answer.Rcode != dns.RcodeSuccess && len(upstreamConfigs) > 1 && containRcode(failoverRcodes, answer.Rcode) {
|
||||||
|
ctrld.Log(ctx, mainLog.Load().Debug(), "failover rcode matched, process to next upstream")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// set compression, as it is not set by default when unpacking
|
||||||
|
answer.Compress = true
|
||||||
|
|
||||||
|
if p.cache != nil {
|
||||||
|
ttl := ttlFromMsg(answer)
|
||||||
|
now := time.Now()
|
||||||
|
expired := now.Add(time.Duration(ttl) * time.Second)
|
||||||
|
if cachedTTL := p.cfg.Service.CacheTTLOverride; cachedTTL > 0 {
|
||||||
|
expired = now.Add(time.Duration(cachedTTL) * time.Second)
|
||||||
|
}
|
||||||
|
setCachedAnswerTTL(answer, now, expired)
|
||||||
|
p.cache.Add(dnscache.NewKey(msg, upstreams[n]), dnscache.NewValue(answer, expired))
|
||||||
|
ctrld.Log(ctx, mainLog.Load().Debug(), "add cached response")
|
||||||
|
}
|
||||||
|
return answer
|
||||||
|
}
|
||||||
|
ctrld.Log(ctx, mainLog.Load().Error(), "all upstreams failed")
|
||||||
|
answer := new(dns.Msg)
|
||||||
|
answer.SetRcode(msg, dns.RcodeServerFailure)
|
||||||
|
return answer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *prog) upstreamConfigsFromUpstreamNumbers(upstreams []string) []*ctrld.UpstreamConfig {
|
||||||
|
upstreamConfigs := make([]*ctrld.UpstreamConfig, 0, len(upstreams))
|
||||||
|
for _, upstream := range upstreams {
|
||||||
|
upstreamNum := strings.TrimPrefix(upstream, "upstream.")
|
||||||
|
upstreamConfigs = append(upstreamConfigs, p.cfg.Upstream[upstreamNum])
|
||||||
|
}
|
||||||
|
return upstreamConfigs
|
||||||
|
}
|
||||||
|
|
||||||
|
// canonicalName returns canonical name from FQDN with "." trimmed.
|
||||||
|
func canonicalName(fqdn string) string {
|
||||||
|
q := strings.TrimSpace(fqdn)
|
||||||
|
q = strings.TrimSuffix(q, ".")
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc4343
|
||||||
|
q = strings.ToLower(q)
|
||||||
|
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
func wildcardMatches(wildcard, domain string) bool {
|
||||||
|
// Wildcard match.
|
||||||
|
wildCardParts := strings.Split(wildcard, "*")
|
||||||
|
if len(wildCardParts) != 2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case len(wildCardParts[0]) > 0 && len(wildCardParts[1]) > 0:
|
||||||
|
// Domain must match both prefix and suffix.
|
||||||
|
return strings.HasPrefix(domain, wildCardParts[0]) && strings.HasSuffix(domain, wildCardParts[1])
|
||||||
|
|
||||||
|
case len(wildCardParts[1]) > 0:
|
||||||
|
// Only suffix must match.
|
||||||
|
return strings.HasSuffix(domain, wildCardParts[1])
|
||||||
|
|
||||||
|
case len(wildCardParts[0]) > 0:
|
||||||
|
// Only prefix must match.
|
||||||
|
return strings.HasPrefix(domain, wildCardParts[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func fmtRemoteToLocal(listenerNum, remote, local string) string {
|
||||||
|
return fmt.Sprintf("%s -> listener.%s: %s:", remote, listenerNum, local)
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestID() string {
|
||||||
|
b := make([]byte, 3) // 6 chars
|
||||||
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func containRcode(rcodes []int, rcode int) bool {
|
||||||
|
for i := range rcodes {
|
||||||
|
if rcodes[i] == rcode {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func setCachedAnswerTTL(answer *dns.Msg, now, expiredTime time.Time) {
|
||||||
|
ttlSecs := expiredTime.Sub(now).Seconds()
|
||||||
|
if ttlSecs < 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ttl := uint32(ttlSecs)
|
||||||
|
for _, rr := range answer.Answer {
|
||||||
|
rr.Header().Ttl = ttl
|
||||||
|
}
|
||||||
|
for _, rr := range answer.Ns {
|
||||||
|
rr.Header().Ttl = ttl
|
||||||
|
}
|
||||||
|
for _, rr := range answer.Extra {
|
||||||
|
if rr.Header().Rrtype != dns.TypeOPT {
|
||||||
|
rr.Header().Ttl = ttl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ttlFromMsg(msg *dns.Msg) uint32 {
|
||||||
|
for _, rr := range msg.Answer {
|
||||||
|
return rr.Header().Ttl
|
||||||
|
}
|
||||||
|
for _, rr := range msg.Ns {
|
||||||
|
return rr.Header().Ttl
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func needLocalIPv6Listener() bool {
|
||||||
|
// On Windows, there's no easy way for disabling/removing IPv6 DNS resolver, so we check whether we can
|
||||||
|
// listen on ::1, then spawn a listener for receiving DNS requests.
|
||||||
|
return ctrldnet.SupportsIPv6ListenLocal() && runtime.GOOS == "windows"
|
||||||
|
}
|
||||||
|
|
||||||
|
func dnsListenAddress(lc *ctrld.ListenerConfig) string {
|
||||||
|
// If we are inside container and the listener loopback address, change
|
||||||
|
// the address to something like 0.0.0.0:53, so user can expose the port to outside.
|
||||||
|
if inContainer() {
|
||||||
|
if ip := net.ParseIP(lc.IP); ip != nil && ip.IsLoopback() {
|
||||||
|
return net.JoinHostPort("0.0.0.0", strconv.Itoa(lc.Port))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return net.JoinHostPort(lc.IP, strconv.Itoa(lc.Port))
|
||||||
|
}
|
||||||
|
|
||||||
|
func macFromMsg(msg *dns.Msg) string {
|
||||||
|
if opt := msg.IsEdns0(); opt != nil {
|
||||||
|
for _, s := range opt.Option {
|
||||||
|
switch e := s.(type) {
|
||||||
|
case *dns.EDNS0_LOCAL:
|
||||||
|
if e.Code == EDNS0_OPTION_MAC {
|
||||||
|
return net.HardwareAddr(e.Data).String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func spoofRemoteAddr(addr net.Addr, ci *ctrld.ClientInfo) net.Addr {
|
||||||
|
if ci != nil && ci.IP != "" {
|
||||||
|
switch addr := addr.(type) {
|
||||||
|
case *net.UDPAddr:
|
||||||
|
udpAddr := &net.UDPAddr{
|
||||||
|
IP: net.ParseIP(ci.IP),
|
||||||
|
Port: addr.Port,
|
||||||
|
Zone: addr.Zone,
|
||||||
|
}
|
||||||
|
return udpAddr
|
||||||
|
case *net.TCPAddr:
|
||||||
|
udpAddr := &net.TCPAddr{
|
||||||
|
IP: net.ParseIP(ci.IP),
|
||||||
|
Port: addr.Port,
|
||||||
|
Zone: addr.Zone,
|
||||||
|
}
|
||||||
|
return udpAddr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return addr
|
||||||
|
}
|
||||||
|
|
||||||
|
// runDNSServer starts a DNS server for given address and network,
|
||||||
|
// with the given handler. It ensures the server has started listening.
|
||||||
|
// Any error will be reported to the caller via returned channel.
|
||||||
|
//
|
||||||
|
// It's the caller responsibility to call Shutdown to close the server.
|
||||||
|
func runDNSServer(addr, network string, handler dns.Handler) (*dns.Server, <-chan error) {
|
||||||
|
s := &dns.Server{
|
||||||
|
Addr: addr,
|
||||||
|
Net: network,
|
||||||
|
Handler: handler,
|
||||||
|
}
|
||||||
|
|
||||||
|
waitLock := sync.Mutex{}
|
||||||
|
waitLock.Lock()
|
||||||
|
s.NotifyStartedFunc = waitLock.Unlock
|
||||||
|
|
||||||
|
errCh := make(chan error)
|
||||||
|
go func() {
|
||||||
|
defer close(errCh)
|
||||||
|
if err := s.ListenAndServe(); err != nil {
|
||||||
|
waitLock.Unlock()
|
||||||
|
mainLog.Load().Error().Err(err).Msgf("could not listen and serve on: %s", s.Addr)
|
||||||
|
errCh <- err
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
waitLock.Lock()
|
||||||
|
return s, errCh
|
||||||
|
}
|
||||||
|
|
||||||
|
// inContainer reports whether we're running in a container.
|
||||||
|
//
|
||||||
|
// Copied from https://github.com/tailscale/tailscale/blob/v1.42.0/hostinfo/hostinfo.go#L260
|
||||||
|
// with modification for ctrld usage.
|
||||||
|
func inContainer() bool {
|
||||||
|
if runtime.GOOS != "linux" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var ret bool
|
||||||
|
if _, err := os.Stat("/.dockerenv"); err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if _, err := os.Stat("/run/.containerenv"); err == nil {
|
||||||
|
// See https://github.com/cri-o/cri-o/issues/5461
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
lineread.File("/proc/1/cgroup", func(line []byte) error {
|
||||||
|
if mem.Contains(mem.B(line), mem.S("/docker/")) ||
|
||||||
|
mem.Contains(mem.B(line), mem.S("/lxc/")) {
|
||||||
|
ret = true
|
||||||
|
return io.EOF // arbitrary non-nil error to stop loop
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
lineread.File("/proc/mounts", func(line []byte) error {
|
||||||
|
if mem.Contains(mem.B(line), mem.S("lxcfs /proc/cpuinfo fuse.lxcfs")) {
|
||||||
|
ret = true
|
||||||
|
return io.EOF
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *prog) getClientInfo(ip, mac string) *ctrld.ClientInfo {
|
||||||
|
ci := &ctrld.ClientInfo{}
|
||||||
|
if mac != "" {
|
||||||
|
ci.Mac = mac
|
||||||
|
ci.IP = p.ciTable.LookupIP(mac)
|
||||||
|
} else {
|
||||||
|
ci.IP = ip
|
||||||
|
ci.Mac = p.ciTable.LookupMac(ip)
|
||||||
|
if ip == "127.0.0.1" || ip == "::1" {
|
||||||
|
ci.IP = p.ciTable.LookupIP(ci.Mac)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ci.Hostname = p.ciTable.LookupHostname(ci.IP, ci.Mac)
|
||||||
|
return ci
|
||||||
|
}
|
||||||
|
|
||||||
|
func needRFC1918Listeners(lc *ctrld.ListenerConfig) bool {
|
||||||
|
return lc.IP == "127.0.0.1" && lc.Port == 53
|
||||||
|
}
|
||||||
|
|
||||||
|
func rfc1918Addresses() []string {
|
||||||
|
var res []string
|
||||||
|
interfaces.ForeachInterface(func(i interfaces.Interface, prefixes []netip.Prefix) {
|
||||||
|
addrs, _ := i.Addrs()
|
||||||
|
for _, addr := range addrs {
|
||||||
|
ipNet, ok := addr.(*net.IPNet)
|
||||||
|
if !ok || !ipNet.IP.IsPrivate() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
res = append(res, ipNet.IP.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return res
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -86,17 +86,17 @@ func Test_prog_upstreamFor(t *testing.T) {
|
|||||||
domain string
|
domain string
|
||||||
upstreams []string
|
upstreams []string
|
||||||
matched bool
|
matched bool
|
||||||
|
testLogMsg string
|
||||||
}{
|
}{
|
||||||
{"Policy map matches", "192.168.0.1:0", "0", prog.cfg.Listener["0"], "abc.xyz", []string{"upstream.1", "upstream.0"}, true},
|
{"Policy map matches", "192.168.0.1:0", "0", prog.cfg.Listener["0"], "abc.xyz", []string{"upstream.1", "upstream.0"}, true, ""},
|
||||||
{"Policy split matches", "192.168.0.1:0", "0", prog.cfg.Listener["0"], "abc.ru", []string{"upstream.1"}, true},
|
{"Policy split matches", "192.168.0.1:0", "0", prog.cfg.Listener["0"], "abc.ru", []string{"upstream.1"}, true, ""},
|
||||||
{"Policy map for other network matches", "192.168.1.2:0", "0", prog.cfg.Listener["0"], "abc.xyz", []string{"upstream.0"}, true},
|
{"Policy map for other network matches", "192.168.1.2:0", "0", prog.cfg.Listener["0"], "abc.xyz", []string{"upstream.0"}, true, ""},
|
||||||
{"No policy map for listener", "192.168.1.2:0", "1", prog.cfg.Listener["1"], "abc.ru", []string{"upstream.1"}, false},
|
{"No policy map for listener", "192.168.1.2:0", "1", prog.cfg.Listener["1"], "abc.ru", []string{"upstream.1"}, false, ""},
|
||||||
|
{"unenforced loging", "192.168.1.2:0", "0", prog.cfg.Listener["0"], "abc.ru", []string{"upstream.1"}, true, "My Policy, network.1 (unenforced), *.ru -> [upstream.1]"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
for _, network := range []string{"udp", "tcp"} {
|
for _, network := range []string{"udp", "tcp"} {
|
||||||
var (
|
var (
|
||||||
addr net.Addr
|
addr net.Addr
|
||||||
@@ -114,6 +114,9 @@ func Test_prog_upstreamFor(t *testing.T) {
|
|||||||
upstreams, matched := prog.upstreamFor(ctx, tc.defaultUpstreamNum, tc.lc, addr, tc.domain)
|
upstreams, matched := prog.upstreamFor(ctx, tc.defaultUpstreamNum, tc.lc, addr, tc.domain)
|
||||||
assert.Equal(t, tc.matched, matched)
|
assert.Equal(t, tc.matched, matched)
|
||||||
assert.Equal(t, tc.upstreams, upstreams)
|
assert.Equal(t, tc.upstreams, upstreams)
|
||||||
|
if tc.testLogMsg != "" {
|
||||||
|
assert.Contains(t, logOutput.String(), tc.testLogMsg)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -146,9 +149,70 @@ func TestCache(t *testing.T) {
|
|||||||
answer2.SetRcode(msg, dns.RcodeRefused)
|
answer2.SetRcode(msg, dns.RcodeRefused)
|
||||||
prog.cache.Add(dnscache.NewKey(msg, "upstream.0"), dnscache.NewValue(answer2, time.Now().Add(time.Minute)))
|
prog.cache.Add(dnscache.NewKey(msg, "upstream.0"), dnscache.NewValue(answer2, time.Now().Add(time.Minute)))
|
||||||
|
|
||||||
got1 := prog.proxy(context.Background(), []string{"upstream.1"}, nil, msg)
|
got1 := prog.proxy(context.Background(), []string{"upstream.1"}, nil, msg, nil)
|
||||||
got2 := prog.proxy(context.Background(), []string{"upstream.0"}, nil, msg)
|
got2 := prog.proxy(context.Background(), []string{"upstream.0"}, nil, msg, nil)
|
||||||
assert.NotSame(t, got1, got2)
|
assert.NotSame(t, got1, got2)
|
||||||
assert.Equal(t, answer1.Rcode, got1.Rcode)
|
assert.Equal(t, answer1.Rcode, got1.Rcode)
|
||||||
assert.Equal(t, answer2.Rcode, got2.Rcode)
|
assert.Equal(t, answer2.Rcode, got2.Rcode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_macFromMsg(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
mac string
|
||||||
|
wantMac bool
|
||||||
|
}{
|
||||||
|
{"has mac", "4c:20:b8:ab:87:1b", true},
|
||||||
|
{"no mac", "4c:20:b8:ab:87:1b", false},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
hw, err := net.ParseMAC(tc.mac)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetQuestion("example.com.", dns.TypeA)
|
||||||
|
o := &dns.OPT{Hdr: dns.RR_Header{Name: ".", Rrtype: dns.TypeOPT}}
|
||||||
|
if tc.wantMac {
|
||||||
|
ec1 := &dns.EDNS0_LOCAL{Code: EDNS0_OPTION_MAC, Data: hw}
|
||||||
|
o.Option = append(o.Option, ec1)
|
||||||
|
}
|
||||||
|
m.Extra = append(m.Extra, o)
|
||||||
|
got := macFromMsg(m)
|
||||||
|
if tc.wantMac && got != tc.mac {
|
||||||
|
t.Errorf("mismatch, want: %q, got: %q", tc.mac, got)
|
||||||
|
}
|
||||||
|
if !tc.wantMac && got != "" {
|
||||||
|
t.Errorf("unexpected mac: %q", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_remoteAddrFromMsg(t *testing.T) {
|
||||||
|
loopbackIP := net.ParseIP("127.0.0.1")
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
addr net.Addr
|
||||||
|
ci *ctrld.ClientInfo
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"tcp", &net.TCPAddr{IP: loopbackIP, Port: 12345}, &ctrld.ClientInfo{IP: "192.168.1.10"}, "192.168.1.10:12345"},
|
||||||
|
{"udp", &net.UDPAddr{IP: loopbackIP, Port: 12345}, &ctrld.ClientInfo{IP: "192.168.1.11"}, "192.168.1.11:12345"},
|
||||||
|
{"nil client info", &net.UDPAddr{IP: loopbackIP, Port: 12345}, nil, "127.0.0.1:12345"},
|
||||||
|
{"empty ip", &net.UDPAddr{IP: loopbackIP, Port: 12345}, &ctrld.ClientInfo{}, "127.0.0.1:12345"},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
addr := spoofRemoteAddr(tc.addr, tc.ci)
|
||||||
|
if addr.String() != tc.want {
|
||||||
|
t.Errorf("unexpected result, want: %q, got: %q", tc.want, addr.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+161
@@ -0,0 +1,161 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kardianos/service"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
|
||||||
|
"github.com/Control-D-Inc/ctrld"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
configPath string
|
||||||
|
configBase64 string
|
||||||
|
daemon bool
|
||||||
|
listenAddress string
|
||||||
|
primaryUpstream string
|
||||||
|
secondaryUpstream string
|
||||||
|
domains []string
|
||||||
|
logPath string
|
||||||
|
homedir string
|
||||||
|
cacheSize int
|
||||||
|
cfg ctrld.Config
|
||||||
|
verbose int
|
||||||
|
silent bool
|
||||||
|
cdUID string
|
||||||
|
cdOrg string
|
||||||
|
cdDev bool
|
||||||
|
iface string
|
||||||
|
ifaceStartStop string
|
||||||
|
|
||||||
|
mainLog atomic.Pointer[zerolog.Logger]
|
||||||
|
consoleWriter zerolog.ConsoleWriter
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
l := zerolog.New(io.Discard)
|
||||||
|
mainLog.Store(&l)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Main() {
|
||||||
|
ctrld.InitConfig(v, "ctrld")
|
||||||
|
initCLI()
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
mainLog.Load().Error().Msg(err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeLogFilePath(logFilePath string) string {
|
||||||
|
if logFilePath == "" || filepath.IsAbs(logFilePath) || service.Interactive() {
|
||||||
|
return logFilePath
|
||||||
|
}
|
||||||
|
if homedir != "" {
|
||||||
|
return filepath.Join(homedir, logFilePath)
|
||||||
|
}
|
||||||
|
dir, _ := userHomeDir()
|
||||||
|
if dir == "" {
|
||||||
|
return logFilePath
|
||||||
|
}
|
||||||
|
return filepath.Join(dir, logFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func initConsoleLogging() {
|
||||||
|
consoleWriter = zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) {
|
||||||
|
w.TimeFormat = time.StampMilli
|
||||||
|
})
|
||||||
|
multi := zerolog.MultiLevelWriter(consoleWriter)
|
||||||
|
l := mainLog.Load().Output(multi).With().Timestamp().Logger()
|
||||||
|
mainLog.Store(&l)
|
||||||
|
switch {
|
||||||
|
case silent:
|
||||||
|
zerolog.SetGlobalLevel(zerolog.NoLevel)
|
||||||
|
case verbose == 1:
|
||||||
|
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||||
|
case verbose > 1:
|
||||||
|
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||||
|
default:
|
||||||
|
zerolog.SetGlobalLevel(zerolog.NoticeLevel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// initLogging initializes global logging setup.
|
||||||
|
func initLogging() {
|
||||||
|
initLoggingWithBackup(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// initLoggingWithBackup initializes log setup base on current config.
|
||||||
|
// If doBackup is true, backup old log file with ".1" suffix.
|
||||||
|
//
|
||||||
|
// This is only used in runCmd for special handling in case of logging config
|
||||||
|
// change in cd mode. Without special reason, the caller should use initLogging
|
||||||
|
// wrapper instead of calling this function directly.
|
||||||
|
func initLoggingWithBackup(doBackup bool) {
|
||||||
|
writers := []io.Writer{io.Discard}
|
||||||
|
if logFilePath := normalizeLogFilePath(cfg.Service.LogPath); logFilePath != "" {
|
||||||
|
// Create parent directory if necessary.
|
||||||
|
if err := os.MkdirAll(filepath.Dir(logFilePath), 0750); err != nil {
|
||||||
|
mainLog.Load().Error().Msgf("failed to create log path: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default open log file in append mode.
|
||||||
|
flags := os.O_CREATE | os.O_RDWR | os.O_APPEND
|
||||||
|
if doBackup {
|
||||||
|
// Backup old log file with .1 suffix.
|
||||||
|
if err := os.Rename(logFilePath, logFilePath+".1"); err != nil && !os.IsNotExist(err) {
|
||||||
|
mainLog.Load().Error().Msgf("could not backup old log file: %v", err)
|
||||||
|
} else {
|
||||||
|
// Backup was created, set flags for truncating old log file.
|
||||||
|
flags = os.O_CREATE | os.O_RDWR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logFile, err := os.OpenFile(logFilePath, flags, os.FileMode(0o600))
|
||||||
|
if err != nil {
|
||||||
|
mainLog.Load().Error().Msgf("failed to create log file: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
writers = append(writers, logFile)
|
||||||
|
}
|
||||||
|
writers = append(writers, consoleWriter)
|
||||||
|
multi := zerolog.MultiLevelWriter(writers...)
|
||||||
|
l := mainLog.Load().Output(multi).With().Timestamp().Logger()
|
||||||
|
mainLog.Store(&l)
|
||||||
|
// TODO: find a better way.
|
||||||
|
ctrld.ProxyLogger.Store(&l)
|
||||||
|
|
||||||
|
zerolog.SetGlobalLevel(zerolog.NoticeLevel)
|
||||||
|
logLevel := cfg.Service.LogLevel
|
||||||
|
switch {
|
||||||
|
case silent:
|
||||||
|
zerolog.SetGlobalLevel(zerolog.NoLevel)
|
||||||
|
return
|
||||||
|
case verbose == 1:
|
||||||
|
logLevel = "info"
|
||||||
|
case verbose > 1:
|
||||||
|
logLevel = "debug"
|
||||||
|
}
|
||||||
|
if logLevel == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
level, err := zerolog.ParseLevel(logLevel)
|
||||||
|
if err != nil {
|
||||||
|
mainLog.Load().Warn().Err(err).Msg("could not set log level")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
zerolog.SetGlobalLevel(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
func initCache() {
|
||||||
|
if !cfg.Service.CacheEnable {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if cfg.Service.CacheSize == 0 {
|
||||||
|
cfg.Service.CacheSize = 4096
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
|
var logOutput strings.Builder
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
l := zerolog.New(&logOutput)
|
||||||
|
mainLog.Store(&l)
|
||||||
|
os.Exit(m.Run())
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func patchNetIfaceName(iface *net.Interface) error {
|
||||||
|
b, err := exec.Command("networksetup", "-listnetworkserviceorder").Output()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if name := networkServiceName(iface.Name, bytes.NewReader(b)); name != "" {
|
||||||
|
iface.Name = name
|
||||||
|
mainLog.Load().Debug().Str("network_service", name).Msg("found network service name for interface")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func networkServiceName(ifaceName string, r io.Reader) string {
|
||||||
|
scanner := bufio.NewScanner(r)
|
||||||
|
prevLine := ""
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
if strings.Contains(line, "*") {
|
||||||
|
// Network services is disabled.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !strings.Contains(line, "Device: "+ifaceName) {
|
||||||
|
prevLine = line
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(prevLine, " ", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
return strings.TrimSpace(parts[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
const listnetworkserviceorderOutput = `
|
||||||
|
(1) USB 10/100/1000 LAN 2
|
||||||
|
(Hardware Port: USB 10/100/1000 LAN, Device: en7)
|
||||||
|
|
||||||
|
(2) Ethernet
|
||||||
|
(Hardware Port: Ethernet, Device: en0)
|
||||||
|
|
||||||
|
(3) Wi-Fi
|
||||||
|
(Hardware Port: Wi-Fi, Device: en1)
|
||||||
|
|
||||||
|
(4) Bluetooth PAN
|
||||||
|
(Hardware Port: Bluetooth PAN, Device: en4)
|
||||||
|
|
||||||
|
(5) Thunderbolt Bridge
|
||||||
|
(Hardware Port: Thunderbolt Bridge, Device: bridge0)
|
||||||
|
|
||||||
|
(6) kernal
|
||||||
|
(Hardware Port: com.wireguard.macos, Device: )
|
||||||
|
|
||||||
|
(7) WS BT
|
||||||
|
(Hardware Port: com.wireguard.macos, Device: )
|
||||||
|
|
||||||
|
(8) ca-001-stg
|
||||||
|
(Hardware Port: com.wireguard.macos, Device: )
|
||||||
|
|
||||||
|
(9) ca-001-stg-2
|
||||||
|
(Hardware Port: com.wireguard.macos, Device: )
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
func Test_networkServiceName(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
ifaceName string
|
||||||
|
networkServiceName string
|
||||||
|
}{
|
||||||
|
{"en7", "USB 10/100/1000 LAN 2"},
|
||||||
|
{"en0", "Ethernet"},
|
||||||
|
{"en1", "Wi-Fi"},
|
||||||
|
{"en4", "Bluetooth PAN"},
|
||||||
|
{"bridge0", "Thunderbolt Bridge"},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.ifaceName, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
name := networkServiceName(tc.ifaceName, strings.NewReader(listnetworkserviceorderOutput))
|
||||||
|
assert.Equal(t, tc.networkServiceName, name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
//go:build !darwin
|
//go:build !darwin
|
||||||
|
|
||||||
package main
|
package cli
|
||||||
|
|
||||||
import "net"
|
import "net"
|
||||||
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vishvananda/netlink"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *prog) watchLinkState() {
|
||||||
|
ch := make(chan netlink.LinkUpdate)
|
||||||
|
done := make(chan struct{})
|
||||||
|
defer close(done)
|
||||||
|
if err := netlink.LinkSubscribe(ch, done); err != nil {
|
||||||
|
mainLog.Load().Warn().Err(err).Msg("could not subscribe link")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for lu := range ch {
|
||||||
|
if lu.Change == 0xFFFFFFFF {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if lu.Change&unix.IFF_UP != 0 {
|
||||||
|
mainLog.Load().Debug().Msgf("link state changed, re-bootstrapping")
|
||||||
|
for _, uc := range p.cfg.Upstream {
|
||||||
|
uc.ReBootstrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
//go:build !linux
|
||||||
|
|
||||||
|
package cli
|
||||||
|
|
||||||
|
func (p *prog) watchLinkState() {}
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
package main
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coreos/go-systemd/v22/dbus"
|
"github.com/coreos/go-systemd/v22/dbus"
|
||||||
@@ -24,46 +23,38 @@ systemd-resolved=false
|
|||||||
var networkManagerCtrldConfFile = filepath.Join(nmConfDir, nmCtrldConfFilename)
|
var networkManagerCtrldConfFile = filepath.Join(nmConfDir, nmCtrldConfFilename)
|
||||||
|
|
||||||
func setupNetworkManager() error {
|
func setupNetworkManager() error {
|
||||||
if runtime.GOOS != "linux" {
|
|
||||||
mainLog.Debug().Msg("skipping NetworkManager setup, not on Linux")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if content, _ := os.ReadFile(nmCtrldConfContent); string(content) == nmCtrldConfContent {
|
if content, _ := os.ReadFile(nmCtrldConfContent); string(content) == nmCtrldConfContent {
|
||||||
mainLog.Debug().Msg("NetworkManager already setup, nothing to do")
|
mainLog.Load().Debug().Msg("NetworkManager already setup, nothing to do")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
err := os.WriteFile(networkManagerCtrldConfFile, []byte(nmCtrldConfContent), os.FileMode(0644))
|
err := os.WriteFile(networkManagerCtrldConfFile, []byte(nmCtrldConfContent), os.FileMode(0644))
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
mainLog.Debug().Msg("NetworkManager is not available")
|
mainLog.Load().Debug().Msg("NetworkManager is not available")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mainLog.Debug().Err(err).Msg("could not write NetworkManager ctrld config file")
|
mainLog.Load().Debug().Err(err).Msg("could not write NetworkManager ctrld config file")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
reloadNetworkManager()
|
reloadNetworkManager()
|
||||||
mainLog.Debug().Msg("setup NetworkManager done")
|
mainLog.Load().Debug().Msg("setup NetworkManager done")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func restoreNetworkManager() error {
|
func restoreNetworkManager() error {
|
||||||
if runtime.GOOS != "linux" {
|
|
||||||
mainLog.Debug().Msg("skipping NetworkManager restoring, not on Linux")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
err := os.Remove(networkManagerCtrldConfFile)
|
err := os.Remove(networkManagerCtrldConfFile)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
mainLog.Debug().Msg("NetworkManager is not available")
|
mainLog.Load().Debug().Msg("NetworkManager is not available")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mainLog.Debug().Err(err).Msg("could not remove NetworkManager ctrld config file")
|
mainLog.Load().Debug().Err(err).Msg("could not remove NetworkManager ctrld config file")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
reloadNetworkManager()
|
reloadNetworkManager()
|
||||||
mainLog.Debug().Msg("restore NetworkManager done")
|
mainLog.Load().Debug().Msg("restore NetworkManager done")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,14 +63,14 @@ func reloadNetworkManager() {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
conn, err := dbus.NewSystemConnectionContext(ctx)
|
conn, err := dbus.NewSystemConnectionContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mainLog.Error().Err(err).Msg("could not create new system connection")
|
mainLog.Load().Error().Err(err).Msg("could not create new system connection")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
waitCh := make(chan string)
|
waitCh := make(chan string)
|
||||||
if _, err := conn.ReloadUnitContext(ctx, nmSystemdUnitName, "ignore-dependencies", waitCh); err != nil {
|
if _, err := conn.ReloadUnitContext(ctx, nmSystemdUnitName, "ignore-dependencies", waitCh); err != nil {
|
||||||
mainLog.Debug().Err(err).Msg("could not reload NetworkManager")
|
mainLog.Load().Debug().Err(err).Msg("could not reload NetworkManager")
|
||||||
}
|
}
|
||||||
<-waitCh
|
<-waitCh
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
//go:build !linux
|
||||||
|
|
||||||
|
package cli
|
||||||
|
|
||||||
|
func setupNetworkManager() error {
|
||||||
|
reloadNetworkManager()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func restoreNetworkManager() error {
|
||||||
|
reloadNetworkManager()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func reloadNetworkManager() {}
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
//go:build darwin
|
package cli
|
||||||
// +build darwin
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
@@ -15,7 +12,7 @@ import (
|
|||||||
func allocateIP(ip string) error {
|
func allocateIP(ip string) error {
|
||||||
cmd := exec.Command("ifconfig", "lo0", "alias", ip, "up")
|
cmd := exec.Command("ifconfig", "lo0", "alias", ip, "up")
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
mainLog.Error().Err(err).Msg("allocateIP failed")
|
mainLog.Load().Error().Err(err).Msg("allocateIP failed")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -24,7 +21,7 @@ func allocateIP(ip string) error {
|
|||||||
func deAllocateIP(ip string) error {
|
func deAllocateIP(ip string) error {
|
||||||
cmd := exec.Command("ifconfig", "lo0", "-alias", ip)
|
cmd := exec.Command("ifconfig", "lo0", "-alias", ip)
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
mainLog.Error().Err(err).Msg("deAllocateIP failed")
|
mainLog.Load().Error().Err(err).Msg("deAllocateIP failed")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -39,7 +36,7 @@ func setDNS(iface *net.Interface, nameservers []string) error {
|
|||||||
args = append(args, nameservers...)
|
args = append(args, nameservers...)
|
||||||
|
|
||||||
if err := exec.Command(cmd, args...).Run(); err != nil {
|
if err := exec.Command(cmd, args...).Run(); err != nil {
|
||||||
mainLog.Error().Err(err).Msgf("setDNS failed, ips = %q", nameservers)
|
mainLog.Load().Error().Err(err).Msgf("setDNS failed, ips = %q", nameservers)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -51,7 +48,7 @@ func resetDNS(iface *net.Interface) error {
|
|||||||
args := []string{"-setdnsservers", iface.Name, "empty"}
|
args := []string{"-setdnsservers", iface.Name, "empty"}
|
||||||
|
|
||||||
if err := exec.Command(cmd, args...).Run(); err != nil {
|
if err := exec.Command(cmd, args...).Run(); err != nil {
|
||||||
mainLog.Error().Err(err).Msgf("resetDNS failed")
|
mainLog.Load().Error().Err(err).Msgf("resetDNS failed")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/Control-D-Inc/ctrld/internal/dns"
|
||||||
|
"github.com/Control-D-Inc/ctrld/internal/resolvconffile"
|
||||||
|
)
|
||||||
|
|
||||||
|
// allocate loopback ip
|
||||||
|
// sudo ifconfig lo0 127.0.0.53 alias
|
||||||
|
func allocateIP(ip string) error {
|
||||||
|
cmd := exec.Command("ifconfig", "lo0", ip, "alias")
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
mainLog.Load().Error().Err(err).Msg("allocateIP failed")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func deAllocateIP(ip string) error {
|
||||||
|
cmd := exec.Command("ifconfig", "lo0", ip, "-alias")
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
mainLog.Load().Error().Err(err).Msg("deAllocateIP failed")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the dns server for the provided network interface
|
||||||
|
func setDNS(iface *net.Interface, nameservers []string) error {
|
||||||
|
r, err := dns.NewOSConfigurator(logf, iface.Name)
|
||||||
|
if err != nil {
|
||||||
|
mainLog.Load().Error().Err(err).Msg("failed to create DNS OS configurator")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ns := make([]netip.Addr, 0, len(nameservers))
|
||||||
|
for _, nameserver := range nameservers {
|
||||||
|
ns = append(ns, netip.MustParseAddr(nameserver))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.SetDNS(dns.OSConfig{Nameservers: ns}); err != nil {
|
||||||
|
mainLog.Load().Error().Err(err).Msg("failed to set DNS")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetDNS(iface *net.Interface) error {
|
||||||
|
r, err := dns.NewOSConfigurator(logf, iface.Name)
|
||||||
|
if err != nil {
|
||||||
|
mainLog.Load().Error().Err(err).Msg("failed to create DNS OS configurator")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.Close(); err != nil {
|
||||||
|
mainLog.Load().Error().Err(err).Msg("failed to rollback DNS setting")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func currentDNS(_ *net.Interface) []string {
|
||||||
|
return resolvconffile.NameServers("")
|
||||||
|
}
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"os/exec"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/insomniacslk/dhcp/dhcpv4/nclient4"
|
||||||
|
"github.com/insomniacslk/dhcp/dhcpv6"
|
||||||
|
"github.com/insomniacslk/dhcp/dhcpv6/client6"
|
||||||
|
"tailscale.com/util/dnsname"
|
||||||
|
|
||||||
|
"github.com/Control-D-Inc/ctrld/internal/dns"
|
||||||
|
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
||||||
|
"github.com/Control-D-Inc/ctrld/internal/resolvconffile"
|
||||||
|
)
|
||||||
|
|
||||||
|
const resolvConfBackupFailedMsg = "open /etc/resolv.pre-ctrld-backup.conf: read-only file system"
|
||||||
|
|
||||||
|
// allocate loopback ip
|
||||||
|
// sudo ip a add 127.0.0.2/24 dev lo
|
||||||
|
func allocateIP(ip string) error {
|
||||||
|
cmd := exec.Command("ip", "a", "add", ip+"/24", "dev", "lo")
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
mainLog.Load().Error().Err(err).Msgf("allocateIP failed: %s", string(out))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func deAllocateIP(ip string) error {
|
||||||
|
cmd := exec.Command("ip", "a", "del", ip+"/24", "dev", "lo")
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
mainLog.Load().Error().Err(err).Msg("deAllocateIP failed")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxSetDNSAttempts = 5
|
||||||
|
|
||||||
|
// set the dns server for the provided network interface
|
||||||
|
func setDNS(iface *net.Interface, nameservers []string) error {
|
||||||
|
r, err := dns.NewOSConfigurator(logf, iface.Name)
|
||||||
|
if err != nil {
|
||||||
|
mainLog.Load().Error().Err(err).Msg("failed to create DNS OS configurator")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ns := make([]netip.Addr, 0, len(nameservers))
|
||||||
|
for _, nameserver := range nameservers {
|
||||||
|
ns = append(ns, netip.MustParseAddr(nameserver))
|
||||||
|
}
|
||||||
|
|
||||||
|
osConfig := dns.OSConfig{
|
||||||
|
Nameservers: ns,
|
||||||
|
SearchDomains: []dnsname.FQDN{},
|
||||||
|
}
|
||||||
|
|
||||||
|
trySystemdResolve := false
|
||||||
|
for i := 0; i < maxSetDNSAttempts; i++ {
|
||||||
|
if err := r.SetDNS(osConfig); err != nil {
|
||||||
|
if strings.Contains(err.Error(), "Rejected send message") &&
|
||||||
|
strings.Contains(err.Error(), "org.freedesktop.network1.Manager") {
|
||||||
|
mainLog.Load().Warn().Msg("Interfaces are managed by systemd-networkd, switch to systemd-resolve for setting DNS")
|
||||||
|
trySystemdResolve = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// This error happens on read-only file system, which causes ctrld failed to create backup
|
||||||
|
// for /etc/resolv.conf file. It is ok, because the DNS is still set anyway, and restore
|
||||||
|
// DNS will fallback to use DHCP if there's no backup /etc/resolv.conf file.
|
||||||
|
// The error format is controlled by us, so checking for error string is fine.
|
||||||
|
// See: ../../internal/dns/direct.go:L278
|
||||||
|
if r.Mode() == "direct" && strings.Contains(err.Error(), resolvConfBackupFailedMsg) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
currentNS := currentDNS(iface)
|
||||||
|
if reflect.DeepEqual(currentNS, nameservers) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if trySystemdResolve {
|
||||||
|
// Stop systemd-networkd and retry setting DNS.
|
||||||
|
if out, err := exec.Command("systemctl", "stop", "systemd-networkd").CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", string(out), err)
|
||||||
|
}
|
||||||
|
args := []string{"--interface=" + iface.Name, "--set-domain=~"}
|
||||||
|
for _, nameserver := range nameservers {
|
||||||
|
args = append(args, "--set-dns="+nameserver)
|
||||||
|
}
|
||||||
|
for i := 0; i < maxSetDNSAttempts; i++ {
|
||||||
|
if out, err := exec.Command("systemd-resolve", args...).CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", string(out), err)
|
||||||
|
}
|
||||||
|
currentNS := currentDNS(iface)
|
||||||
|
if reflect.DeepEqual(currentNS, nameservers) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mainLog.Load().Debug().Msg("DNS was not set for some reason")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetDNS(iface *net.Interface) (err error) {
|
||||||
|
defer func() {
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Start systemd-networkd if present.
|
||||||
|
if exe, _ := exec.LookPath("/lib/systemd/systemd-networkd"); exe != "" {
|
||||||
|
_ = exec.Command("systemctl", "start", "systemd-networkd").Run()
|
||||||
|
}
|
||||||
|
if r, oerr := dns.NewOSConfigurator(logf, iface.Name); oerr == nil {
|
||||||
|
_ = r.SetDNS(dns.OSConfig{})
|
||||||
|
if err := r.Close(); err != nil {
|
||||||
|
mainLog.Load().Error().Err(err).Msg("failed to rollback DNS setting")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
var ns []string
|
||||||
|
c, err := nclient4.New(iface.Name)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("nclient4.New: %w", err)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||||
|
defer cancel()
|
||||||
|
lease, err := c.Request(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("nclient4.Request: %w", err)
|
||||||
|
}
|
||||||
|
for _, nameserver := range lease.ACK.DNS() {
|
||||||
|
if nameserver.Equal(net.IPv4zero) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ns = append(ns, nameserver.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(cuonglm): handle DHCPv6 properly.
|
||||||
|
if ctrldnet.IPv6Available(ctx) {
|
||||||
|
c := client6.NewClient()
|
||||||
|
conversation, err := c.Exchange(iface.Name)
|
||||||
|
if err != nil && !errAddrInUse(err) {
|
||||||
|
mainLog.Load().Debug().Err(err).Msg("could not exchange DHCPv6")
|
||||||
|
}
|
||||||
|
for _, packet := range conversation {
|
||||||
|
if packet.Type() == dhcpv6.MessageTypeReply {
|
||||||
|
msg, err := packet.GetInnerMessage()
|
||||||
|
if err != nil {
|
||||||
|
mainLog.Load().Debug().Err(err).Msg("could not get inner DHCPv6 message")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
nameservers := msg.Options.DNS()
|
||||||
|
for _, nameserver := range nameservers {
|
||||||
|
ns = append(ns, nameserver.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ignoringEINTR(func() error {
|
||||||
|
return setDNS(iface, ns)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func currentDNS(iface *net.Interface) []string {
|
||||||
|
for _, fn := range []getDNS{getDNSByResolvectl, getDNSBySystemdResolved, getDNSByNmcli, resolvconffile.NameServers} {
|
||||||
|
if ns := fn(iface.Name); len(ns) > 0 {
|
||||||
|
return ns
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDNSByResolvectl(iface string) []string {
|
||||||
|
b, err := exec.Command("resolvectl", "dns", "-i", iface).Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Fields(strings.SplitN(string(b), "%", 2)[0])
|
||||||
|
if len(parts) > 2 {
|
||||||
|
return parts[3:]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDNSBySystemdResolved(iface string) []string {
|
||||||
|
b, err := exec.Command("systemd-resolve", "--status", iface).Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return getDNSBySystemdResolvedFromReader(bytes.NewReader(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDNSBySystemdResolvedFromReader(r io.Reader) []string {
|
||||||
|
scanner := bufio.NewScanner(r)
|
||||||
|
var ret []string
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if len(ret) > 0 {
|
||||||
|
if net.ParseIP(line) != nil {
|
||||||
|
ret = append(ret, line)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
after, found := strings.CutPrefix(line, "DNS Servers: ")
|
||||||
|
if !found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if net.ParseIP(after) != nil {
|
||||||
|
ret = append(ret, after)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDNSByNmcli(iface string) []string {
|
||||||
|
b, err := exec.Command("nmcli", "dev", "show", iface).Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
s := bufio.NewScanner(bytes.NewReader(b))
|
||||||
|
var dns []string
|
||||||
|
do := func(line string) {
|
||||||
|
parts := strings.SplitN(line, ":", 2)
|
||||||
|
if len(parts) > 1 {
|
||||||
|
dns = append(dns, strings.TrimSpace(parts[1]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for s.Scan() {
|
||||||
|
line := s.Text()
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(line, "IP4.DNS"):
|
||||||
|
fallthrough
|
||||||
|
case strings.HasPrefix(line, "IP6.DNS"):
|
||||||
|
do(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dns
|
||||||
|
}
|
||||||
|
|
||||||
|
func ignoringEINTR(fn func() error) error {
|
||||||
|
for {
|
||||||
|
err := fn()
|
||||||
|
if err != syscall.EINTR {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_getDNSBySystemdResolvedFromReader(t *testing.T) {
|
||||||
|
r := strings.NewReader(`Link 2 (eth0)
|
||||||
|
Current Scopes: DNS
|
||||||
|
LLMNR setting: yes
|
||||||
|
MulticastDNS setting: no
|
||||||
|
DNSSEC setting: no
|
||||||
|
DNSSEC supported: no
|
||||||
|
DNS Servers: 8.8.8.8
|
||||||
|
8.8.4.4`)
|
||||||
|
want := []string{"8.8.8.8", "8.8.4.4"}
|
||||||
|
ns := getDNSBySystemdResolvedFromReader(r)
|
||||||
|
if !reflect.DeepEqual(ns, want) {
|
||||||
|
t.Logf("unexpected result, want: %v, got: %v", want, ns)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
//go:build !linux && !darwin && !freebsd
|
||||||
|
|
||||||
|
package cli
|
||||||
|
|
||||||
|
// TODO(cuonglm): implement.
|
||||||
|
func allocateIP(ip string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(cuonglm): implement.
|
||||||
|
func deAllocateIP(ip string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
//go:build windows
|
package cli
|
||||||
// +build windows
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@@ -10,18 +7,10 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||||
|
|
||||||
|
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO(cuonglm): implement.
|
|
||||||
func allocateIP(ip string) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(cuonglm): implement.
|
|
||||||
func deAllocateIP(ip string) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func setDNS(iface *net.Interface, nameservers []string) error {
|
func setDNS(iface *net.Interface, nameservers []string) error {
|
||||||
if len(nameservers) == 0 {
|
if len(nameservers) == 0 {
|
||||||
return errors.New("empty DNS nameservers")
|
return errors.New("empty DNS nameservers")
|
||||||
@@ -39,14 +28,14 @@ func setDNS(iface *net.Interface, nameservers []string) error {
|
|||||||
|
|
||||||
// TODO(cuonglm): should we use system API?
|
// TODO(cuonglm): should we use system API?
|
||||||
func resetDNS(iface *net.Interface) error {
|
func resetDNS(iface *net.Interface) error {
|
||||||
if supportsIPv6ListenLocal() {
|
if ctrldnet.SupportsIPv6ListenLocal() {
|
||||||
if output, err := netsh("interface", "ipv6", "set", "dnsserver", strconv.Itoa(iface.Index), "dhcp"); err != nil {
|
if output, err := netsh("interface", "ipv6", "set", "dnsserver", strconv.Itoa(iface.Index), "dhcp"); err != nil {
|
||||||
mainLog.Warn().Err(err).Msgf("failed to reset ipv6 DNS: %s", string(output))
|
mainLog.Load().Warn().Err(err).Msgf("failed to reset ipv6 DNS: %s", string(output))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
output, err := netsh("interface", "ipv4", "set", "dnsserver", strconv.Itoa(iface.Index), "dhcp")
|
output, err := netsh("interface", "ipv4", "set", "dnsserver", strconv.Itoa(iface.Index), "dhcp")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mainLog.Error().Err(err).Msgf("failed to reset ipv4 DNS: %s", string(output))
|
mainLog.Load().Error().Err(err).Msgf("failed to reset ipv4 DNS: %s", string(output))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -54,16 +43,16 @@ func resetDNS(iface *net.Interface) error {
|
|||||||
|
|
||||||
func setPrimaryDNS(iface *net.Interface, dns string) error {
|
func setPrimaryDNS(iface *net.Interface, dns string) error {
|
||||||
ipVer := "ipv4"
|
ipVer := "ipv4"
|
||||||
if isIPv6(dns) {
|
if ctrldnet.IsIPv6(dns) {
|
||||||
ipVer = "ipv6"
|
ipVer = "ipv6"
|
||||||
}
|
}
|
||||||
idx := strconv.Itoa(iface.Index)
|
idx := strconv.Itoa(iface.Index)
|
||||||
output, err := netsh("interface", ipVer, "set", "dnsserver", idx, "static", dns)
|
output, err := netsh("interface", ipVer, "set", "dnsserver", idx, "static", dns)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mainLog.Error().Err(err).Msgf("failed to set primary DNS: %s", string(output))
|
mainLog.Load().Error().Err(err).Msgf("failed to set primary DNS: %s", string(output))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if ipVer == "ipv4" {
|
if ipVer == "ipv4" && ctrldnet.SupportsIPv6ListenLocal() {
|
||||||
// Disable IPv6 DNS, so the query will be fallback to IPv4.
|
// Disable IPv6 DNS, so the query will be fallback to IPv4.
|
||||||
_, _ = netsh("interface", "ipv6", "set", "dnsserver", idx, "static", "::1", "primary")
|
_, _ = netsh("interface", "ipv6", "set", "dnsserver", idx, "static", "::1", "primary")
|
||||||
}
|
}
|
||||||
@@ -73,12 +62,12 @@ func setPrimaryDNS(iface *net.Interface, dns string) error {
|
|||||||
|
|
||||||
func addSecondaryDNS(iface *net.Interface, dns string) error {
|
func addSecondaryDNS(iface *net.Interface, dns string) error {
|
||||||
ipVer := "ipv4"
|
ipVer := "ipv4"
|
||||||
if isIPv6(dns) {
|
if ctrldnet.IsIPv6(dns) {
|
||||||
ipVer = "ipv6"
|
ipVer = "ipv6"
|
||||||
}
|
}
|
||||||
output, err := netsh("interface", ipVer, "add", "dns", strconv.Itoa(iface.Index), dns, "index=2")
|
output, err := netsh("interface", ipVer, "add", "dns", strconv.Itoa(iface.Index), dns, "index=2")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mainLog.Warn().Err(err).Msgf("failed to add secondary DNS: %s", string(output))
|
mainLog.Load().Warn().Err(err).Msgf("failed to add secondary DNS: %s", string(output))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -90,12 +79,12 @@ func netsh(args ...string) ([]byte, error) {
|
|||||||
func currentDNS(iface *net.Interface) []string {
|
func currentDNS(iface *net.Interface) []string {
|
||||||
luid, err := winipcfg.LUIDFromIndex(uint32(iface.Index))
|
luid, err := winipcfg.LUIDFromIndex(uint32(iface.Index))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mainLog.Error().Err(err).Msg("failed to get interface LUID")
|
mainLog.Load().Error().Err(err).Msg("failed to get interface LUID")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
nameservers, err := luid.DNS()
|
nameservers, err := luid.DNS()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mainLog.Error().Err(err).Msg("failed to get interface DNS")
|
mainLog.Load().Error().Err(err).Msg("failed to get interface DNS")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
ns := make([]string, 0, len(nameservers))
|
ns := make([]string, 0, len(nameservers))
|
||||||
+392
@@ -0,0 +1,392 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/kardianos/service"
|
||||||
|
"tailscale.com/net/interfaces"
|
||||||
|
|
||||||
|
"github.com/Control-D-Inc/ctrld"
|
||||||
|
"github.com/Control-D-Inc/ctrld/internal/clientinfo"
|
||||||
|
"github.com/Control-D-Inc/ctrld/internal/dnscache"
|
||||||
|
"github.com/Control-D-Inc/ctrld/internal/router"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultSemaphoreCap = 256
|
||||||
|
ctrldLogUnixSock = "ctrld_start.sock"
|
||||||
|
ctrldControlUnixSock = "ctrld_control.sock"
|
||||||
|
)
|
||||||
|
|
||||||
|
var logf = func(format string, args ...any) {
|
||||||
|
mainLog.Load().Debug().Msgf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
var svcConfig = &service.Config{
|
||||||
|
Name: "ctrld",
|
||||||
|
DisplayName: "Control-D Helper Service",
|
||||||
|
Option: service.KeyValue{},
|
||||||
|
}
|
||||||
|
|
||||||
|
var useSystemdResolved = false
|
||||||
|
|
||||||
|
type prog struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
waitCh chan struct{}
|
||||||
|
stopCh chan struct{}
|
||||||
|
logConn net.Conn
|
||||||
|
cs *controlServer
|
||||||
|
|
||||||
|
cfg *ctrld.Config
|
||||||
|
cache dnscache.Cacher
|
||||||
|
sema semaphore
|
||||||
|
ciTable *clientinfo.Table
|
||||||
|
router router.Router
|
||||||
|
|
||||||
|
started chan struct{}
|
||||||
|
onStartedDone chan struct{}
|
||||||
|
onStarted []func()
|
||||||
|
onStopped []func()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *prog) Start(s service.Service) error {
|
||||||
|
p.cfg = &cfg
|
||||||
|
go p.run()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *prog) preRun() {
|
||||||
|
if !service.Interactive() {
|
||||||
|
p.setDNS()
|
||||||
|
}
|
||||||
|
if runtime.GOOS == "darwin" {
|
||||||
|
p.onStopped = append(p.onStopped, func() {
|
||||||
|
if !service.Interactive() {
|
||||||
|
p.resetDNS()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *prog) run() {
|
||||||
|
// Wait the caller to signal that we can do our logic.
|
||||||
|
<-p.waitCh
|
||||||
|
p.preRun()
|
||||||
|
numListeners := len(p.cfg.Listener)
|
||||||
|
p.started = make(chan struct{}, numListeners)
|
||||||
|
p.onStartedDone = make(chan struct{})
|
||||||
|
if p.cfg.Service.CacheEnable {
|
||||||
|
cacher, err := dnscache.NewLRUCache(p.cfg.Service.CacheSize)
|
||||||
|
if err != nil {
|
||||||
|
mainLog.Load().Error().Err(err).Msg("failed to create cacher, caching is disabled")
|
||||||
|
} else {
|
||||||
|
p.cache = cacher
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.sema = &chanSemaphore{ready: make(chan struct{}, defaultSemaphoreCap)}
|
||||||
|
if mcr := p.cfg.Service.MaxConcurrentRequests; mcr != nil {
|
||||||
|
n := *mcr
|
||||||
|
if n == 0 {
|
||||||
|
p.sema = &noopSemaphore{}
|
||||||
|
} else {
|
||||||
|
p.sema = &chanSemaphore{ready: make(chan struct{}, n)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(len(p.cfg.Listener))
|
||||||
|
|
||||||
|
for _, nc := range p.cfg.Network {
|
||||||
|
for _, cidr := range nc.Cidrs {
|
||||||
|
_, ipNet, err := net.ParseCIDR(cidr)
|
||||||
|
if err != nil {
|
||||||
|
mainLog.Load().Error().Err(err).Str("network", nc.Name).Str("cidr", cidr).Msg("invalid cidr")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
nc.IPNets = append(nc.IPNets, ipNet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for n := range p.cfg.Upstream {
|
||||||
|
uc := p.cfg.Upstream[n]
|
||||||
|
uc.Init()
|
||||||
|
if uc.BootstrapIP == "" {
|
||||||
|
uc.SetupBootstrapIP()
|
||||||
|
mainLog.Load().Info().Msgf("bootstrap IPs for upstream.%s: %q", n, uc.BootstrapIPs())
|
||||||
|
} else {
|
||||||
|
mainLog.Load().Info().Str("bootstrap_ip", uc.BootstrapIP).Msgf("using bootstrap IP for upstream.%s", n)
|
||||||
|
}
|
||||||
|
uc.SetCertPool(rootCertPool)
|
||||||
|
go uc.Ping()
|
||||||
|
}
|
||||||
|
|
||||||
|
p.ciTable = clientinfo.NewTable(&cfg, defaultRouteIP(), cdUID)
|
||||||
|
if leaseFile := p.cfg.Service.DHCPLeaseFile; leaseFile != "" {
|
||||||
|
mainLog.Load().Debug().Msgf("watching custom lease file: %s", leaseFile)
|
||||||
|
format := ctrld.LeaseFileFormat(p.cfg.Service.DHCPLeaseFileFormat)
|
||||||
|
p.ciTable.AddLeaseFile(leaseFile, format)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
p.ciTable.Init()
|
||||||
|
p.ciTable.RefreshLoop(p.stopCh)
|
||||||
|
}()
|
||||||
|
go p.watchLinkState()
|
||||||
|
|
||||||
|
for listenerNum := range p.cfg.Listener {
|
||||||
|
p.cfg.Listener[listenerNum].Init()
|
||||||
|
go func(listenerNum string) {
|
||||||
|
defer wg.Done()
|
||||||
|
listenerConfig := p.cfg.Listener[listenerNum]
|
||||||
|
upstreamConfig := p.cfg.Upstream[listenerNum]
|
||||||
|
if upstreamConfig == nil {
|
||||||
|
mainLog.Load().Warn().Msgf("no default upstream for: [listener.%s]", listenerNum)
|
||||||
|
}
|
||||||
|
addr := net.JoinHostPort(listenerConfig.IP, strconv.Itoa(listenerConfig.Port))
|
||||||
|
mainLog.Load().Info().Msgf("starting DNS server on listener.%s: %s", listenerNum, addr)
|
||||||
|
if err := p.serveDNS(listenerNum); err != nil {
|
||||||
|
mainLog.Load().Fatal().Err(err).Msgf("unable to start dns proxy on listener.%s", listenerNum)
|
||||||
|
}
|
||||||
|
}(listenerNum)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < numListeners; i++ {
|
||||||
|
<-p.started
|
||||||
|
}
|
||||||
|
for _, f := range p.onStarted {
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
close(p.onStartedDone)
|
||||||
|
|
||||||
|
// Stop writing log to unix socket.
|
||||||
|
consoleWriter.Out = os.Stdout
|
||||||
|
initLoggingWithBackup(false)
|
||||||
|
if p.logConn != nil {
|
||||||
|
_ = p.logConn.Close()
|
||||||
|
}
|
||||||
|
if p.cs != nil {
|
||||||
|
p.registerControlServerHandler()
|
||||||
|
if err := p.cs.start(); err != nil {
|
||||||
|
mainLog.Load().Warn().Err(err).Msg("could not start control server")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *prog) Stop(s service.Service) error {
|
||||||
|
mainLog.Load().Info().Msg("Service stopped")
|
||||||
|
close(p.stopCh)
|
||||||
|
if err := p.deAllocateIP(); err != nil {
|
||||||
|
mainLog.Load().Error().Err(err).Msg("de-allocate ip failed")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *prog) allocateIP(ip string) error {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
if !p.cfg.Service.AllocateIP {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return allocateIP(ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *prog) deAllocateIP() error {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
if !p.cfg.Service.AllocateIP {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, lc := range p.cfg.Listener {
|
||||||
|
if err := deAllocateIP(lc.IP); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *prog) setDNS() {
|
||||||
|
if cfg.Listener == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if iface == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if iface == "auto" {
|
||||||
|
iface = defaultIfaceName()
|
||||||
|
}
|
||||||
|
lc := cfg.FirstListener()
|
||||||
|
if lc == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger := mainLog.Load().With().Str("iface", iface).Logger()
|
||||||
|
netIface, err := netInterface(iface)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error().Err(err).Msg("could not get interface")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := setupNetworkManager(); err != nil {
|
||||||
|
logger.Error().Err(err).Msg("could not patch NetworkManager")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug().Msg("setting DNS for interface")
|
||||||
|
ns := lc.IP
|
||||||
|
switch {
|
||||||
|
case lc.IsDirectDnsListener():
|
||||||
|
// If ctrld is direct listener, use 127.0.0.1 as nameserver.
|
||||||
|
ns = "127.0.0.1"
|
||||||
|
case lc.Port != 53:
|
||||||
|
ns = "127.0.0.1"
|
||||||
|
if resolver := router.LocalResolverIP(); resolver != "" {
|
||||||
|
ns = resolver
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// If we ever reach here, it means ctrld is running on lc.IP port 53,
|
||||||
|
// so we could just use lc.IP as nameserver.
|
||||||
|
}
|
||||||
|
|
||||||
|
nameservers := []string{ns}
|
||||||
|
if needRFC1918Listeners(lc) {
|
||||||
|
nameservers = append(nameservers, rfc1918Addresses()...)
|
||||||
|
}
|
||||||
|
if err := setDNS(netIface, nameservers); err != nil {
|
||||||
|
logger.Error().Err(err).Msgf("could not set DNS for interface")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Debug().Msg("setting DNS successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *prog) resetDNS() {
|
||||||
|
if iface == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if iface == "auto" {
|
||||||
|
iface = defaultIfaceName()
|
||||||
|
}
|
||||||
|
logger := mainLog.Load().With().Str("iface", iface).Logger()
|
||||||
|
netIface, err := netInterface(iface)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error().Err(err).Msg("could not get interface")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := restoreNetworkManager(); err != nil {
|
||||||
|
logger.Error().Err(err).Msg("could not restore NetworkManager")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Debug().Msg("Restoring DNS for interface")
|
||||||
|
if err := resetDNS(netIface); err != nil {
|
||||||
|
logger.Error().Err(err).Msgf("could not reset DNS")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Debug().Msg("Restoring DNS successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
func randomLocalIP() string {
|
||||||
|
n := rand.Intn(254-2) + 2
|
||||||
|
return fmt.Sprintf("127.0.0.%d", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func randomPort() int {
|
||||||
|
max := 1<<16 - 1
|
||||||
|
min := 1025
|
||||||
|
n := rand.Intn(max-min) + min
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// runLogServer starts a unix listener, use by startCmd to gather log from runCmd.
|
||||||
|
func runLogServer(sockPath string) net.Conn {
|
||||||
|
addr, err := net.ResolveUnixAddr("unix", sockPath)
|
||||||
|
if err != nil {
|
||||||
|
mainLog.Load().Warn().Err(err).Msg("invalid log sock path")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ln, err := net.ListenUnix("unix", addr)
|
||||||
|
if err != nil {
|
||||||
|
mainLog.Load().Warn().Err(err).Msg("could not listen log socket")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer ln.Close()
|
||||||
|
|
||||||
|
server, err := ln.Accept()
|
||||||
|
if err != nil {
|
||||||
|
mainLog.Load().Warn().Err(err).Msg("could not accept connection")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return server
|
||||||
|
}
|
||||||
|
|
||||||
|
func errAddrInUse(err error) bool {
|
||||||
|
var opErr *net.OpError
|
||||||
|
if errors.As(err, &opErr) {
|
||||||
|
return errors.Is(opErr.Err, syscall.EADDRINUSE) || errors.Is(opErr.Err, windowsEADDRINUSE)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = errAddrInUse
|
||||||
|
|
||||||
|
// https://learn.microsoft.com/en-us/windows/win32/winsock/windows-sockets-error-codes-2
|
||||||
|
var (
|
||||||
|
windowsECONNREFUSED = syscall.Errno(10061)
|
||||||
|
windowsENETUNREACH = syscall.Errno(10051)
|
||||||
|
windowsEINVAL = syscall.Errno(10022)
|
||||||
|
windowsEADDRINUSE = syscall.Errno(10048)
|
||||||
|
)
|
||||||
|
|
||||||
|
func errUrlNetworkError(err error) bool {
|
||||||
|
var urlErr *url.Error
|
||||||
|
if errors.As(err, &urlErr) {
|
||||||
|
var opErr *net.OpError
|
||||||
|
if errors.As(urlErr.Err, &opErr) {
|
||||||
|
if opErr.Temporary() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case errors.Is(opErr.Err, syscall.ECONNREFUSED),
|
||||||
|
errors.Is(opErr.Err, syscall.EINVAL),
|
||||||
|
errors.Is(opErr.Err, syscall.ENETUNREACH),
|
||||||
|
errors.Is(opErr.Err, windowsENETUNREACH),
|
||||||
|
errors.Is(opErr.Err, windowsEINVAL),
|
||||||
|
errors.Is(opErr.Err, windowsECONNREFUSED):
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultRouteIP returns IP string of the default route if present, prefer IPv4 over IPv6.
|
||||||
|
func defaultRouteIP() string {
|
||||||
|
if dr, err := interfaces.DefaultRoute(); err == nil {
|
||||||
|
if netIface, err := netInterface(dr.InterfaceName); err == nil {
|
||||||
|
addrs, _ := netIface.Addrs()
|
||||||
|
do := func(v4 bool) net.IP {
|
||||||
|
for _, addr := range addrs {
|
||||||
|
if netIP, ok := addr.(*net.IPNet); ok && netIP.IP.IsPrivate() {
|
||||||
|
if v4 {
|
||||||
|
return netIP.IP.To4()
|
||||||
|
}
|
||||||
|
return netIP.IP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if ip := do(true); ip != nil {
|
||||||
|
return ip.String()
|
||||||
|
}
|
||||||
|
if ip := do(false); ip != nil {
|
||||||
|
return ip.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/kardianos/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setDependencies(svc *service.Config) {}
|
||||||
|
|
||||||
|
func setWorkingDirectory(svc *service.Config, dir string) {
|
||||||
|
svc.WorkingDirectory = dir
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/kardianos/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setDependencies(svc *service.Config) {
|
||||||
|
// TODO(cuonglm): remove once https://github.com/kardianos/service/issues/359 fixed.
|
||||||
|
_ = os.MkdirAll("/usr/local/etc/rc.d", 0755)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setWorkingDirectory(svc *service.Config, dir string) {}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/kardianos/service"
|
||||||
|
|
||||||
|
"github.com/Control-D-Inc/ctrld/internal/dns"
|
||||||
|
"github.com/Control-D-Inc/ctrld/internal/router"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if r, err := dns.NewOSConfigurator(func(format string, args ...any) {}, "lo"); err == nil {
|
||||||
|
useSystemdResolved = r.Mode() == "systemd-resolved"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setDependencies(svc *service.Config) {
|
||||||
|
svc.Dependencies = []string{
|
||||||
|
"Wants=network-online.target",
|
||||||
|
"After=network-online.target",
|
||||||
|
"Wants=NetworkManager-wait-online.service",
|
||||||
|
"After=NetworkManager-wait-online.service",
|
||||||
|
"Wants=systemd-networkd-wait-online.service",
|
||||||
|
"After=systemd-networkd-wait-online.service",
|
||||||
|
}
|
||||||
|
if routerDeps := router.ServiceDependencies(); len(routerDeps) > 0 {
|
||||||
|
svc.Dependencies = append(svc.Dependencies, routerDeps...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setWorkingDirectory(svc *service.Config, dir string) {
|
||||||
|
svc.WorkingDirectory = dir
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
//go:build !linux && !freebsd && !darwin
|
||||||
|
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import "github.com/kardianos/service"
|
||||||
|
|
||||||
|
func setDependencies(svc *service.Config) {}
|
||||||
|
|
||||||
|
func setWorkingDirectory(svc *service.Config, dir string) {
|
||||||
|
// WorkingDirectory is not supported on Windows.
|
||||||
|
svc.WorkingDirectory = dir
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
type semaphore interface {
|
||||||
|
acquire()
|
||||||
|
release()
|
||||||
|
}
|
||||||
|
|
||||||
|
type noopSemaphore struct{}
|
||||||
|
|
||||||
|
func (n noopSemaphore) acquire() {}
|
||||||
|
|
||||||
|
func (n noopSemaphore) release() {}
|
||||||
|
|
||||||
|
type chanSemaphore struct {
|
||||||
|
ready chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *chanSemaphore) acquire() {
|
||||||
|
c.ready <- struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *chanSemaphore) release() {
|
||||||
|
<-c.ready
|
||||||
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/kardianos/service"
|
||||||
|
|
||||||
|
"github.com/Control-D-Inc/ctrld/internal/router"
|
||||||
|
)
|
||||||
|
|
||||||
|
// newService wraps service.New call to return service.Service
|
||||||
|
// wrapper which is suitable for the current platform.
|
||||||
|
func newService(i service.Interface, c *service.Config) (service.Service, error) {
|
||||||
|
s, err := service.New(i, c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case router.IsOldOpenwrt():
|
||||||
|
return &procd{&sysV{s}}, nil
|
||||||
|
case router.IsGLiNet():
|
||||||
|
return &sysV{s}, nil
|
||||||
|
case s.Platform() == "unix-systemv":
|
||||||
|
return &sysV{s}, nil
|
||||||
|
case s.Platform() == "linux-systemd":
|
||||||
|
return &systemd{s}, nil
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sysV wraps a service.Service, and provide start/stop/status command
|
||||||
|
// base on "/etc/init.d/<service_name>".
|
||||||
|
//
|
||||||
|
// Use this on system where "service" command is not available, like GL.iNET router.
|
||||||
|
type sysV struct {
|
||||||
|
service.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sysV) installed() bool {
|
||||||
|
fi, err := os.Stat("/etc/init.d/ctrld")
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
mode := fi.Mode()
|
||||||
|
return mode.IsRegular() && (mode&0111) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sysV) Start() error {
|
||||||
|
if !s.installed() {
|
||||||
|
return service.ErrNotInstalled
|
||||||
|
}
|
||||||
|
_, err := exec.Command("/etc/init.d/ctrld", "start").CombinedOutput()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sysV) Stop() error {
|
||||||
|
if !s.installed() {
|
||||||
|
return service.ErrNotInstalled
|
||||||
|
}
|
||||||
|
_, err := exec.Command("/etc/init.d/ctrld", "stop").CombinedOutput()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sysV) Restart() error {
|
||||||
|
if !s.installed() {
|
||||||
|
return service.ErrNotInstalled
|
||||||
|
}
|
||||||
|
// We don't care about error returned by s.Stop,
|
||||||
|
// because the service may already be stopped.
|
||||||
|
_ = s.Stop()
|
||||||
|
return s.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sysV) Status() (service.Status, error) {
|
||||||
|
if !s.installed() {
|
||||||
|
return service.StatusUnknown, service.ErrNotInstalled
|
||||||
|
}
|
||||||
|
return unixSystemVServiceStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
// procd wraps a service.Service, and provide start/stop command
|
||||||
|
// base on "/etc/init.d/<service_name>", status command base on parsing "ps" command output.
|
||||||
|
//
|
||||||
|
// Use this on system where "/etc/init.d/<service_name> status" command is not available,
|
||||||
|
// like old GL.iNET Opal router.
|
||||||
|
type procd struct {
|
||||||
|
*sysV
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *procd) Status() (service.Status, error) {
|
||||||
|
if !s.installed() {
|
||||||
|
return service.StatusUnknown, service.ErrNotInstalled
|
||||||
|
}
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return service.StatusUnknown, nil
|
||||||
|
}
|
||||||
|
// Looking for something like "/sbin/ctrld run ".
|
||||||
|
shellCmd := fmt.Sprintf("ps | grep -q %q", exe+" [r]un ")
|
||||||
|
if err := exec.Command("sh", "-c", shellCmd).Run(); err != nil {
|
||||||
|
return service.StatusStopped, nil
|
||||||
|
}
|
||||||
|
return service.StatusRunning, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// procd wraps a service.Service, and provide status command to
|
||||||
|
// report the status correctly.
|
||||||
|
type systemd struct {
|
||||||
|
service.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *systemd) Status() (service.Status, error) {
|
||||||
|
out, _ := exec.Command("systemctl", "status", "ctrld").CombinedOutput()
|
||||||
|
if bytes.Contains(out, []byte("/FAILURE)")) {
|
||||||
|
return service.StatusStopped, nil
|
||||||
|
}
|
||||||
|
return s.Service.Status()
|
||||||
|
}
|
||||||
|
|
||||||
|
type task struct {
|
||||||
|
f func() error
|
||||||
|
abortOnError bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func doTasks(tasks []task) bool {
|
||||||
|
var prevErr error
|
||||||
|
for _, task := range tasks {
|
||||||
|
if err := task.f(); err != nil {
|
||||||
|
if task.abortOnError {
|
||||||
|
mainLog.Load().Error().Msg(errors.Join(prevErr, err).Error())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
prevErr = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkHasElevatedPrivilege() {
|
||||||
|
ok, err := hasElevatedPrivilege()
|
||||||
|
if err != nil {
|
||||||
|
mainLog.Load().Error().Msgf("could not detect user privilege: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
mainLog.Load().Error().Msg("Please relaunch process with admin/root privilege.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func unixSystemVServiceStatus() (service.Status, error) {
|
||||||
|
out, err := exec.Command("/etc/init.d/ctrld", "status").CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return service.StatusUnknown, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch string(bytes.ToLower(bytes.TrimSpace(out))) {
|
||||||
|
case "running":
|
||||||
|
return service.StatusRunning, nil
|
||||||
|
default:
|
||||||
|
return service.StatusStopped, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
//go:build !windows
|
//go:build !windows
|
||||||
|
|
||||||
package main
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package cli
|
||||||
|
|
||||||
import "golang.org/x/sys/windows"
|
import "golang.org/x/sys/windows"
|
||||||
|
|
||||||
@@ -1,668 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
"github.com/kardianos/service"
|
|
||||||
"github.com/pelletier/go-toml/v2"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
"tailscale.com/net/interfaces"
|
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/controld"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
v = viper.NewWithOptions(viper.KeyDelimiter("::"))
|
|
||||||
defaultConfigWritten = false
|
|
||||||
defaultConfigFile = "ctrld.toml"
|
|
||||||
)
|
|
||||||
|
|
||||||
var basicModeFlags = []string{"listen", "primary_upstream", "secondary_upstream", "domains"}
|
|
||||||
|
|
||||||
func isNoConfigStart(cmd *cobra.Command) bool {
|
|
||||||
for _, flagName := range basicModeFlags {
|
|
||||||
if cmd.Flags().Lookup(flagName).Changed {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const rootShortDesc = `
|
|
||||||
__ .__ .___
|
|
||||||
_____/ |________| | __| _/
|
|
||||||
_/ ___\ __\_ __ \ | / __ |
|
|
||||||
\ \___| | | | \/ |__/ /_/ |
|
|
||||||
\___ >__| |__| |____/\____ |
|
|
||||||
\/ dns forwarding proxy \/
|
|
||||||
`
|
|
||||||
|
|
||||||
func initCLI() {
|
|
||||||
// Enable opening via explorer.exe on Windows.
|
|
||||||
// See: https://github.com/spf13/cobra/issues/844.
|
|
||||||
cobra.MousetrapHelpText = ""
|
|
||||||
|
|
||||||
rootCmd := &cobra.Command{
|
|
||||||
Use: "ctrld",
|
|
||||||
Short: strings.TrimLeft(rootShortDesc, "\n"),
|
|
||||||
Version: "1.1.0",
|
|
||||||
}
|
|
||||||
rootCmd.PersistentFlags().CountVarP(
|
|
||||||
&verbose,
|
|
||||||
"verbose",
|
|
||||||
"v",
|
|
||||||
`verbose log output, "-v" basic logging, "-vv" debug level logging`,
|
|
||||||
)
|
|
||||||
rootCmd.SetHelpCommand(&cobra.Command{Hidden: true})
|
|
||||||
rootCmd.CompletionOptions.HiddenDefaultCmd = true
|
|
||||||
|
|
||||||
runCmd := &cobra.Command{
|
|
||||||
Use: "run",
|
|
||||||
Short: "Run the DNS proxy server",
|
|
||||||
Args: cobra.NoArgs,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
if daemon && runtime.GOOS == "windows" {
|
|
||||||
log.Fatal("Cannot run in daemon mode. Please install a Windows service.")
|
|
||||||
}
|
|
||||||
|
|
||||||
noConfigStart := isNoConfigStart(cmd)
|
|
||||||
writeDefaultConfig := !noConfigStart && configBase64 == ""
|
|
||||||
configs := []struct {
|
|
||||||
name string
|
|
||||||
written bool
|
|
||||||
}{
|
|
||||||
// For compatibility, we check for config.toml first, but only read it if exists.
|
|
||||||
{"config", false},
|
|
||||||
{"ctrld", writeDefaultConfig},
|
|
||||||
}
|
|
||||||
for _, config := range configs {
|
|
||||||
ctrld.SetConfigName(v, config.name)
|
|
||||||
v.SetConfigFile(configPath)
|
|
||||||
if readConfigFile(config.written) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readBase64Config()
|
|
||||||
processNoConfigFlags(noConfigStart)
|
|
||||||
if err := v.Unmarshal(&cfg); err != nil {
|
|
||||||
log.Fatalf("failed to unmarshal config: %v", err)
|
|
||||||
}
|
|
||||||
// Wait for network up.
|
|
||||||
if !netUp() {
|
|
||||||
log.Fatal("network is not up yet")
|
|
||||||
}
|
|
||||||
processLogAndCacheFlags()
|
|
||||||
// Log config do not have thing to validate, so it's safe to init log here,
|
|
||||||
// so it's able to log information in processCDFlags.
|
|
||||||
initLogging()
|
|
||||||
processCDFlags()
|
|
||||||
if err := ctrld.ValidateConfig(validator.New(), &cfg); err != nil {
|
|
||||||
log.Fatalf("invalid config: %v", err)
|
|
||||||
}
|
|
||||||
initCache()
|
|
||||||
|
|
||||||
if daemon {
|
|
||||||
exe, err := os.Executable()
|
|
||||||
if err != nil {
|
|
||||||
mainLog.Error().Err(err).Msg("failed to find the binary")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
curDir, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
mainLog.Error().Err(err).Msg("failed to get current working directory")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
// If running as daemon, re-run the command in background, with daemon off.
|
|
||||||
cmd := exec.Command(exe, append(os.Args[1:], "-d=false")...)
|
|
||||||
cmd.Dir = curDir
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
mainLog.Error().Err(err).Msg("failed to start process as daemon")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
mainLog.Info().Int("pid", cmd.Process.Pid).Msg("DNS proxy started")
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
s, err := service.New(&prog{}, svcConfig)
|
|
||||||
if err != nil {
|
|
||||||
mainLog.Fatal().Err(err).Msg("failed create new service")
|
|
||||||
}
|
|
||||||
serviceLogger, err := s.Logger(nil)
|
|
||||||
if err != nil {
|
|
||||||
mainLog.Error().Err(err).Msg("failed to get service logger")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.Run(); err != nil {
|
|
||||||
if sErr := serviceLogger.Error(err); sErr != nil {
|
|
||||||
mainLog.Error().Err(sErr).Msg("failed to write service log")
|
|
||||||
}
|
|
||||||
mainLog.Error().Err(err).Msg("failed to start service")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
runCmd.Flags().BoolVarP(&daemon, "daemon", "d", false, "Run as daemon")
|
|
||||||
runCmd.Flags().StringVarP(&configPath, "config", "c", "", "Path to config file")
|
|
||||||
runCmd.Flags().StringVarP(&configBase64, "base64_config", "", "", "Base64 encoded config")
|
|
||||||
runCmd.Flags().StringVarP(&listenAddress, "listen", "", "", "Listener address and port, in format: address:port")
|
|
||||||
runCmd.Flags().StringVarP(&primaryUpstream, "primary_upstream", "", "", "Primary upstream endpoint")
|
|
||||||
runCmd.Flags().StringVarP(&secondaryUpstream, "secondary_upstream", "", "", "Secondary upstream endpoint")
|
|
||||||
runCmd.Flags().StringSliceVarP(&domains, "domains", "", nil, "List of domain to apply in a split DNS policy")
|
|
||||||
runCmd.Flags().StringVarP(&logPath, "log", "", "", "Path to log file")
|
|
||||||
runCmd.Flags().IntVarP(&cacheSize, "cache_size", "", 0, "Enable cache with size items")
|
|
||||||
runCmd.Flags().StringVarP(&cdUID, "cd", "", "", "Control D resolver uid")
|
|
||||||
runCmd.Flags().StringVarP(&homedir, "homedir", "", "", "")
|
|
||||||
_ = runCmd.Flags().MarkHidden("homedir")
|
|
||||||
runCmd.Flags().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`)
|
|
||||||
_ = runCmd.Flags().MarkHidden("iface")
|
|
||||||
|
|
||||||
rootCmd.AddCommand(runCmd)
|
|
||||||
|
|
||||||
startCmd := &cobra.Command{
|
|
||||||
PreRun: checkHasElevatedPrivilege,
|
|
||||||
Use: "start",
|
|
||||||
Short: "Start the ctrld service",
|
|
||||||
Args: cobra.NoArgs,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
sc := &service.Config{}
|
|
||||||
*sc = *svcConfig
|
|
||||||
osArgs := os.Args[2:]
|
|
||||||
if os.Args[1] == "service" {
|
|
||||||
osArgs = os.Args[3:]
|
|
||||||
}
|
|
||||||
setDependencies(sc)
|
|
||||||
sc.Arguments = append([]string{"run"}, osArgs...)
|
|
||||||
if dir, err := os.UserHomeDir(); err == nil {
|
|
||||||
// WorkingDirectory is not supported on Windows.
|
|
||||||
sc.WorkingDirectory = dir
|
|
||||||
// No config path, generating config in HOME directory.
|
|
||||||
noConfigStart := isNoConfigStart(cmd)
|
|
||||||
writeDefaultConfig := !noConfigStart && configBase64 == ""
|
|
||||||
if configPath == "" && writeDefaultConfig {
|
|
||||||
defaultConfigFile = filepath.Join(dir, defaultConfigFile)
|
|
||||||
readConfigFile(writeDefaultConfig && cdUID == "")
|
|
||||||
}
|
|
||||||
sc.Arguments = append(sc.Arguments, "--homedir="+dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
initLogging()
|
|
||||||
processCDFlags()
|
|
||||||
// On Windows, the service will be run as SYSTEM, so if ctrld start as Admin,
|
|
||||||
// the user home dir is different, so pass specific arguments that relevant here.
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
if configPath == "" {
|
|
||||||
sc.Arguments = append(sc.Arguments, "--config="+defaultConfigFile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
prog := &prog{}
|
|
||||||
s, err := service.New(prog, sc)
|
|
||||||
if err != nil {
|
|
||||||
stderrMsg(err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tasks := []task{
|
|
||||||
{s.Stop, false},
|
|
||||||
{s.Uninstall, false},
|
|
||||||
{s.Install, false},
|
|
||||||
{s.Start, true},
|
|
||||||
}
|
|
||||||
if doTasks(tasks) {
|
|
||||||
prog.setDNS()
|
|
||||||
mainLog.Info().Msg("Service started")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
// Keep these flags in sync with runCmd above, except for "-d".
|
|
||||||
startCmd.Flags().StringVarP(&configPath, "config", "c", "", "Path to config file")
|
|
||||||
startCmd.Flags().StringVarP(&configBase64, "base64_config", "", "", "Base64 encoded config")
|
|
||||||
startCmd.Flags().StringVarP(&listenAddress, "listen", "", "", "Listener address and port, in format: address:port")
|
|
||||||
startCmd.Flags().StringVarP(&primaryUpstream, "primary_upstream", "", "", "Primary upstream endpoint")
|
|
||||||
startCmd.Flags().StringVarP(&secondaryUpstream, "secondary_upstream", "", "", "Secondary upstream endpoint")
|
|
||||||
startCmd.Flags().StringSliceVarP(&domains, "domains", "", nil, "List of domain to apply in a split DNS policy")
|
|
||||||
startCmd.Flags().StringVarP(&logPath, "log", "", "", "Path to log file")
|
|
||||||
startCmd.Flags().IntVarP(&cacheSize, "cache_size", "", 0, "Enable cache with size items")
|
|
||||||
startCmd.Flags().StringVarP(&cdUID, "cd", "", "", "Control D resolver uid")
|
|
||||||
startCmd.Flags().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`)
|
|
||||||
|
|
||||||
stopCmd := &cobra.Command{
|
|
||||||
PreRun: checkHasElevatedPrivilege,
|
|
||||||
Use: "stop",
|
|
||||||
Short: "Stop the ctrld service",
|
|
||||||
Args: cobra.NoArgs,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
prog := &prog{}
|
|
||||||
s, err := service.New(prog, svcConfig)
|
|
||||||
if err != nil {
|
|
||||||
stderrMsg(err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
initLogging()
|
|
||||||
if doTasks([]task{{s.Stop, true}}) {
|
|
||||||
prog.resetDNS()
|
|
||||||
mainLog.Info().Msg("Service stopped")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
stopCmd.Flags().StringVarP(&iface, "iface", "", "", `Reset DNS setting for iface, "auto" means the default interface gateway`)
|
|
||||||
|
|
||||||
restartCmd := &cobra.Command{
|
|
||||||
PreRun: checkHasElevatedPrivilege,
|
|
||||||
Use: "restart",
|
|
||||||
Short: "Restart the ctrld service",
|
|
||||||
Args: cobra.NoArgs,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
s, err := service.New(&prog{}, svcConfig)
|
|
||||||
if err != nil {
|
|
||||||
stderrMsg(err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
initLogging()
|
|
||||||
if doTasks([]task{{s.Restart, true}}) {
|
|
||||||
stdoutMsg("Service restarted")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
statusCmd := &cobra.Command{
|
|
||||||
Use: "status",
|
|
||||||
Short: "Show status of the ctrld service",
|
|
||||||
Args: cobra.NoArgs,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
s, err := service.New(&prog{}, svcConfig)
|
|
||||||
if err != nil {
|
|
||||||
stderrMsg(err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
status, err := s.Status()
|
|
||||||
if err != nil {
|
|
||||||
stderrMsg(err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch status {
|
|
||||||
case service.StatusUnknown:
|
|
||||||
stdoutMsg("Unknown status")
|
|
||||||
case service.StatusRunning:
|
|
||||||
stdoutMsg("Service is running")
|
|
||||||
case service.StatusStopped:
|
|
||||||
stdoutMsg("Service is stopped")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
uninstallCmd := &cobra.Command{
|
|
||||||
PreRun: checkHasElevatedPrivilege,
|
|
||||||
Use: "uninstall",
|
|
||||||
Short: "Uninstall the ctrld service",
|
|
||||||
Args: cobra.NoArgs,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
prog := &prog{}
|
|
||||||
s, err := service.New(prog, svcConfig)
|
|
||||||
if err != nil {
|
|
||||||
stderrMsg(err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tasks := []task{
|
|
||||||
{s.Stop, false},
|
|
||||||
{s.Uninstall, true},
|
|
||||||
}
|
|
||||||
initLogging()
|
|
||||||
if doTasks(tasks) {
|
|
||||||
prog.resetDNS()
|
|
||||||
mainLog.Info().Msg("Service uninstalled")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
uninstallCmd.Flags().StringVarP(&iface, "iface", "", "auto", `Reset DNS setting for iface, "auto" means the default interface gateway`)
|
|
||||||
|
|
||||||
listIfacesCmd := &cobra.Command{
|
|
||||||
Use: "list",
|
|
||||||
Short: "List network interfaces of the host",
|
|
||||||
Args: cobra.NoArgs,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
err := interfaces.ForeachInterface(func(i interfaces.Interface, prefixes []netip.Prefix) {
|
|
||||||
fmt.Printf("Index : %d\n", i.Index)
|
|
||||||
fmt.Printf("Name : %s\n", i.Name)
|
|
||||||
addrs, _ := i.Addrs()
|
|
||||||
for i, ipaddr := range addrs {
|
|
||||||
if i == 0 {
|
|
||||||
fmt.Printf("Addrs : %v\n", ipaddr)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fmt.Printf(" %v\n", ipaddr)
|
|
||||||
}
|
|
||||||
for i, dns := range currentDNS(i.Interface) {
|
|
||||||
if i == 0 {
|
|
||||||
fmt.Printf("DNS : %s\n", dns)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fmt.Printf(" : %s\n", dns)
|
|
||||||
}
|
|
||||||
println()
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
stderrMsg(err.Error())
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
interfacesCmd := &cobra.Command{
|
|
||||||
Use: "interfaces",
|
|
||||||
Short: "Manage network interfaces",
|
|
||||||
Args: cobra.OnlyValidArgs,
|
|
||||||
ValidArgs: []string{
|
|
||||||
listIfacesCmd.Use,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
interfacesCmd.AddCommand(listIfacesCmd)
|
|
||||||
|
|
||||||
serviceCmd := &cobra.Command{
|
|
||||||
Use: "service",
|
|
||||||
Short: "Manage ctrld service",
|
|
||||||
Args: cobra.OnlyValidArgs,
|
|
||||||
ValidArgs: []string{
|
|
||||||
statusCmd.Use,
|
|
||||||
stopCmd.Use,
|
|
||||||
restartCmd.Use,
|
|
||||||
statusCmd.Use,
|
|
||||||
uninstallCmd.Use,
|
|
||||||
interfacesCmd.Use,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
serviceCmd.AddCommand(startCmd)
|
|
||||||
serviceCmd.AddCommand(stopCmd)
|
|
||||||
serviceCmd.AddCommand(restartCmd)
|
|
||||||
serviceCmd.AddCommand(statusCmd)
|
|
||||||
serviceCmd.AddCommand(uninstallCmd)
|
|
||||||
serviceCmd.AddCommand(interfacesCmd)
|
|
||||||
rootCmd.AddCommand(serviceCmd)
|
|
||||||
startCmdAlias := &cobra.Command{
|
|
||||||
PreRun: checkHasElevatedPrivilege,
|
|
||||||
Use: "start",
|
|
||||||
Short: "Quick start service and configure DNS on interface",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
if !cmd.Flags().Changed("iface") {
|
|
||||||
os.Args = append(os.Args, "--iface="+ifaceStartStop)
|
|
||||||
}
|
|
||||||
iface = ifaceStartStop
|
|
||||||
startCmd.Run(cmd, args)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
startCmdAlias.Flags().StringVarP(&ifaceStartStop, "iface", "", "auto", `Update DNS setting for iface, "auto" means the default interface gateway`)
|
|
||||||
startCmdAlias.Flags().AddFlagSet(startCmd.Flags())
|
|
||||||
rootCmd.AddCommand(startCmdAlias)
|
|
||||||
stopCmdAlias := &cobra.Command{
|
|
||||||
PreRun: checkHasElevatedPrivilege,
|
|
||||||
Use: "stop",
|
|
||||||
Short: "Quick stop service and remove DNS from interface",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
if !cmd.Flags().Changed("iface") {
|
|
||||||
os.Args = append(os.Args, "--iface="+ifaceStartStop)
|
|
||||||
}
|
|
||||||
iface = ifaceStartStop
|
|
||||||
stopCmd.Run(cmd, args)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
stopCmdAlias.Flags().StringVarP(&ifaceStartStop, "iface", "", "auto", `Reset DNS setting for iface, "auto" means the default interface gateway`)
|
|
||||||
stopCmdAlias.Flags().AddFlagSet(stopCmd.Flags())
|
|
||||||
rootCmd.AddCommand(stopCmdAlias)
|
|
||||||
|
|
||||||
if err := rootCmd.Execute(); err != nil {
|
|
||||||
stderrMsg(err.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeConfigFile() error {
|
|
||||||
if cfu := v.ConfigFileUsed(); cfu != "" {
|
|
||||||
defaultConfigFile = cfu
|
|
||||||
}
|
|
||||||
f, err := os.OpenFile(defaultConfigFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, os.FileMode(0o644))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
if cdUID != "" {
|
|
||||||
if _, err := f.WriteString("# AUTO-GENERATED VIA CD FLAG - DO NOT MODIFY\n\n"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
enc := toml.NewEncoder(f).SetIndentTables(true)
|
|
||||||
if err := enc.Encode(v.AllSettings()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := f.Close(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readConfigFile(writeDefaultConfig bool) bool {
|
|
||||||
// If err == nil, there's a config supplied via `--config`, no default config written.
|
|
||||||
err := v.ReadInConfig()
|
|
||||||
if err == nil {
|
|
||||||
fmt.Println("loading config file from:", v.ConfigFileUsed())
|
|
||||||
defaultConfigFile = v.ConfigFileUsed()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if !writeDefaultConfig {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// If error is viper.ConfigFileNotFoundError, write default config.
|
|
||||||
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
|
|
||||||
if err := writeConfigFile(); err != nil {
|
|
||||||
log.Fatalf("failed to write default config file: %v", err)
|
|
||||||
} else {
|
|
||||||
fmt.Println("writing default config file to: " + defaultConfigFile)
|
|
||||||
}
|
|
||||||
defaultConfigWritten = true
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// Otherwise, report fatal error and exit.
|
|
||||||
log.Fatalf("failed to decode config file: %v", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func readBase64Config() {
|
|
||||||
if configBase64 == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
configStr, err := base64.StdEncoding.DecodeString(configBase64)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("invalid base64 config: %v", err)
|
|
||||||
}
|
|
||||||
if err := v.ReadConfig(bytes.NewReader(configStr)); err != nil {
|
|
||||||
log.Fatalf("failed to read base64 config: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func processNoConfigFlags(noConfigStart bool) {
|
|
||||||
if !noConfigStart {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if listenAddress == "" || primaryUpstream == "" {
|
|
||||||
log.Fatal(`"listen" and "primary_upstream" flags must be set in no config mode`)
|
|
||||||
}
|
|
||||||
processListenFlag()
|
|
||||||
|
|
||||||
upstream := map[string]*ctrld.UpstreamConfig{
|
|
||||||
"0": {
|
|
||||||
Name: primaryUpstream,
|
|
||||||
Endpoint: primaryUpstream,
|
|
||||||
Type: ctrld.ResolverTypeDOH,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if secondaryUpstream != "" {
|
|
||||||
upstream["1"] = &ctrld.UpstreamConfig{
|
|
||||||
Name: secondaryUpstream,
|
|
||||||
Endpoint: secondaryUpstream,
|
|
||||||
Type: ctrld.ResolverTypeLegacy,
|
|
||||||
}
|
|
||||||
rules := make([]ctrld.Rule, 0, len(domains))
|
|
||||||
for _, domain := range domains {
|
|
||||||
rules = append(rules, ctrld.Rule{domain: []string{"upstream.1"}})
|
|
||||||
}
|
|
||||||
lc := v.Get("listener").(map[string]*ctrld.ListenerConfig)["0"]
|
|
||||||
lc.Policy = &ctrld.ListenerPolicyConfig{Name: "My Policy", Rules: rules}
|
|
||||||
}
|
|
||||||
v.Set("upstream", upstream)
|
|
||||||
}
|
|
||||||
|
|
||||||
func processCDFlags() {
|
|
||||||
if cdUID == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if iface == "" {
|
|
||||||
iface = "auto"
|
|
||||||
}
|
|
||||||
logger := mainLog.With().Str("mode", "cd").Logger()
|
|
||||||
logger.Info().Msg("fetching Controld-D configuration")
|
|
||||||
resolverConfig, err := controld.FetchResolverConfig(cdUID)
|
|
||||||
if uer, ok := err.(*controld.UtilityErrorResponse); ok && uer.ErrorField.Code == controld.InvalidConfigCode {
|
|
||||||
s, err := service.New(&prog{}, svcConfig)
|
|
||||||
if err != nil {
|
|
||||||
logger.Warn().Err(err).Msg("failed to create new service")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if netIface, _ := netInterface(iface); netIface != nil {
|
|
||||||
if err := restoreNetworkManager(); err != nil {
|
|
||||||
logger.Error().Err(err).Msg("could not restore NetworkManager")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
logger.Debug().Str("iface", netIface.Name).Msg("Restoring DNS for interface")
|
|
||||||
if err := resetDNS(netIface); err != nil {
|
|
||||||
logger.Warn().Err(err).Msg("something went wrong while restoring DNS")
|
|
||||||
} else {
|
|
||||||
logger.Debug().Str("iface", netIface.Name).Msg("Restoring DNS successfully")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks := []task{{s.Uninstall, true}}
|
|
||||||
if doTasks(tasks) {
|
|
||||||
logger.Info().Msg("uninstalled service")
|
|
||||||
}
|
|
||||||
logger.Fatal().Err(uer).Msg("failed to fetch resolver config")
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
logger.Warn().Err(err).Msg("could not fetch resolver config")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info().Msg("generating ctrld config from Controld-D configuration")
|
|
||||||
cfg = ctrld.Config{}
|
|
||||||
cfg.Network = make(map[string]*ctrld.NetworkConfig)
|
|
||||||
cfg.Network["0"] = &ctrld.NetworkConfig{
|
|
||||||
Name: "Network 0",
|
|
||||||
Cidrs: []string{"0.0.0.0/0"},
|
|
||||||
}
|
|
||||||
cfg.Upstream = make(map[string]*ctrld.UpstreamConfig)
|
|
||||||
cfg.Upstream["0"] = &ctrld.UpstreamConfig{
|
|
||||||
Endpoint: resolverConfig.DOH,
|
|
||||||
Type: ctrld.ResolverTypeDOH,
|
|
||||||
Timeout: 5000,
|
|
||||||
}
|
|
||||||
rules := make([]ctrld.Rule, 0, len(resolverConfig.Exclude))
|
|
||||||
for _, domain := range resolverConfig.Exclude {
|
|
||||||
rules = append(rules, ctrld.Rule{domain: []string{}})
|
|
||||||
}
|
|
||||||
cfg.Listener = make(map[string]*ctrld.ListenerConfig)
|
|
||||||
cfg.Listener["0"] = &ctrld.ListenerConfig{
|
|
||||||
IP: "127.0.0.1",
|
|
||||||
Port: 53,
|
|
||||||
Policy: &ctrld.ListenerPolicyConfig{
|
|
||||||
Name: "My Policy",
|
|
||||||
Rules: rules,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
v = viper.NewWithOptions(viper.KeyDelimiter("::"))
|
|
||||||
v.Set("network", cfg.Network)
|
|
||||||
v.Set("upstream", cfg.Upstream)
|
|
||||||
v.Set("listener", cfg.Listener)
|
|
||||||
processLogAndCacheFlags()
|
|
||||||
if err := writeConfigFile(); err != nil {
|
|
||||||
logger.Fatal().Err(err).Msg("failed to write config file")
|
|
||||||
} else {
|
|
||||||
logger.Info().Msg("writing config file to: " + defaultConfigFile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func processListenFlag() {
|
|
||||||
if listenAddress == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
host, portStr, err := net.SplitHostPort(listenAddress)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("invalid listener address: %v", err)
|
|
||||||
}
|
|
||||||
port, err := strconv.Atoi(portStr)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("invalid port number: %v", err)
|
|
||||||
}
|
|
||||||
lc := &ctrld.ListenerConfig{
|
|
||||||
IP: host,
|
|
||||||
Port: port,
|
|
||||||
}
|
|
||||||
v.Set("listener", map[string]*ctrld.ListenerConfig{
|
|
||||||
"0": lc,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func processLogAndCacheFlags() {
|
|
||||||
if logPath != "" {
|
|
||||||
cfg.Service.LogLevel = "debug"
|
|
||||||
cfg.Service.LogPath = logPath
|
|
||||||
}
|
|
||||||
|
|
||||||
if cacheSize != 0 {
|
|
||||||
cfg.Service.CacheEnable = true
|
|
||||||
cfg.Service.CacheSize = cacheSize
|
|
||||||
}
|
|
||||||
v.Set("service", cfg.Service)
|
|
||||||
}
|
|
||||||
|
|
||||||
func netInterface(ifaceName string) (*net.Interface, error) {
|
|
||||||
if ifaceName == "auto" {
|
|
||||||
ifaceName = defaultIfaceName()
|
|
||||||
}
|
|
||||||
var iface *net.Interface
|
|
||||||
err := interfaces.ForeachInterface(func(i interfaces.Interface, prefixes []netip.Prefix) {
|
|
||||||
if i.Name == ifaceName {
|
|
||||||
iface = i.Interface
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if iface == nil {
|
|
||||||
return nil, errors.New("interface not found")
|
|
||||||
}
|
|
||||||
if err := patchNetIfaceName(iface); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return iface, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func defaultIfaceName() string {
|
|
||||||
dri, err := interfaces.DefaultRouteInterface()
|
|
||||||
if err != nil {
|
|
||||||
mainLog.Fatal().Err(err).Msg("failed to get default route interface")
|
|
||||||
}
|
|
||||||
return dri
|
|
||||||
}
|
|
||||||
@@ -1,321 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"runtime"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/dnscache"
|
|
||||||
)
|
|
||||||
|
|
||||||
const staleTTL = 60 * time.Second
|
|
||||||
|
|
||||||
func (p *prog) serveUDP(listenerNum string) error {
|
|
||||||
listenerConfig := p.cfg.Listener[listenerNum]
|
|
||||||
// make sure ip is allocated
|
|
||||||
if allocErr := p.allocateIP(listenerConfig.IP); allocErr != nil {
|
|
||||||
mainLog.Error().Err(allocErr).Str("ip", listenerConfig.IP).Msg("serveUDP: failed to allocate listen ip")
|
|
||||||
return allocErr
|
|
||||||
}
|
|
||||||
var failoverRcodes []int
|
|
||||||
if listenerConfig.Policy != nil {
|
|
||||||
failoverRcodes = listenerConfig.Policy.FailoverRcodeNumbers
|
|
||||||
}
|
|
||||||
handler := dns.HandlerFunc(func(w dns.ResponseWriter, m *dns.Msg) {
|
|
||||||
domain := canonicalName(m.Question[0].Name)
|
|
||||||
reqId := requestID()
|
|
||||||
fmtSrcToDest := fmtRemoteToLocal(listenerNum, w.RemoteAddr().String(), w.LocalAddr().String())
|
|
||||||
t := time.Now()
|
|
||||||
ctx := context.WithValue(context.Background(), ctrld.ReqIdCtxKey{}, reqId)
|
|
||||||
ctrld.Log(ctx, proxyLog.Debug(), "%s received query: %s", fmtSrcToDest, domain)
|
|
||||||
upstreams, matched := p.upstreamFor(ctx, listenerNum, listenerConfig, w.RemoteAddr(), domain)
|
|
||||||
var answer *dns.Msg
|
|
||||||
if !matched && listenerConfig.Restricted {
|
|
||||||
answer = new(dns.Msg)
|
|
||||||
answer.SetRcode(m, dns.RcodeRefused)
|
|
||||||
|
|
||||||
} else {
|
|
||||||
answer = p.proxy(ctx, upstreams, failoverRcodes, m)
|
|
||||||
rtt := time.Since(t)
|
|
||||||
ctrld.Log(ctx, proxyLog.Debug(), "received response of %d bytes in %s", answer.Len(), rtt)
|
|
||||||
}
|
|
||||||
if err := w.WriteMsg(answer); err != nil {
|
|
||||||
ctrld.Log(ctx, mainLog.Error().Err(err), "serveUDP: failed to send DNS response to client")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// On Windows, there's no easy way for disabling/removing IPv6 DNS resolver, so we check whether we can
|
|
||||||
// listen on ::1, then spawn a listener for receiving DNS requests.
|
|
||||||
if runtime.GOOS == "windows" && supportsIPv6ListenLocal() {
|
|
||||||
go func() {
|
|
||||||
s := &dns.Server{
|
|
||||||
Addr: net.JoinHostPort("::1", strconv.Itoa(listenerConfig.Port)),
|
|
||||||
Net: "udp",
|
|
||||||
Handler: handler,
|
|
||||||
}
|
|
||||||
_ = s.ListenAndServe()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
s := &dns.Server{
|
|
||||||
Addr: net.JoinHostPort(listenerConfig.IP, strconv.Itoa(listenerConfig.Port)),
|
|
||||||
Net: "udp",
|
|
||||||
Handler: handler,
|
|
||||||
}
|
|
||||||
return s.ListenAndServe()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *prog) upstreamFor(ctx context.Context, defaultUpstreamNum string, lc *ctrld.ListenerConfig, addr net.Addr, domain string) ([]string, bool) {
|
|
||||||
upstreams := []string{"upstream." + defaultUpstreamNum}
|
|
||||||
matchedPolicy := "no policy"
|
|
||||||
matchedNetwork := "no network"
|
|
||||||
matchedRule := "no rule"
|
|
||||||
matched := false
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if !matched && lc.Restricted {
|
|
||||||
ctrld.Log(ctx, proxyLog.Info(), "query refused, %s does not match any network policy", addr.String())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctrld.Log(ctx, proxyLog.Info(), "%s, %s, %s -> %v", matchedPolicy, matchedNetwork, matchedRule, upstreams)
|
|
||||||
}()
|
|
||||||
|
|
||||||
if lc.Policy == nil {
|
|
||||||
return upstreams, false
|
|
||||||
}
|
|
||||||
|
|
||||||
do := func(policyUpstreams []string) {
|
|
||||||
upstreams = append([]string(nil), policyUpstreams...)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, rule := range lc.Policy.Rules {
|
|
||||||
// There's only one entry per rule, config validation ensures this.
|
|
||||||
for source, targets := range rule {
|
|
||||||
if source == domain || wildcardMatches(source, domain) {
|
|
||||||
matchedPolicy = lc.Policy.Name
|
|
||||||
matchedRule = source
|
|
||||||
do(targets)
|
|
||||||
matched = true
|
|
||||||
return upstreams, matched
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var sourceIP net.IP
|
|
||||||
switch addr := addr.(type) {
|
|
||||||
case *net.UDPAddr:
|
|
||||||
sourceIP = addr.IP
|
|
||||||
case *net.TCPAddr:
|
|
||||||
sourceIP = addr.IP
|
|
||||||
}
|
|
||||||
for _, rule := range lc.Policy.Networks {
|
|
||||||
for source, targets := range rule {
|
|
||||||
networkNum := strings.TrimPrefix(source, "network.")
|
|
||||||
nc := p.cfg.Network[networkNum]
|
|
||||||
if nc == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, ipNet := range nc.IPNets {
|
|
||||||
if ipNet.Contains(sourceIP) {
|
|
||||||
matchedPolicy = lc.Policy.Name
|
|
||||||
matchedNetwork = source
|
|
||||||
do(targets)
|
|
||||||
matched = true
|
|
||||||
return upstreams, matched
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return upstreams, matched
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *prog) proxy(ctx context.Context, upstreams []string, failoverRcodes []int, msg *dns.Msg) *dns.Msg {
|
|
||||||
var staleAnswer *dns.Msg
|
|
||||||
serveStaleCache := p.cache != nil && p.cfg.Service.CacheServeStale
|
|
||||||
upstreamConfigs := p.upstreamConfigsFromUpstreamNumbers(upstreams)
|
|
||||||
if len(upstreamConfigs) == 0 {
|
|
||||||
upstreamConfigs = []*ctrld.UpstreamConfig{osUpstreamConfig}
|
|
||||||
upstreams = []string{"upstream.os"}
|
|
||||||
}
|
|
||||||
// Inverse query should not be cached: https://www.rfc-editor.org/rfc/rfc1035#section-7.4
|
|
||||||
if p.cache != nil && msg.Question[0].Qtype != dns.TypePTR {
|
|
||||||
for _, upstream := range upstreams {
|
|
||||||
cachedValue := p.cache.Get(dnscache.NewKey(msg, upstream))
|
|
||||||
if cachedValue == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
answer := cachedValue.Msg.Copy()
|
|
||||||
answer.SetRcode(msg, answer.Rcode)
|
|
||||||
now := time.Now()
|
|
||||||
if cachedValue.Expire.After(now) {
|
|
||||||
ctrld.Log(ctx, proxyLog.Debug(), "hit cached response")
|
|
||||||
setCachedAnswerTTL(answer, now, cachedValue.Expire)
|
|
||||||
return answer
|
|
||||||
}
|
|
||||||
staleAnswer = answer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resolve := func(n int, upstreamConfig *ctrld.UpstreamConfig, msg *dns.Msg) *dns.Msg {
|
|
||||||
ctrld.Log(ctx, proxyLog.Debug(), "sending query to %s: %s", upstreams[n], upstreamConfig.Name)
|
|
||||||
dnsResolver, err := ctrld.NewResolver(upstreamConfig)
|
|
||||||
if err != nil {
|
|
||||||
ctrld.Log(ctx, proxyLog.Error().Err(err), "failed to create resolver")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
resolveCtx, cancel := context.WithCancel(ctx)
|
|
||||||
defer cancel()
|
|
||||||
if upstreamConfig.Timeout > 0 {
|
|
||||||
timeoutCtx, cancel := context.WithTimeout(resolveCtx, time.Millisecond*time.Duration(upstreamConfig.Timeout))
|
|
||||||
defer cancel()
|
|
||||||
resolveCtx = timeoutCtx
|
|
||||||
}
|
|
||||||
answer, err := dnsResolver.Resolve(resolveCtx, msg)
|
|
||||||
if err != nil {
|
|
||||||
ctrld.Log(ctx, proxyLog.Error().Err(err), "failed to resolve query")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return answer
|
|
||||||
}
|
|
||||||
for n, upstreamConfig := range upstreamConfigs {
|
|
||||||
answer := resolve(n, upstreamConfig, msg)
|
|
||||||
if answer == nil {
|
|
||||||
if serveStaleCache && staleAnswer != nil {
|
|
||||||
ctrld.Log(ctx, proxyLog.Debug(), "serving stale cached response")
|
|
||||||
now := time.Now()
|
|
||||||
setCachedAnswerTTL(staleAnswer, now, now.Add(staleTTL))
|
|
||||||
return staleAnswer
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if answer.Rcode != dns.RcodeSuccess && len(upstreamConfigs) > 1 && containRcode(failoverRcodes, answer.Rcode) {
|
|
||||||
ctrld.Log(ctx, proxyLog.Debug(), "failover rcode matched, process to next upstream")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if p.cache != nil {
|
|
||||||
ttl := ttlFromMsg(answer)
|
|
||||||
now := time.Now()
|
|
||||||
expired := now.Add(time.Duration(ttl) * time.Second)
|
|
||||||
if cachedTTL := p.cfg.Service.CacheTTLOverride; cachedTTL > 0 {
|
|
||||||
expired = now.Add(time.Duration(cachedTTL) * time.Second)
|
|
||||||
}
|
|
||||||
setCachedAnswerTTL(answer, now, expired)
|
|
||||||
p.cache.Add(dnscache.NewKey(msg, upstreams[n]), dnscache.NewValue(answer, expired))
|
|
||||||
ctrld.Log(ctx, proxyLog.Debug(), "add cached response")
|
|
||||||
}
|
|
||||||
return answer
|
|
||||||
}
|
|
||||||
ctrld.Log(ctx, proxyLog.Error(), "all upstreams failed")
|
|
||||||
answer := new(dns.Msg)
|
|
||||||
answer.SetRcode(msg, dns.RcodeServerFailure)
|
|
||||||
return answer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *prog) upstreamConfigsFromUpstreamNumbers(upstreams []string) []*ctrld.UpstreamConfig {
|
|
||||||
upstreamConfigs := make([]*ctrld.UpstreamConfig, 0, len(upstreams))
|
|
||||||
for _, upstream := range upstreams {
|
|
||||||
upstreamNum := strings.TrimPrefix(upstream, "upstream.")
|
|
||||||
upstreamConfigs = append(upstreamConfigs, p.cfg.Upstream[upstreamNum])
|
|
||||||
}
|
|
||||||
return upstreamConfigs
|
|
||||||
}
|
|
||||||
|
|
||||||
// canonicalName returns canonical name from FQDN with "." trimmed.
|
|
||||||
func canonicalName(fqdn string) string {
|
|
||||||
q := strings.TrimSpace(fqdn)
|
|
||||||
q = strings.TrimSuffix(q, ".")
|
|
||||||
// https://datatracker.ietf.org/doc/html/rfc4343
|
|
||||||
q = strings.ToLower(q)
|
|
||||||
|
|
||||||
return q
|
|
||||||
}
|
|
||||||
|
|
||||||
func wildcardMatches(wildcard, domain string) bool {
|
|
||||||
// Wildcard match.
|
|
||||||
wildCardParts := strings.Split(wildcard, "*")
|
|
||||||
if len(wildCardParts) != 2 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case len(wildCardParts[0]) > 0 && len(wildCardParts[1]) > 0:
|
|
||||||
// Domain must match both prefix and suffix.
|
|
||||||
return strings.HasPrefix(domain, wildCardParts[0]) && strings.HasSuffix(domain, wildCardParts[1])
|
|
||||||
|
|
||||||
case len(wildCardParts[1]) > 0:
|
|
||||||
// Only suffix must match.
|
|
||||||
return strings.HasSuffix(domain, wildCardParts[1])
|
|
||||||
|
|
||||||
case len(wildCardParts[0]) > 0:
|
|
||||||
// Only prefix must match.
|
|
||||||
return strings.HasPrefix(domain, wildCardParts[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func fmtRemoteToLocal(listenerNum, remote, local string) string {
|
|
||||||
return fmt.Sprintf("%s -> listener.%s: %s:", remote, listenerNum, local)
|
|
||||||
}
|
|
||||||
|
|
||||||
func requestID() string {
|
|
||||||
b := make([]byte, 3) // 6 chars
|
|
||||||
if _, err := rand.Read(b); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return hex.EncodeToString(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func containRcode(rcodes []int, rcode int) bool {
|
|
||||||
for i := range rcodes {
|
|
||||||
if rcodes[i] == rcode {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func setCachedAnswerTTL(answer *dns.Msg, now, expiredTime time.Time) {
|
|
||||||
ttlSecs := expiredTime.Sub(now).Seconds()
|
|
||||||
if ttlSecs < 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ttl := uint32(ttlSecs)
|
|
||||||
for _, rr := range answer.Answer {
|
|
||||||
rr.Header().Ttl = ttl
|
|
||||||
}
|
|
||||||
for _, rr := range answer.Ns {
|
|
||||||
rr.Header().Ttl = ttl
|
|
||||||
}
|
|
||||||
for _, rr := range answer.Extra {
|
|
||||||
if rr.Header().Rrtype != dns.TypeOPT {
|
|
||||||
rr.Header().Ttl = ttl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ttlFromMsg(msg *dns.Msg) uint32 {
|
|
||||||
for _, rr := range msg.Answer {
|
|
||||||
return rr.Header().Ttl
|
|
||||||
}
|
|
||||||
for _, rr := range msg.Ns {
|
|
||||||
return rr.Header().Ttl
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
var osUpstreamConfig = &ctrld.UpstreamConfig{
|
|
||||||
Name: "OS resolver",
|
|
||||||
Type: ctrld.ResolverTypeOS,
|
|
||||||
}
|
|
||||||
+2
-108
@@ -1,113 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import "github.com/Control-D-Inc/ctrld/cmd/cli"
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/kardianos/service"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
configPath string
|
|
||||||
configBase64 string
|
|
||||||
daemon bool
|
|
||||||
listenAddress string
|
|
||||||
primaryUpstream string
|
|
||||||
secondaryUpstream string
|
|
||||||
domains []string
|
|
||||||
logPath string
|
|
||||||
homedir string
|
|
||||||
cacheSize int
|
|
||||||
cfg ctrld.Config
|
|
||||||
verbose int
|
|
||||||
|
|
||||||
bootstrapDNS = "76.76.2.0"
|
|
||||||
|
|
||||||
rootLogger = zerolog.New(io.Discard)
|
|
||||||
mainLog = rootLogger
|
|
||||||
proxyLog = rootLogger
|
|
||||||
|
|
||||||
cdUID string
|
|
||||||
iface string
|
|
||||||
ifaceStartStop string
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
ctrld.InitConfig(v, "ctrld")
|
cli.Main()
|
||||||
initCLI()
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeLogFilePath(logFilePath string) string {
|
|
||||||
if logFilePath == "" || filepath.IsAbs(logFilePath) || service.Interactive() {
|
|
||||||
return logFilePath
|
|
||||||
}
|
|
||||||
if homedir != "" {
|
|
||||||
return filepath.Join(homedir, logFilePath)
|
|
||||||
}
|
|
||||||
dir, _ := os.UserHomeDir()
|
|
||||||
if dir == "" {
|
|
||||||
return logFilePath
|
|
||||||
}
|
|
||||||
return filepath.Join(dir, logFilePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func initLogging() {
|
|
||||||
writers := []io.Writer{io.Discard}
|
|
||||||
isLog := cfg.Service.LogLevel != ""
|
|
||||||
if logFilePath := normalizeLogFilePath(cfg.Service.LogPath); logFilePath != "" {
|
|
||||||
// Create parent directory if necessary.
|
|
||||||
if err := os.MkdirAll(filepath.Dir(logFilePath), 0750); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "failed to create log path: %v", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
logFile, err := os.OpenFile(logFilePath, os.O_APPEND|os.O_CREATE|os.O_RDWR, os.FileMode(0o600))
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "failed to create log file: %v", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
isLog = true
|
|
||||||
writers = append(writers, logFile)
|
|
||||||
}
|
|
||||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
|
|
||||||
consoleWriter := zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) {
|
|
||||||
w.TimeFormat = time.StampMilli
|
|
||||||
})
|
|
||||||
writers = append(writers, consoleWriter)
|
|
||||||
multi := zerolog.MultiLevelWriter(writers...)
|
|
||||||
mainLog = mainLog.Output(multi).With().Timestamp().Str("prefix", "main").Logger()
|
|
||||||
if verbose > 0 || isLog {
|
|
||||||
proxyLog = proxyLog.Output(multi).With().Timestamp().Logger()
|
|
||||||
// TODO: find a better way.
|
|
||||||
ctrld.ProxyLog = proxyLog
|
|
||||||
}
|
|
||||||
|
|
||||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
|
||||||
logLevel := cfg.Service.LogLevel
|
|
||||||
if verbose > 1 {
|
|
||||||
logLevel = "debug"
|
|
||||||
}
|
|
||||||
if logLevel == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
level, err := zerolog.ParseLevel(logLevel)
|
|
||||||
if err != nil {
|
|
||||||
mainLog.Warn().Err(err).Msg("could not set log level")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
zerolog.SetGlobalLevel(level)
|
|
||||||
}
|
|
||||||
|
|
||||||
func initCache() {
|
|
||||||
if !cfg.Service.CacheEnable {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if cfg.Service.CacheSize == 0 {
|
|
||||||
cfg.Service.CacheSize = 4096
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"tailscale.com/logtail/backoff"
|
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/controld"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
controldIPv6Test = "ipv6.controld.io"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
stackOnce sync.Once
|
|
||||||
ipv6Enabled bool
|
|
||||||
canListenIPv6Local bool
|
|
||||||
hasNetworkUp bool
|
|
||||||
)
|
|
||||||
|
|
||||||
func probeStack() {
|
|
||||||
b := backoff.NewBackoff("probeStack", func(format string, args ...any) {}, time.Minute)
|
|
||||||
for {
|
|
||||||
if _, err := controld.Dialer.Dial("udp", net.JoinHostPort(bootstrapDNS, "53")); err == nil {
|
|
||||||
hasNetworkUp = true
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
b.BackOff(context.Background(), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if _, err := controld.Dialer.Dial("tcp6", net.JoinHostPort(controldIPv6Test, "80")); err == nil {
|
|
||||||
ipv6Enabled = true
|
|
||||||
}
|
|
||||||
if ln, err := net.Listen("tcp6", "[::1]:53"); err == nil {
|
|
||||||
ln.Close()
|
|
||||||
canListenIPv6Local = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func netUp() bool {
|
|
||||||
stackOnce.Do(probeStack)
|
|
||||||
return hasNetworkUp
|
|
||||||
}
|
|
||||||
|
|
||||||
func supportsIPv6() bool {
|
|
||||||
stackOnce.Do(probeStack)
|
|
||||||
return ipv6Enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
func supportsIPv6ListenLocal() bool {
|
|
||||||
stackOnce.Do(probeStack)
|
|
||||||
return canListenIPv6Local
|
|
||||||
}
|
|
||||||
|
|
||||||
// isIPv6 checks if the provided IP is v6.
|
|
||||||
//
|
|
||||||
//lint:ignore U1000 use in os_windows.go
|
|
||||||
func isIPv6(ip string) bool {
|
|
||||||
parsedIP := net.ParseIP(ip)
|
|
||||||
return parsedIP != nil && parsedIP.To4() == nil && parsedIP.To16() != nil
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"net"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func patchNetIfaceName(iface *net.Interface) error {
|
|
||||||
b, err := exec.Command("networksetup", "-listnetworkserviceorder").Output()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(bytes.NewReader(b))
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
if strings.Contains(line, "*") {
|
|
||||||
// Network services is disabled.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !strings.Contains(line, "Device: "+iface.Name) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
parts := strings.Split(line, ",")
|
|
||||||
if _, networkServiceName, ok := strings.Cut(parts[0], "(Hardware Port: "); ok {
|
|
||||||
mainLog.Debug().Str("network_service", networkServiceName).Msg("found network service name for interface")
|
|
||||||
iface.Name = networkServiceName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
|
||||||
"os/exec"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/insomniacslk/dhcp/dhcpv4/nclient4"
|
|
||||||
"github.com/insomniacslk/dhcp/dhcpv6"
|
|
||||||
"github.com/insomniacslk/dhcp/dhcpv6/client6"
|
|
||||||
"tailscale.com/util/dnsname"
|
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/dns"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/resolvconffile"
|
|
||||||
)
|
|
||||||
|
|
||||||
var logf = func(format string, args ...any) {
|
|
||||||
mainLog.Debug().Msgf(format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// allocate loopback ip
|
|
||||||
// sudo ip a add 127.0.0.2/24 dev lo
|
|
||||||
func allocateIP(ip string) error {
|
|
||||||
cmd := exec.Command("ip", "a", "add", ip+"/24", "dev", "lo")
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
mainLog.Error().Err(err).Msg("allocateIP failed")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func deAllocateIP(ip string) error {
|
|
||||||
cmd := exec.Command("ip", "a", "del", ip+"/24", "dev", "lo")
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
mainLog.Error().Err(err).Msg("deAllocateIP failed")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxSetDNSAttempts = 5
|
|
||||||
|
|
||||||
// set the dns server for the provided network interface
|
|
||||||
func setDNS(iface *net.Interface, nameservers []string) error {
|
|
||||||
r, err := dns.NewOSConfigurator(logf, iface.Name)
|
|
||||||
if err != nil {
|
|
||||||
mainLog.Error().Err(err).Msg("failed to create DNS OS configurator")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ns := make([]netip.Addr, 0, len(nameservers))
|
|
||||||
for _, nameserver := range nameservers {
|
|
||||||
ns = append(ns, netip.MustParseAddr(nameserver))
|
|
||||||
}
|
|
||||||
|
|
||||||
osConfig := dns.OSConfig{
|
|
||||||
Nameservers: ns,
|
|
||||||
SearchDomains: []dnsname.FQDN{},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < maxSetDNSAttempts; i++ {
|
|
||||||
if err := r.SetDNS(osConfig); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
currentNS := currentDNS(iface)
|
|
||||||
if reflect.DeepEqual(currentNS, nameservers) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mainLog.Debug().Msg("DNS was not set for some reason")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func resetDNS(iface *net.Interface) error {
|
|
||||||
if r, err := dns.NewOSConfigurator(logf, iface.Name); err == nil {
|
|
||||||
if err := r.Close(); err != nil {
|
|
||||||
mainLog.Error().Err(err).Msg("failed to rollback DNS setting")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if r.Mode() == "direct" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var ns []string
|
|
||||||
c, err := nclient4.New(iface.Name)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("nclient4.New: %w", err)
|
|
||||||
}
|
|
||||||
defer c.Close()
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
|
||||||
defer cancel()
|
|
||||||
lease, err := c.Request(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("nclient4.Request: %w", err)
|
|
||||||
}
|
|
||||||
for _, nameserver := range lease.ACK.DNS() {
|
|
||||||
if nameserver.Equal(net.IPv4zero) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ns = append(ns, nameserver.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(cuonglm): handle DHCPv6 properly.
|
|
||||||
if supportsIPv6() {
|
|
||||||
c := client6.NewClient()
|
|
||||||
conversation, err := c.Exchange(iface.Name)
|
|
||||||
if err != nil {
|
|
||||||
mainLog.Debug().Err(err).Msg("could not exchange DHCPv6")
|
|
||||||
}
|
|
||||||
for _, packet := range conversation {
|
|
||||||
if packet.Type() == dhcpv6.MessageTypeReply {
|
|
||||||
msg, err := packet.GetInnerMessage()
|
|
||||||
if err != nil {
|
|
||||||
mainLog.Debug().Err(err).Msg("could not get inner DHCPv6 message")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
nameservers := msg.Options.DNS()
|
|
||||||
for _, nameserver := range nameservers {
|
|
||||||
ns = append(ns, nameserver.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ignoringEINTR(func() error {
|
|
||||||
return setDNS(iface, ns)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func currentDNS(iface *net.Interface) []string {
|
|
||||||
for _, fn := range []getDNS{getDNSByResolvectl, getDNSByNmcli, resolvconffile.NameServers} {
|
|
||||||
if ns := fn(iface.Name); len(ns) > 0 {
|
|
||||||
return ns
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDNSByResolvectl(iface string) []string {
|
|
||||||
b, err := exec.Command("resolvectl", "dns", "-i", iface).Output()
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.Fields(strings.SplitN(string(b), "%", 2)[0])
|
|
||||||
if len(parts) > 2 {
|
|
||||||
return parts[3:]
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDNSByNmcli(iface string) []string {
|
|
||||||
b, err := exec.Command("nmcli", "dev", "show", iface).Output()
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
s := bufio.NewScanner(bytes.NewReader(b))
|
|
||||||
var dns []string
|
|
||||||
do := func(line string) {
|
|
||||||
parts := strings.SplitN(line, ":", 2)
|
|
||||||
if len(parts) > 1 {
|
|
||||||
dns = append(dns, strings.TrimSpace(parts[1]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for s.Scan() {
|
|
||||||
line := s.Text()
|
|
||||||
switch {
|
|
||||||
case strings.HasPrefix(line, "IP4.DNS"):
|
|
||||||
fallthrough
|
|
||||||
case strings.HasPrefix(line, "IP6.DNS"):
|
|
||||||
do(line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dns
|
|
||||||
}
|
|
||||||
|
|
||||||
func ignoringEINTR(fn func() error) error {
|
|
||||||
for {
|
|
||||||
err := fn()
|
|
||||||
if err != syscall.EINTR {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"github.com/kardianos/service"
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld"
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/dnscache"
|
|
||||||
)
|
|
||||||
|
|
||||||
var errWindowsAddrInUse = syscall.Errno(0x2740)
|
|
||||||
|
|
||||||
var svcConfig = &service.Config{
|
|
||||||
Name: "ctrld",
|
|
||||||
DisplayName: "Control-D Helper Service",
|
|
||||||
}
|
|
||||||
|
|
||||||
type prog struct {
|
|
||||||
cfg *ctrld.Config
|
|
||||||
cache dnscache.Cacher
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *prog) Start(s service.Service) error {
|
|
||||||
p.cfg = &cfg
|
|
||||||
go p.run()
|
|
||||||
mainLog.Info().Msg("Service started")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *prog) run() {
|
|
||||||
p.preRun()
|
|
||||||
if p.cfg.Service.CacheEnable {
|
|
||||||
cacher, err := dnscache.NewLRUCache(p.cfg.Service.CacheSize)
|
|
||||||
if err != nil {
|
|
||||||
mainLog.Error().Err(err).Msg("failed to create cacher, caching is disabled")
|
|
||||||
} else {
|
|
||||||
p.cache = cacher
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
wg.Add(len(p.cfg.Listener))
|
|
||||||
|
|
||||||
for _, nc := range p.cfg.Network {
|
|
||||||
for _, cidr := range nc.Cidrs {
|
|
||||||
_, ipNet, err := net.ParseCIDR(cidr)
|
|
||||||
if err != nil {
|
|
||||||
proxyLog.Error().Err(err).Str("network", nc.Name).Str("cidr", cidr).Msg("invalid cidr")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
nc.IPNets = append(nc.IPNets, ipNet)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for n := range p.cfg.Upstream {
|
|
||||||
uc := p.cfg.Upstream[n]
|
|
||||||
uc.Init()
|
|
||||||
if uc.BootstrapIP == "" {
|
|
||||||
// resolve it manually and set the bootstrap ip
|
|
||||||
c := new(dns.Client)
|
|
||||||
for _, dnsType := range []uint16{dns.TypeAAAA, dns.TypeA} {
|
|
||||||
if !supportsIPv6() && dnsType == dns.TypeAAAA {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
m := new(dns.Msg)
|
|
||||||
m.SetQuestion(uc.Domain+".", dnsType)
|
|
||||||
m.RecursionDesired = true
|
|
||||||
r, _, err := c.Exchange(m, net.JoinHostPort(bootstrapDNS, "53"))
|
|
||||||
if err != nil {
|
|
||||||
proxyLog.Error().Err(err).Msgf("could not resolve domain %s for upstream.%s", uc.Domain, n)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if r.Rcode != dns.RcodeSuccess {
|
|
||||||
proxyLog.Error().Msgf("could not resolve domain return code: %d, upstream.%s", r.Rcode, n)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if len(r.Answer) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, a := range r.Answer {
|
|
||||||
switch ar := a.(type) {
|
|
||||||
case *dns.A:
|
|
||||||
uc.BootstrapIP = ar.A.String()
|
|
||||||
case *dns.AAAA:
|
|
||||||
uc.BootstrapIP = ar.AAAA.String()
|
|
||||||
default:
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
mainLog.Info().Str("bootstrap_ip", uc.BootstrapIP).Msgf("Setting bootstrap IP for upstream.%s", n)
|
|
||||||
// Stop if we reached here, because we got the bootstrap IP from r.Answer.
|
|
||||||
break
|
|
||||||
}
|
|
||||||
// If we reached here, uc.BootstrapIP was set, nothing to do anymore.
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
uc.SetupTransport()
|
|
||||||
}
|
|
||||||
|
|
||||||
for listenerNum := range p.cfg.Listener {
|
|
||||||
p.cfg.Listener[listenerNum].Init()
|
|
||||||
go func(listenerNum string) {
|
|
||||||
defer wg.Done()
|
|
||||||
listenerConfig := p.cfg.Listener[listenerNum]
|
|
||||||
upstreamConfig := p.cfg.Upstream[listenerNum]
|
|
||||||
if upstreamConfig == nil {
|
|
||||||
proxyLog.Error().Msgf("missing upstream config for: [listener.%s]", listenerNum)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
addr := net.JoinHostPort(listenerConfig.IP, strconv.Itoa(listenerConfig.Port))
|
|
||||||
mainLog.Info().Msgf("Starting DNS server on listener.%s: %s", listenerNum, addr)
|
|
||||||
err := p.serveUDP(listenerNum)
|
|
||||||
if err != nil && !defaultConfigWritten {
|
|
||||||
proxyLog.Fatal().Err(err).Msgf("Unable to start dns proxy on listener.%s", listenerNum)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if opErr, ok := err.(*net.OpError); ok {
|
|
||||||
if sErr, ok := opErr.Err.(*os.SyscallError); ok && errors.Is(opErr.Err, syscall.EADDRINUSE) || errors.Is(sErr.Err, errWindowsAddrInUse) {
|
|
||||||
proxyLog.Warn().Msgf("Address %s already in used, pick a random one", addr)
|
|
||||||
pc, err := net.ListenPacket("udp", net.JoinHostPort(listenerConfig.IP, "0"))
|
|
||||||
if err != nil {
|
|
||||||
proxyLog.Fatal().Err(err).Msg("failed to listen packet")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_, portStr, _ := net.SplitHostPort(pc.LocalAddr().String())
|
|
||||||
port, err := strconv.Atoi(portStr)
|
|
||||||
if err != nil {
|
|
||||||
proxyLog.Fatal().Err(err).Msg("malformed port")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
listenerConfig.Port = port
|
|
||||||
v.Set("listener", map[string]*ctrld.ListenerConfig{
|
|
||||||
"0": {
|
|
||||||
IP: "127.0.0.1",
|
|
||||||
Port: port,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err := writeConfigFile(); err != nil {
|
|
||||||
proxyLog.Fatal().Err(err).Msg("failed to write config file")
|
|
||||||
} else {
|
|
||||||
mainLog.Info().Msg("writing config file to: " + defaultConfigFile)
|
|
||||||
}
|
|
||||||
mainLog.Info().Msgf("Starting DNS server on listener.%s: %s", listenerNum, pc.LocalAddr())
|
|
||||||
// There can be a race between closing the listener and start our own UDP server, but it's
|
|
||||||
// rare, and we only do this once, so let conservative here.
|
|
||||||
if err := pc.Close(); err != nil {
|
|
||||||
proxyLog.Fatal().Err(err).Msg("failed to close packet conn")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := p.serveUDP(listenerNum); err != nil {
|
|
||||||
proxyLog.Fatal().Err(err).Msgf("Unable to start dns proxy on listener.%s", listenerNum)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
proxyLog.Fatal().Err(err).Msgf("Unable to start dns proxy on listener.%s", listenerNum)
|
|
||||||
}(listenerNum)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *prog) Stop(s service.Service) error {
|
|
||||||
if err := p.deAllocateIP(); err != nil {
|
|
||||||
mainLog.Error().Err(err).Msg("de-allocate ip failed")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
mainLog.Info().Msg("Service stopped")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *prog) allocateIP(ip string) error {
|
|
||||||
if !p.cfg.Service.AllocateIP {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return allocateIP(ip)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *prog) deAllocateIP() error {
|
|
||||||
if !p.cfg.Service.AllocateIP {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for _, lc := range p.cfg.Listener {
|
|
||||||
if err := deAllocateIP(lc.IP); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *prog) setDNS() {
|
|
||||||
if cfg.Listener == nil || cfg.Listener["0"] == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if iface == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if iface == "auto" {
|
|
||||||
iface = defaultIfaceName()
|
|
||||||
}
|
|
||||||
logger := mainLog.With().Str("iface", iface).Logger()
|
|
||||||
netIface, err := netInterface(iface)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error().Err(err).Msg("could not get interface")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := setupNetworkManager(); err != nil {
|
|
||||||
logger.Error().Err(err).Msg("could not patch NetworkManager")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
logger.Debug().Msg("setting DNS for interface")
|
|
||||||
if err := setDNS(netIface, []string{cfg.Listener["0"].IP}); err != nil {
|
|
||||||
logger.Error().Err(err).Msgf("could not set DNS for interface")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
logger.Debug().Msg("setting DNS successfully")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *prog) resetDNS() {
|
|
||||||
if iface == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if iface == "auto" {
|
|
||||||
iface = defaultIfaceName()
|
|
||||||
}
|
|
||||||
logger := mainLog.With().Str("iface", iface).Logger()
|
|
||||||
netIface, err := netInterface(iface)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error().Err(err).Msg("could not get interface")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := restoreNetworkManager(); err != nil {
|
|
||||||
logger.Error().Err(err).Msg("could not restore NetworkManager")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
logger.Debug().Msg("Restoring DNS for interface")
|
|
||||||
if err := resetDNS(netIface); err != nil {
|
|
||||||
logger.Error().Err(err).Msgf("could not reset DNS")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
logger.Debug().Msg("Restoring DNS successfully")
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/kardianos/service"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (p *prog) preRun() {
|
|
||||||
if !service.Interactive() {
|
|
||||||
p.setDNS()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func setDependencies(svc *service.Config) {
|
|
||||||
svc.Dependencies = []string{
|
|
||||||
"Wants=network-online.target",
|
|
||||||
"After=network-online.target",
|
|
||||||
"Wants=NetworkManager-wait-online.service",
|
|
||||||
"After=NetworkManager-wait-online.service",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
//go:build !linux
|
|
||||||
// +build !linux
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import "github.com/kardianos/service"
|
|
||||||
|
|
||||||
func (p *prog) preRun() {}
|
|
||||||
|
|
||||||
func setDependencies(svc *service.Config) {}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
func stderrMsg(msg string) {
|
|
||||||
_, _ = fmt.Fprintln(os.Stderr, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func stdoutMsg(msg string) {
|
|
||||||
_, _ = fmt.Fprintln(os.Stdout, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
type task struct {
|
|
||||||
f func() error
|
|
||||||
abortOnError bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func doTasks(tasks []task) bool {
|
|
||||||
for _, task := range tasks {
|
|
||||||
if err := task.f(); err != nil {
|
|
||||||
if task.abortOnError {
|
|
||||||
stderrMsg(err.Error())
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkHasElevatedPrivilege(cmd *cobra.Command, args []string) {
|
|
||||||
ok, err := hasElevatedPrivilege()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("could not detect user privilege: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
fmt.Println("Please relaunch process with admin/root privilege.")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,39 +3,79 @@ package ctrld
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/dnsrcode"
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/lucas-clemente/quic-go"
|
|
||||||
"github.com/lucas-clemente/quic-go/http3"
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
"golang.org/x/sync/singleflight"
|
||||||
|
"tailscale.com/logtail/backoff"
|
||||||
|
|
||||||
|
"github.com/Control-D-Inc/ctrld/internal/dnsrcode"
|
||||||
|
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IpStackBoth ...
|
||||||
|
const (
|
||||||
|
// IpStackBoth indicates that ctrld will use either ipv4 or ipv6 for connecting to upstream,
|
||||||
|
// depending on which stack is available when receiving the DNS query.
|
||||||
|
IpStackBoth = "both"
|
||||||
|
// IpStackV4 indicates that ctrld will use only ipv4 for connecting to upstream.
|
||||||
|
IpStackV4 = "v4"
|
||||||
|
// IpStackV6 indicates that ctrld will use only ipv6 for connecting to upstream.
|
||||||
|
IpStackV6 = "v6"
|
||||||
|
// IpStackSplit indicates that ctrld will use either ipv4 or ipv6 for connecting to upstream,
|
||||||
|
// depending on the record type of the DNS query.
|
||||||
|
IpStackSplit = "split"
|
||||||
|
|
||||||
|
controlDComDomain = "controld.com"
|
||||||
|
controlDNetDomain = "controld.net"
|
||||||
|
controlDDevDomain = "controld.dev"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
controldParentDomains = []string{controlDComDomain, controlDNetDomain, controlDDevDomain}
|
||||||
|
controldVerifiedDomain = map[string]string{
|
||||||
|
controlDComDomain: "verify.controld.com",
|
||||||
|
controlDDevDomain: "verify.controld.dev",
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// SetConfigName set the config name that ctrld will look for.
|
// SetConfigName set the config name that ctrld will look for.
|
||||||
|
// DEPRECATED: use SetConfigNameWithPath instead.
|
||||||
func SetConfigName(v *viper.Viper, name string) {
|
func SetConfigName(v *viper.Viper, name string) {
|
||||||
v.SetConfigName(name)
|
|
||||||
|
|
||||||
configPath := "$HOME"
|
configPath := "$HOME"
|
||||||
// viper has its own way to get user home directory: https://github.com/spf13/viper/blob/v1.14.0/util.go#L134
|
// viper has its own way to get user home directory: https://github.com/spf13/viper/blob/v1.14.0/util.go#L134
|
||||||
// To be consistent, we prefer os.UserHomeDir instead.
|
// To be consistent, we prefer os.UserHomeDir instead.
|
||||||
if homeDir, err := os.UserHomeDir(); err == nil {
|
if homeDir, err := os.UserHomeDir(); err == nil {
|
||||||
configPath = homeDir
|
configPath = homeDir
|
||||||
}
|
}
|
||||||
|
SetConfigNameWithPath(v, name, configPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConfigNameWithPath set the config path and name that ctrld will look for.
|
||||||
|
func SetConfigNameWithPath(v *viper.Viper, name, configPath string) {
|
||||||
|
v.SetConfigName(name)
|
||||||
v.AddConfigPath(configPath)
|
v.AddConfigPath(configPath)
|
||||||
v.AddConfigPath(".")
|
v.AddConfigPath(".")
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitConfig initializes default config values for given *viper.Viper instance.
|
// InitConfig initializes default config values for given *viper.Viper instance.
|
||||||
func InitConfig(v *viper.Viper, name string) {
|
func InitConfig(v *viper.Viper, name string) {
|
||||||
SetConfigName(v, name)
|
|
||||||
|
|
||||||
v.SetDefault("listener", map[string]*ListenerConfig{
|
v.SetDefault("listener", map[string]*ListenerConfig{
|
||||||
"0": {
|
"0": {
|
||||||
IP: "127.0.0.1",
|
IP: "127.0.0.1",
|
||||||
@@ -69,21 +109,77 @@ func InitConfig(v *viper.Viper, name string) {
|
|||||||
// Config represents ctrld supported configuration.
|
// Config represents ctrld supported configuration.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Service ServiceConfig `mapstructure:"service" toml:"service,omitempty"`
|
Service ServiceConfig `mapstructure:"service" toml:"service,omitempty"`
|
||||||
|
Listener map[string]*ListenerConfig `mapstructure:"listener" toml:"listener" validate:"min=1,dive"`
|
||||||
Network map[string]*NetworkConfig `mapstructure:"network" toml:"network" validate:"min=1,dive"`
|
Network map[string]*NetworkConfig `mapstructure:"network" toml:"network" validate:"min=1,dive"`
|
||||||
Upstream map[string]*UpstreamConfig `mapstructure:"upstream" toml:"upstream" validate:"min=1,dive"`
|
Upstream map[string]*UpstreamConfig `mapstructure:"upstream" toml:"upstream" validate:"min=1,dive"`
|
||||||
Listener map[string]*ListenerConfig `mapstructure:"listener" toml:"listener" validate:"min=1,dive"`
|
}
|
||||||
|
|
||||||
|
// HasUpstreamSendClientInfo reports whether the config has any upstream
|
||||||
|
// is configured to send client info to Control D DNS server.
|
||||||
|
func (c *Config) HasUpstreamSendClientInfo() bool {
|
||||||
|
for _, uc := range c.Upstream {
|
||||||
|
if uc.UpstreamSendClientInfo() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// FirstListener returns the first listener config of current config. Listeners are sorted numerically.
|
||||||
|
//
|
||||||
|
// It panics if Config has no listeners configured.
|
||||||
|
func (c *Config) FirstListener() *ListenerConfig {
|
||||||
|
listeners := make([]int, 0, len(c.Listener))
|
||||||
|
for k := range c.Listener {
|
||||||
|
n, err := strconv.Atoi(k)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
listeners = append(listeners, n)
|
||||||
|
}
|
||||||
|
if len(listeners) == 0 {
|
||||||
|
panic("missing listener config")
|
||||||
|
}
|
||||||
|
sort.Ints(listeners)
|
||||||
|
return c.Listener[strconv.Itoa(listeners[0])]
|
||||||
|
}
|
||||||
|
|
||||||
|
// FirstUpstream returns the first upstream of current config. Upstreams are sorted numerically.
|
||||||
|
//
|
||||||
|
// It panics if Config has no upstreams configured.
|
||||||
|
func (c *Config) FirstUpstream() *UpstreamConfig {
|
||||||
|
upstreams := make([]int, 0, len(c.Upstream))
|
||||||
|
for k := range c.Upstream {
|
||||||
|
n, err := strconv.Atoi(k)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
upstreams = append(upstreams, n)
|
||||||
|
}
|
||||||
|
if len(upstreams) == 0 {
|
||||||
|
panic("missing listener config")
|
||||||
|
}
|
||||||
|
sort.Ints(upstreams)
|
||||||
|
return c.Upstream[strconv.Itoa(upstreams[0])]
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceConfig specifies the general ctrld config.
|
// ServiceConfig specifies the general ctrld config.
|
||||||
type ServiceConfig struct {
|
type ServiceConfig struct {
|
||||||
LogLevel string `mapstructure:"log_level" toml:"log_level,omitempty"`
|
LogLevel string `mapstructure:"log_level" toml:"log_level,omitempty"`
|
||||||
LogPath string `mapstructure:"log_path" toml:"log_path,omitempty"`
|
LogPath string `mapstructure:"log_path" toml:"log_path,omitempty"`
|
||||||
CacheEnable bool `mapstructure:"cache_enable" toml:"cache_enable,omitempty"`
|
CacheEnable bool `mapstructure:"cache_enable" toml:"cache_enable,omitempty"`
|
||||||
CacheSize int `mapstructure:"cache_size" toml:"cache_size,omitempty"`
|
CacheSize int `mapstructure:"cache_size" toml:"cache_size,omitempty"`
|
||||||
CacheTTLOverride int `mapstructure:"cache_ttl_override" toml:"cache_ttl_override,omitempty"`
|
CacheTTLOverride int `mapstructure:"cache_ttl_override" toml:"cache_ttl_override,omitempty"`
|
||||||
CacheServeStale bool `mapstructure:"cache_serve_stale" toml:"cache_serve_stale,omitempty"`
|
CacheServeStale bool `mapstructure:"cache_serve_stale" toml:"cache_serve_stale,omitempty"`
|
||||||
Daemon bool `mapstructure:"-" toml:"-"`
|
MaxConcurrentRequests *int `mapstructure:"max_concurrent_requests" toml:"max_concurrent_requests,omitempty" validate:"omitempty,gte=0"`
|
||||||
AllocateIP bool `mapstructure:"-" toml:"-"`
|
DHCPLeaseFile string `mapstructure:"dhcp_lease_file_path" toml:"dhcp_lease_file_path" validate:"omitempty,file"`
|
||||||
|
DHCPLeaseFileFormat string `mapstructure:"dhcp_lease_file_format" toml:"dhcp_lease_file_format" validate:"required_unless=DHCPLeaseFile '',omitempty,oneof=dnsmasq isc-dhcp"`
|
||||||
|
DiscoverMDNS *bool `mapstructure:"discover_mdns" toml:"discover_mdns,omitempty"`
|
||||||
|
DiscoverARP *bool `mapstructure:"discover_arp" toml:"discover_dhcp,omitempty"`
|
||||||
|
DiscoverDHCP *bool `mapstructure:"discover_dhcp" toml:"discover_dhcp,omitempty"`
|
||||||
|
DiscoverPtr *bool `mapstructure:"discover_ptr" toml:"discover_ptr,omitempty"`
|
||||||
|
Daemon bool `mapstructure:"-" toml:"-"`
|
||||||
|
AllocateIP bool `mapstructure:"-" toml:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NetworkConfig specifies configuration for networks where ctrld will handle requests.
|
// NetworkConfig specifies configuration for networks where ctrld will handle requests.
|
||||||
@@ -95,24 +191,59 @@ type NetworkConfig struct {
|
|||||||
|
|
||||||
// UpstreamConfig specifies configuration for upstreams that ctrld will forward requests to.
|
// UpstreamConfig specifies configuration for upstreams that ctrld will forward requests to.
|
||||||
type UpstreamConfig struct {
|
type UpstreamConfig struct {
|
||||||
Name string `mapstructure:"name" toml:"name,omitempty"`
|
Name string `mapstructure:"name" toml:"name,omitempty"`
|
||||||
Type string `mapstructure:"type" toml:"type,omitempty" validate:"oneof=doh doh3 dot doq os legacy"`
|
Type string `mapstructure:"type" toml:"type,omitempty" validate:"oneof=doh doh3 dot doq os legacy"`
|
||||||
Endpoint string `mapstructure:"endpoint" toml:"endpoint,omitempty" validate:"required_unless=Type os"`
|
Endpoint string `mapstructure:"endpoint" toml:"endpoint,omitempty"`
|
||||||
BootstrapIP string `mapstructure:"bootstrap_ip" toml:"bootstrap_ip,omitempty"`
|
BootstrapIP string `mapstructure:"bootstrap_ip" toml:"bootstrap_ip,omitempty"`
|
||||||
Domain string `mapstructure:"-" toml:"-"`
|
Domain string `mapstructure:"-" toml:"-"`
|
||||||
Timeout int `mapstructure:"timeout" toml:"timeout,omitempty" validate:"gte=0"`
|
IPStack string `mapstructure:"ip_stack" toml:"ip_stack,omitempty" validate:"ipstack"`
|
||||||
transport *http.Transport `mapstructure:"-" toml:"-"`
|
Timeout int `mapstructure:"timeout" toml:"timeout,omitempty" validate:"gte=0"`
|
||||||
http3RoundTripper *http3.RoundTripper `mapstructure:"-" toml:"-"`
|
// The caller should not access this field directly.
|
||||||
|
// Use UpstreamSendClientInfo instead.
|
||||||
|
SendClientInfo *bool `mapstructure:"send_client_info" toml:"send_client_info,omitempty"`
|
||||||
|
|
||||||
|
g singleflight.Group
|
||||||
|
rebootstrap atomic.Bool
|
||||||
|
bootstrapIPs []string
|
||||||
|
bootstrapIPs4 []string
|
||||||
|
bootstrapIPs6 []string
|
||||||
|
transport *http.Transport
|
||||||
|
transportOnce sync.Once
|
||||||
|
transport4 *http.Transport
|
||||||
|
transport6 *http.Transport
|
||||||
|
http3RoundTripper http.RoundTripper
|
||||||
|
http3RoundTripper4 http.RoundTripper
|
||||||
|
http3RoundTripper6 http.RoundTripper
|
||||||
|
certPool *x509.CertPool
|
||||||
|
u *url.URL
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListenerConfig specifies the networks configuration that ctrld will run on.
|
// ListenerConfig specifies the networks configuration that ctrld will run on.
|
||||||
type ListenerConfig struct {
|
type ListenerConfig struct {
|
||||||
IP string `mapstructure:"ip" toml:"ip,omitempty" validate:"ip"`
|
IP string `mapstructure:"ip" toml:"ip,omitempty" validate:"iporempty"`
|
||||||
Port int `mapstructure:"port" toml:"port,omitempty" validate:"gt=0"`
|
Port int `mapstructure:"port" toml:"port,omitempty" validate:"gte=0"`
|
||||||
Restricted bool `mapstructure:"restricted" toml:"restricted,omitempty"`
|
Restricted bool `mapstructure:"restricted" toml:"restricted,omitempty"`
|
||||||
Policy *ListenerPolicyConfig `mapstructure:"policy" toml:"policy,omitempty"`
|
Policy *ListenerPolicyConfig `mapstructure:"policy" toml:"policy,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsDirectDnsListener reports whether ctrld can be a direct listener on port 53.
|
||||||
|
// It returns true only if ctrld can listen on port 53 for all interfaces. That means
|
||||||
|
// there's no other software listening on port 53.
|
||||||
|
//
|
||||||
|
// If someone listening on port 53, or ctrld could only listen on port 53 for a specific
|
||||||
|
// interface, ctrld could only be configured as a DNS forwarder.
|
||||||
|
func (lc *ListenerConfig) IsDirectDnsListener() bool {
|
||||||
|
if lc == nil || lc.Port != 53 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch lc.IP {
|
||||||
|
case "", "::", "0.0.0.0":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ListenerPolicyConfig specifies the policy rules for ctrld to filter incoming requests.
|
// ListenerPolicyConfig specifies the policy rules for ctrld to filter incoming requests.
|
||||||
type ListenerPolicyConfig struct {
|
type ListenerPolicyConfig struct {
|
||||||
Name string `mapstructure:"name" toml:"name,omitempty"`
|
Name string `mapstructure:"name" toml:"name,omitempty"`
|
||||||
@@ -131,20 +262,132 @@ type Rule map[string][]string
|
|||||||
func (uc *UpstreamConfig) Init() {
|
func (uc *UpstreamConfig) Init() {
|
||||||
if u, err := url.Parse(uc.Endpoint); err == nil {
|
if u, err := url.Parse(uc.Endpoint); err == nil {
|
||||||
uc.Domain = u.Host
|
uc.Domain = u.Host
|
||||||
|
switch uc.Type {
|
||||||
|
case ResolverTypeDOH, ResolverTypeDOH3:
|
||||||
|
uc.u = u
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if uc.Domain != "" {
|
if uc.Domain == "" {
|
||||||
|
if !strings.Contains(uc.Endpoint, ":") {
|
||||||
|
uc.Domain = uc.Endpoint
|
||||||
|
uc.Endpoint = net.JoinHostPort(uc.Endpoint, defaultPortFor(uc.Type))
|
||||||
|
}
|
||||||
|
host, _, _ := net.SplitHostPort(uc.Endpoint)
|
||||||
|
uc.Domain = host
|
||||||
|
if net.ParseIP(uc.Domain) != nil {
|
||||||
|
uc.BootstrapIP = uc.Domain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if uc.IPStack == "" {
|
||||||
|
if uc.isControlD() {
|
||||||
|
uc.IPStack = IpStackSplit
|
||||||
|
} else {
|
||||||
|
uc.IPStack = IpStackBoth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyDomain returns the domain name that could be resolved by the upstream endpoint.
|
||||||
|
// It returns empty for non-ControlD upstream endpoint.
|
||||||
|
func (uc *UpstreamConfig) VerifyDomain() string {
|
||||||
|
domain := uc.Domain
|
||||||
|
if domain == "" {
|
||||||
|
if u, err := url.Parse(uc.Endpoint); err == nil {
|
||||||
|
domain = u.Hostname()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, parent := range controldParentDomains {
|
||||||
|
if dns.IsSubDomain(parent, domain) {
|
||||||
|
return controldVerifiedDomain[parent]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpstreamSendClientInfo reports whether the upstream is
|
||||||
|
// configured to send client info to Control D DNS server.
|
||||||
|
//
|
||||||
|
// Client info includes:
|
||||||
|
// - MAC
|
||||||
|
// - Lan IP
|
||||||
|
// - Hostname
|
||||||
|
func (uc *UpstreamConfig) UpstreamSendClientInfo() bool {
|
||||||
|
if uc.SendClientInfo != nil {
|
||||||
|
return *uc.SendClientInfo
|
||||||
|
}
|
||||||
|
switch uc.Type {
|
||||||
|
case ResolverTypeDOH, ResolverTypeDOH3:
|
||||||
|
if uc.isControlD() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// BootstrapIPs returns the bootstrap IPs list of upstreams.
|
||||||
|
func (uc *UpstreamConfig) BootstrapIPs() []string {
|
||||||
|
return uc.bootstrapIPs
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCertPool sets the system cert pool used for TLS connections.
|
||||||
|
func (uc *UpstreamConfig) SetCertPool(cp *x509.CertPool) {
|
||||||
|
uc.certPool = cp
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupBootstrapIP manually find all available IPs of the upstream.
|
||||||
|
// The first usable IP will be used as bootstrap IP of the upstream.
|
||||||
|
func (uc *UpstreamConfig) SetupBootstrapIP() {
|
||||||
|
uc.setupBootstrapIP(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupBootstrapIP manually find all available IPs of the upstream.
|
||||||
|
// The first usable IP will be used as bootstrap IP of the upstream.
|
||||||
|
func (uc *UpstreamConfig) setupBootstrapIP(withBootstrapDNS bool) {
|
||||||
|
b := backoff.NewBackoff("setupBootstrapIP", func(format string, args ...any) {}, 10*time.Second)
|
||||||
|
isControlD := uc.isControlD()
|
||||||
|
for {
|
||||||
|
uc.bootstrapIPs = lookupIP(uc.Domain, uc.Timeout, withBootstrapDNS)
|
||||||
|
// For ControlD upstream, the bootstrap IPs could not be RFC 1918 addresses,
|
||||||
|
// filtering them out here to prevent weird behavior.
|
||||||
|
if isControlD {
|
||||||
|
n := 0
|
||||||
|
for _, ip := range uc.bootstrapIPs {
|
||||||
|
netIP := net.ParseIP(ip)
|
||||||
|
if netIP != nil && !netIP.IsPrivate() {
|
||||||
|
uc.bootstrapIPs[n] = ip
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uc.bootstrapIPs = uc.bootstrapIPs[:n]
|
||||||
|
}
|
||||||
|
if len(uc.bootstrapIPs) > 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
ProxyLogger.Load().Warn().Msg("could not resolve bootstrap IPs, retrying...")
|
||||||
|
b.BackOff(context.Background(), errors.New("no bootstrap IPs"))
|
||||||
|
}
|
||||||
|
for _, ip := range uc.bootstrapIPs {
|
||||||
|
if ctrldnet.IsIPv6(ip) {
|
||||||
|
uc.bootstrapIPs6 = append(uc.bootstrapIPs6, ip)
|
||||||
|
} else {
|
||||||
|
uc.bootstrapIPs4 = append(uc.bootstrapIPs4, ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ProxyLogger.Load().Debug().Msgf("bootstrap IPs: %v", uc.bootstrapIPs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReBootstrap re-setup the bootstrap IP and the transport.
|
||||||
|
func (uc *UpstreamConfig) ReBootstrap() {
|
||||||
|
switch uc.Type {
|
||||||
|
case ResolverTypeDOH, ResolverTypeDOH3:
|
||||||
|
default:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
_, _, _ = uc.g.Do("ReBootstrap", func() (any, error) {
|
||||||
if !strings.Contains(uc.Endpoint, ":") {
|
ProxyLogger.Load().Debug().Msg("re-bootstrapping upstream ip")
|
||||||
uc.Domain = uc.Endpoint
|
uc.rebootstrap.Store(true)
|
||||||
uc.Endpoint = net.JoinHostPort(uc.Endpoint, defaultPortFor(uc.Type))
|
return true, nil
|
||||||
}
|
})
|
||||||
host, _, _ := net.SplitHostPort(uc.Endpoint)
|
|
||||||
uc.Domain = host
|
|
||||||
if net.ParseIP(uc.Domain) != nil {
|
|
||||||
uc.BootstrapIP = uc.Domain
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetupTransport initializes the network transport used to connect to upstream server.
|
// SetupTransport initializes the network transport used to connect to upstream server.
|
||||||
@@ -159,66 +402,176 @@ func (uc *UpstreamConfig) SetupTransport() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (uc *UpstreamConfig) setupDOHTransport() {
|
func (uc *UpstreamConfig) setupDOHTransport() {
|
||||||
uc.transport = http.DefaultTransport.(*http.Transport).Clone()
|
switch uc.IPStack {
|
||||||
uc.transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
case IpStackBoth, "":
|
||||||
dialer := &net.Dialer{
|
uc.transport = uc.newDOHTransport(uc.bootstrapIPs)
|
||||||
Timeout: 10 * time.Second,
|
case IpStackV4:
|
||||||
KeepAlive: 10 * time.Second,
|
uc.transport = uc.newDOHTransport(uc.bootstrapIPs4)
|
||||||
|
case IpStackV6:
|
||||||
|
uc.transport = uc.newDOHTransport(uc.bootstrapIPs6)
|
||||||
|
case IpStackSplit:
|
||||||
|
uc.transport4 = uc.newDOHTransport(uc.bootstrapIPs4)
|
||||||
|
if hasIPv6() {
|
||||||
|
uc.transport6 = uc.newDOHTransport(uc.bootstrapIPs6)
|
||||||
|
} else {
|
||||||
|
uc.transport6 = uc.transport4
|
||||||
}
|
}
|
||||||
Log(ctx, ProxyLog.Debug(), "debug dial context %s - %s - %s", addr, network, bootstrapDNS)
|
uc.transport = uc.newDOHTransport(uc.bootstrapIPs)
|
||||||
// if we have a bootstrap ip set, use it to avoid DNS lookup
|
|
||||||
if uc.BootstrapIP != "" {
|
|
||||||
if _, port, _ := net.SplitHostPort(addr); port != "" {
|
|
||||||
addr = net.JoinHostPort(uc.BootstrapIP, port)
|
|
||||||
}
|
|
||||||
Log(ctx, ProxyLog.Debug(), "sending doh request to: %s", addr)
|
|
||||||
}
|
|
||||||
return dialer.DialContext(ctx, network, addr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uc.pingUpstream()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uc *UpstreamConfig) setupDOH3Transport() {
|
func (uc *UpstreamConfig) newDOHTransport(addrs []string) *http.Transport {
|
||||||
uc.http3RoundTripper = &http3.RoundTripper{}
|
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||||
uc.http3RoundTripper.Dial = func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
transport.MaxIdleConnsPerHost = 100
|
||||||
host := addr
|
transport.TLSClientConfig = &tls.Config{
|
||||||
ProxyLog.Debug().Msgf("debug dial context D0H3 %s - %s", addr, bootstrapDNS)
|
RootCAs: uc.certPool,
|
||||||
// if we have a bootstrap ip set, use it to avoid DNS lookup
|
ClientSessionCache: tls.NewLRUClientSessionCache(0),
|
||||||
|
}
|
||||||
|
|
||||||
|
dialerTimeoutMs := 2000
|
||||||
|
if uc.Timeout > 0 && uc.Timeout < dialerTimeoutMs {
|
||||||
|
dialerTimeoutMs = uc.Timeout
|
||||||
|
}
|
||||||
|
dialerTimeout := time.Duration(dialerTimeoutMs) * time.Millisecond
|
||||||
|
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
_, port, _ := net.SplitHostPort(addr)
|
||||||
if uc.BootstrapIP != "" {
|
if uc.BootstrapIP != "" {
|
||||||
if _, port, _ := net.SplitHostPort(addr); port != "" {
|
dialer := net.Dialer{Timeout: dialerTimeout, KeepAlive: dialerTimeout}
|
||||||
addr = net.JoinHostPort(uc.BootstrapIP, port)
|
addr := net.JoinHostPort(uc.BootstrapIP, port)
|
||||||
}
|
Log(ctx, ProxyLogger.Load().Debug(), "sending doh request to: %s", addr)
|
||||||
ProxyLog.Debug().Msgf("sending doh3 request to: %s", addr)
|
return dialer.DialContext(ctx, network, addr)
|
||||||
}
|
}
|
||||||
remoteAddr, err := net.ResolveUDPAddr("udp", addr)
|
pd := &ctrldnet.ParallelDialer{}
|
||||||
|
pd.Timeout = dialerTimeout
|
||||||
|
pd.KeepAlive = dialerTimeout
|
||||||
|
dialAddrs := make([]string, len(addrs))
|
||||||
|
for i := range addrs {
|
||||||
|
dialAddrs[i] = net.JoinHostPort(addrs[i], port)
|
||||||
|
}
|
||||||
|
conn, err := pd.DialContext(ctx, network, dialAddrs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
Log(ctx, ProxyLogger.Load().Debug(), "sending doh request to: %s", conn.RemoteAddr())
|
||||||
udpConn, err := net.ListenUDP("udp", nil)
|
return conn, nil
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return quic.DialEarlyContext(ctx, udpConn, remoteAddr, host, tlsCfg, cfg)
|
|
||||||
}
|
}
|
||||||
|
runtime.SetFinalizer(transport, func(transport *http.Transport) {
|
||||||
uc.pingUpstream()
|
transport.CloseIdleConnections()
|
||||||
|
})
|
||||||
|
return transport
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uc *UpstreamConfig) pingUpstream() {
|
// Ping warms up the connection to DoH/DoH3 upstream.
|
||||||
// Warming up the transport by querying a test packet.
|
func (uc *UpstreamConfig) Ping() {
|
||||||
dnsResolver, err := NewResolver(uc)
|
switch uc.Type {
|
||||||
if err != nil {
|
case ResolverTypeDOH, ResolverTypeDOH3:
|
||||||
ProxyLog.Error().Err(err).Msgf("failed to create resolver for upstream: %s", uc.Name)
|
default:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
msg := new(dns.Msg)
|
|
||||||
msg.SetQuestion(".", dns.TypeNS)
|
ping := func(t http.RoundTripper) {
|
||||||
msg.MsgHdr.RecursionDesired = true
|
if t == nil {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
return
|
||||||
defer cancel()
|
}
|
||||||
_, _ = dnsResolver.Resolve(ctx, msg)
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
req, _ := http.NewRequestWithContext(ctx, "HEAD", uc.Endpoint, nil)
|
||||||
|
resp, _ := t.RoundTrip(req)
|
||||||
|
if resp == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
_, _ = io.Copy(io.Discard, resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, typ := range []uint16{dns.TypeA, dns.TypeAAAA} {
|
||||||
|
switch uc.Type {
|
||||||
|
case ResolverTypeDOH:
|
||||||
|
ping(uc.dohTransport(typ))
|
||||||
|
case ResolverTypeDOH3:
|
||||||
|
ping(uc.doh3Transport(typ))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uc *UpstreamConfig) isControlD() bool {
|
||||||
|
domain := uc.Domain
|
||||||
|
if domain == "" {
|
||||||
|
if u, err := url.Parse(uc.Endpoint); err == nil {
|
||||||
|
domain = u.Hostname()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, parent := range controldParentDomains {
|
||||||
|
if dns.IsSubDomain(parent, domain) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uc *UpstreamConfig) dohTransport(dnsType uint16) http.RoundTripper {
|
||||||
|
uc.transportOnce.Do(func() {
|
||||||
|
uc.SetupTransport()
|
||||||
|
})
|
||||||
|
if uc.rebootstrap.CompareAndSwap(true, false) {
|
||||||
|
uc.SetupTransport()
|
||||||
|
}
|
||||||
|
switch uc.IPStack {
|
||||||
|
case IpStackBoth, IpStackV4, IpStackV6:
|
||||||
|
return uc.transport
|
||||||
|
case IpStackSplit:
|
||||||
|
switch dnsType {
|
||||||
|
case dns.TypeA:
|
||||||
|
return uc.transport4
|
||||||
|
default:
|
||||||
|
return uc.transport6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uc.transport
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uc *UpstreamConfig) bootstrapIPForDNSType(dnsType uint16) string {
|
||||||
|
switch uc.IPStack {
|
||||||
|
case IpStackBoth:
|
||||||
|
return pick(uc.bootstrapIPs)
|
||||||
|
case IpStackV4:
|
||||||
|
return pick(uc.bootstrapIPs4)
|
||||||
|
case IpStackV6:
|
||||||
|
return pick(uc.bootstrapIPs6)
|
||||||
|
case IpStackSplit:
|
||||||
|
switch dnsType {
|
||||||
|
case dns.TypeA:
|
||||||
|
return pick(uc.bootstrapIPs4)
|
||||||
|
default:
|
||||||
|
if hasIPv6() {
|
||||||
|
return pick(uc.bootstrapIPs6)
|
||||||
|
}
|
||||||
|
return pick(uc.bootstrapIPs4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pick(uc.bootstrapIPs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uc *UpstreamConfig) netForDNSType(dnsType uint16) (string, string) {
|
||||||
|
switch uc.IPStack {
|
||||||
|
case IpStackBoth:
|
||||||
|
return "tcp-tls", "udp"
|
||||||
|
case IpStackV4:
|
||||||
|
return "tcp4-tls", "udp4"
|
||||||
|
case IpStackV6:
|
||||||
|
return "tcp6-tls", "udp6"
|
||||||
|
case IpStackSplit:
|
||||||
|
switch dnsType {
|
||||||
|
case dns.TypeA:
|
||||||
|
return "tcp4-tls", "udp4"
|
||||||
|
default:
|
||||||
|
if hasIPv6() {
|
||||||
|
return "tcp6-tls", "udp6"
|
||||||
|
}
|
||||||
|
return "tcp4-tls", "udp4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "tcp-tls", "udp"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init initialized necessary values for an ListenerConfig.
|
// Init initialized necessary values for an ListenerConfig.
|
||||||
@@ -234,6 +587,9 @@ func (lc *ListenerConfig) Init() {
|
|||||||
// ValidateConfig validates the given config.
|
// ValidateConfig validates the given config.
|
||||||
func ValidateConfig(validate *validator.Validate, cfg *Config) error {
|
func ValidateConfig(validate *validator.Validate, cfg *Config) error {
|
||||||
_ = validate.RegisterValidation("dnsrcode", validateDnsRcode)
|
_ = validate.RegisterValidation("dnsrcode", validateDnsRcode)
|
||||||
|
_ = validate.RegisterValidation("ipstack", validateIpStack)
|
||||||
|
_ = validate.RegisterValidation("iporempty", validateIpOrEmpty)
|
||||||
|
validate.RegisterStructValidation(upstreamConfigStructLevelValidation, UpstreamConfig{})
|
||||||
return validate.Struct(cfg)
|
return validate.Struct(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,6 +597,49 @@ func validateDnsRcode(fl validator.FieldLevel) bool {
|
|||||||
return dnsrcode.FromString(fl.Field().String()) != -1
|
return dnsrcode.FromString(fl.Field().String()) != -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateIpStack(fl validator.FieldLevel) bool {
|
||||||
|
switch fl.Field().String() {
|
||||||
|
case IpStackBoth, IpStackV4, IpStackV6, IpStackSplit, "":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateIpOrEmpty(fl validator.FieldLevel) bool {
|
||||||
|
val := fl.Field().String()
|
||||||
|
if val == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return net.ParseIP(val) != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func upstreamConfigStructLevelValidation(sl validator.StructLevel) {
|
||||||
|
uc := sl.Current().Addr().Interface().(*UpstreamConfig)
|
||||||
|
if uc.Type == ResolverTypeOS {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Endpoint is required for non os resolver.
|
||||||
|
if uc.Endpoint == "" {
|
||||||
|
sl.ReportError(uc.Endpoint, "endpoint", "Endpoint", "required_unless", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoH/DoH3 requires endpoint is an HTTP url.
|
||||||
|
if uc.Type == ResolverTypeDOH || uc.Type == ResolverTypeDOH3 {
|
||||||
|
u, err := url.Parse(uc.Endpoint)
|
||||||
|
if err != nil || u.Host == "" {
|
||||||
|
sl.ReportError(uc.Endpoint, "endpoint", "Endpoint", "http_url", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if u.Scheme != "http" && u.Scheme != "https" {
|
||||||
|
sl.ReportError(uc.Endpoint, "endpoint", "Endpoint", "http_url", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func defaultPortFor(typ string) string {
|
func defaultPortFor(typ string) string {
|
||||||
switch typ {
|
switch typ {
|
||||||
case ResolverTypeDOH, ResolverTypeDOH3:
|
case ResolverTypeDOH, ResolverTypeDOH3:
|
||||||
@@ -252,3 +651,31 @@ func defaultPortFor(typ string) string {
|
|||||||
}
|
}
|
||||||
return "53"
|
return "53"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResolverTypeFromEndpoint tries guessing the resolver type with a given endpoint
|
||||||
|
// using following rules:
|
||||||
|
//
|
||||||
|
// - If endpoint is an IP address -> ResolverTypeLegacy
|
||||||
|
// - If endpoint starts with "https://" -> ResolverTypeDOH
|
||||||
|
// - If endpoint starts with "quic://" -> ResolverTypeDOQ
|
||||||
|
// - For anything else -> ResolverTypeDOT
|
||||||
|
func ResolverTypeFromEndpoint(endpoint string) string {
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(endpoint, "https://"):
|
||||||
|
return ResolverTypeDOH
|
||||||
|
case strings.HasPrefix(endpoint, "quic://"):
|
||||||
|
return ResolverTypeDOQ
|
||||||
|
}
|
||||||
|
host := endpoint
|
||||||
|
if strings.Contains(endpoint, ":") {
|
||||||
|
host, _, _ = net.SplitHostPort(host)
|
||||||
|
}
|
||||||
|
if ip := net.ParseIP(host); ip != nil {
|
||||||
|
return ResolverTypeLegacy
|
||||||
|
}
|
||||||
|
return ResolverTypeDOT
|
||||||
|
}
|
||||||
|
|
||||||
|
func pick(s []string) string {
|
||||||
|
return s[rand.Intn(len(s))]
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,283 @@
|
|||||||
|
package ctrld
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUpstreamConfig_SetupBootstrapIP(t *testing.T) {
|
||||||
|
uc := &UpstreamConfig{
|
||||||
|
Name: "test",
|
||||||
|
Type: ResolverTypeDOH,
|
||||||
|
Endpoint: "https://freedns.controld.com/p2",
|
||||||
|
Timeout: 5000,
|
||||||
|
}
|
||||||
|
uc.Init()
|
||||||
|
uc.setupBootstrapIP(false)
|
||||||
|
if len(uc.bootstrapIPs) == 0 {
|
||||||
|
t.Log(nameservers())
|
||||||
|
t.Fatal("could not bootstrap ip without bootstrap DNS")
|
||||||
|
}
|
||||||
|
t.Log(uc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpstreamConfig_Init(t *testing.T) {
|
||||||
|
u1, _ := url.Parse("https://example.com")
|
||||||
|
u2, _ := url.Parse("https://example.com?k=v")
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
uc *UpstreamConfig
|
||||||
|
expected *UpstreamConfig
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"doh+doh3",
|
||||||
|
&UpstreamConfig{
|
||||||
|
Name: "doh",
|
||||||
|
Type: "doh",
|
||||||
|
Endpoint: "https://example.com",
|
||||||
|
BootstrapIP: "",
|
||||||
|
Domain: "",
|
||||||
|
Timeout: 0,
|
||||||
|
},
|
||||||
|
&UpstreamConfig{
|
||||||
|
Name: "doh",
|
||||||
|
Type: "doh",
|
||||||
|
Endpoint: "https://example.com",
|
||||||
|
BootstrapIP: "",
|
||||||
|
Domain: "example.com",
|
||||||
|
Timeout: 0,
|
||||||
|
IPStack: IpStackBoth,
|
||||||
|
u: u1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"doh+doh3 with query param",
|
||||||
|
&UpstreamConfig{
|
||||||
|
Name: "doh",
|
||||||
|
Type: "doh",
|
||||||
|
Endpoint: "https://example.com?k=v",
|
||||||
|
BootstrapIP: "",
|
||||||
|
Domain: "",
|
||||||
|
Timeout: 0,
|
||||||
|
},
|
||||||
|
&UpstreamConfig{
|
||||||
|
Name: "doh",
|
||||||
|
Type: "doh",
|
||||||
|
Endpoint: "https://example.com?k=v",
|
||||||
|
BootstrapIP: "",
|
||||||
|
Domain: "example.com",
|
||||||
|
Timeout: 0,
|
||||||
|
IPStack: IpStackBoth,
|
||||||
|
u: u2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dot+doq",
|
||||||
|
&UpstreamConfig{
|
||||||
|
Name: "dot",
|
||||||
|
Type: "dot",
|
||||||
|
Endpoint: "freedns.controld.com:8853",
|
||||||
|
BootstrapIP: "",
|
||||||
|
Domain: "",
|
||||||
|
Timeout: 0,
|
||||||
|
},
|
||||||
|
&UpstreamConfig{
|
||||||
|
Name: "dot",
|
||||||
|
Type: "dot",
|
||||||
|
Endpoint: "freedns.controld.com:8853",
|
||||||
|
BootstrapIP: "",
|
||||||
|
Domain: "freedns.controld.com",
|
||||||
|
Timeout: 0,
|
||||||
|
IPStack: IpStackSplit,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dot+doq without port",
|
||||||
|
&UpstreamConfig{
|
||||||
|
Name: "dot",
|
||||||
|
Type: "dot",
|
||||||
|
Endpoint: "freedns.controld.com",
|
||||||
|
BootstrapIP: "",
|
||||||
|
Domain: "",
|
||||||
|
Timeout: 0,
|
||||||
|
IPStack: IpStackSplit,
|
||||||
|
},
|
||||||
|
&UpstreamConfig{
|
||||||
|
Name: "dot",
|
||||||
|
Type: "dot",
|
||||||
|
Endpoint: "freedns.controld.com:853",
|
||||||
|
BootstrapIP: "",
|
||||||
|
Domain: "freedns.controld.com",
|
||||||
|
Timeout: 0,
|
||||||
|
IPStack: IpStackSplit,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"legacy",
|
||||||
|
&UpstreamConfig{
|
||||||
|
Name: "legacy",
|
||||||
|
Type: "legacy",
|
||||||
|
Endpoint: "1.2.3.4:53",
|
||||||
|
BootstrapIP: "",
|
||||||
|
Domain: "",
|
||||||
|
Timeout: 0,
|
||||||
|
},
|
||||||
|
&UpstreamConfig{
|
||||||
|
Name: "legacy",
|
||||||
|
Type: "legacy",
|
||||||
|
Endpoint: "1.2.3.4:53",
|
||||||
|
BootstrapIP: "1.2.3.4",
|
||||||
|
Domain: "1.2.3.4",
|
||||||
|
Timeout: 0,
|
||||||
|
IPStack: IpStackBoth,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"legacy without port",
|
||||||
|
&UpstreamConfig{
|
||||||
|
Name: "legacy",
|
||||||
|
Type: "legacy",
|
||||||
|
Endpoint: "1.2.3.4",
|
||||||
|
BootstrapIP: "",
|
||||||
|
Domain: "",
|
||||||
|
Timeout: 0,
|
||||||
|
},
|
||||||
|
&UpstreamConfig{
|
||||||
|
Name: "legacy",
|
||||||
|
Type: "legacy",
|
||||||
|
Endpoint: "1.2.3.4:53",
|
||||||
|
BootstrapIP: "1.2.3.4",
|
||||||
|
Domain: "1.2.3.4",
|
||||||
|
Timeout: 0,
|
||||||
|
IPStack: IpStackBoth,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"doh+doh3 with send client info set",
|
||||||
|
&UpstreamConfig{
|
||||||
|
Name: "doh",
|
||||||
|
Type: "doh",
|
||||||
|
Endpoint: "https://example.com?k=v",
|
||||||
|
BootstrapIP: "",
|
||||||
|
Domain: "",
|
||||||
|
Timeout: 0,
|
||||||
|
SendClientInfo: ptrBool(false),
|
||||||
|
IPStack: IpStackBoth,
|
||||||
|
},
|
||||||
|
&UpstreamConfig{
|
||||||
|
Name: "doh",
|
||||||
|
Type: "doh",
|
||||||
|
Endpoint: "https://example.com?k=v",
|
||||||
|
BootstrapIP: "",
|
||||||
|
Domain: "example.com",
|
||||||
|
Timeout: 0,
|
||||||
|
SendClientInfo: ptrBool(false),
|
||||||
|
IPStack: IpStackBoth,
|
||||||
|
u: u2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
tc.uc.Init()
|
||||||
|
assert.Equal(t, tc.expected, tc.uc)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpstreamConfig_VerifyDomain(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
uc *UpstreamConfig
|
||||||
|
verifyDomain string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
controlDComDomain,
|
||||||
|
&UpstreamConfig{Endpoint: "https://freedns.controld.com/p2"},
|
||||||
|
controldVerifiedDomain[controlDComDomain],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
controlDDevDomain,
|
||||||
|
&UpstreamConfig{Endpoint: "https://freedns.controld.dev/p2"},
|
||||||
|
controldVerifiedDomain[controlDDevDomain],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"non-ControlD upstream",
|
||||||
|
&UpstreamConfig{Endpoint: "https://dns.google/dns-query"},
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if got := tc.uc.VerifyDomain(); got != tc.verifyDomain {
|
||||||
|
t.Errorf("unexpected verify domain, want: %q, got: %q", tc.verifyDomain, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpstreamConfig_UpstreamSendClientInfo(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
uc *UpstreamConfig
|
||||||
|
sendClientInfo bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"default with controld upstream DoH",
|
||||||
|
&UpstreamConfig{Endpoint: "https://freedns.controld.com/p2", Type: ResolverTypeDOH},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default with controld upstream DoH3",
|
||||||
|
&UpstreamConfig{Endpoint: "https://freedns.controld.com/p2", Type: ResolverTypeDOH3},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default with non-ControlD upstream",
|
||||||
|
&UpstreamConfig{Endpoint: "https://dns.google/dns-query", Type: ResolverTypeDOH},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"set false with controld upstream",
|
||||||
|
&UpstreamConfig{Endpoint: "https://freedns.controld.com/p2", Type: ResolverTypeDOH, SendClientInfo: ptrBool(false)},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"set true with controld upstream",
|
||||||
|
&UpstreamConfig{Endpoint: "https://freedns.controld.com/p2", SendClientInfo: ptrBool(true)},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"set false with non-ControlD upstream",
|
||||||
|
&UpstreamConfig{Endpoint: "https://dns.google/dns-query", SendClientInfo: ptrBool(false)},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"set true with non-ControlD upstream",
|
||||||
|
&UpstreamConfig{Endpoint: "https://dns.google/dns-query", Type: ResolverTypeDOH, SendClientInfo: ptrBool(true)},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if got := tc.uc.UpstreamSendClientInfo(); got != tc.sendClientInfo {
|
||||||
|
t.Errorf("unexpected result, want: %v, got: %v", tc.sendClientInfo, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ptrBool(b bool) *bool {
|
||||||
|
return &b
|
||||||
|
}
|
||||||
+154
@@ -0,0 +1,154 @@
|
|||||||
|
//go:build !qf
|
||||||
|
|
||||||
|
package ctrld
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"github.com/quic-go/quic-go"
|
||||||
|
"github.com/quic-go/quic-go/http3"
|
||||||
|
|
||||||
|
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (uc *UpstreamConfig) setupDOH3Transport() {
|
||||||
|
switch uc.IPStack {
|
||||||
|
case IpStackBoth, "":
|
||||||
|
uc.http3RoundTripper = uc.newDOH3Transport(uc.bootstrapIPs)
|
||||||
|
case IpStackV4:
|
||||||
|
uc.http3RoundTripper = uc.newDOH3Transport(uc.bootstrapIPs4)
|
||||||
|
case IpStackV6:
|
||||||
|
uc.http3RoundTripper = uc.newDOH3Transport(uc.bootstrapIPs6)
|
||||||
|
case IpStackSplit:
|
||||||
|
uc.http3RoundTripper4 = uc.newDOH3Transport(uc.bootstrapIPs4)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if ctrldnet.IPv6Available(ctx) {
|
||||||
|
uc.http3RoundTripper6 = uc.newDOH3Transport(uc.bootstrapIPs6)
|
||||||
|
} else {
|
||||||
|
uc.http3RoundTripper6 = uc.http3RoundTripper4
|
||||||
|
}
|
||||||
|
uc.http3RoundTripper = uc.newDOH3Transport(uc.bootstrapIPs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uc *UpstreamConfig) newDOH3Transport(addrs []string) http.RoundTripper {
|
||||||
|
rt := &http3.RoundTripper{}
|
||||||
|
rt.TLSClientConfig = &tls.Config{RootCAs: uc.certPool}
|
||||||
|
rt.Dial = func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
||||||
|
domain := addr
|
||||||
|
_, port, _ := net.SplitHostPort(addr)
|
||||||
|
// if we have a bootstrap ip set, use it to avoid DNS lookup
|
||||||
|
if uc.BootstrapIP != "" {
|
||||||
|
addr = net.JoinHostPort(uc.BootstrapIP, port)
|
||||||
|
ProxyLogger.Load().Debug().Msgf("sending doh3 request to: %s", addr)
|
||||||
|
udpConn, err := net.ListenUDP("udp", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
remoteAddr, err := net.ResolveUDPAddr("udp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return quic.DialEarlyContext(ctx, udpConn, remoteAddr, domain, tlsCfg, cfg)
|
||||||
|
}
|
||||||
|
dialAddrs := make([]string, len(addrs))
|
||||||
|
for i := range addrs {
|
||||||
|
dialAddrs[i] = net.JoinHostPort(addrs[i], port)
|
||||||
|
}
|
||||||
|
pd := &quicParallelDialer{}
|
||||||
|
conn, err := pd.Dial(ctx, domain, dialAddrs, tlsCfg, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ProxyLogger.Load().Debug().Msgf("sending doh3 request to: %s", conn.RemoteAddr())
|
||||||
|
return conn, err
|
||||||
|
}
|
||||||
|
return rt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uc *UpstreamConfig) doh3Transport(dnsType uint16) http.RoundTripper {
|
||||||
|
uc.transportOnce.Do(func() {
|
||||||
|
uc.SetupTransport()
|
||||||
|
})
|
||||||
|
if uc.rebootstrap.CompareAndSwap(true, false) {
|
||||||
|
uc.SetupTransport()
|
||||||
|
}
|
||||||
|
switch uc.IPStack {
|
||||||
|
case IpStackBoth, IpStackV4, IpStackV6:
|
||||||
|
return uc.http3RoundTripper
|
||||||
|
case IpStackSplit:
|
||||||
|
switch dnsType {
|
||||||
|
case dns.TypeA:
|
||||||
|
return uc.http3RoundTripper4
|
||||||
|
default:
|
||||||
|
return uc.http3RoundTripper6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uc.http3RoundTripper
|
||||||
|
}
|
||||||
|
|
||||||
|
// Putting the code for quic parallel dialer here:
|
||||||
|
//
|
||||||
|
// - quic dialer is different with net.Dialer
|
||||||
|
// - simplification for quic free version
|
||||||
|
type parallelDialerResult struct {
|
||||||
|
conn quic.EarlyConnection
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type quicParallelDialer struct{}
|
||||||
|
|
||||||
|
// Dial performs parallel dialing to the given address list.
|
||||||
|
func (d *quicParallelDialer) Dial(ctx context.Context, domain string, addrs []string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
||||||
|
if len(addrs) == 0 {
|
||||||
|
return nil, errors.New("empty addresses")
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
ch := make(chan *parallelDialerResult, len(addrs))
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(len(addrs))
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(ch)
|
||||||
|
}()
|
||||||
|
|
||||||
|
udpConn, err := net.ListenUDP("udp", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, addr := range addrs {
|
||||||
|
go func(addr string) {
|
||||||
|
defer wg.Done()
|
||||||
|
remoteAddr, err := net.ResolveUDPAddr("udp", addr)
|
||||||
|
if err != nil {
|
||||||
|
ch <- ¶llelDialerResult{conn: nil, err: err}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := quic.DialEarlyContext(ctx, udpConn, remoteAddr, domain, tlsCfg, cfg)
|
||||||
|
ch <- ¶llelDialerResult{conn: conn, err: err}
|
||||||
|
}(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
errs := make([]error, 0, len(addrs))
|
||||||
|
for res := range ch {
|
||||||
|
if res.err == nil {
|
||||||
|
cancel()
|
||||||
|
return res.conn, res.err
|
||||||
|
}
|
||||||
|
errs = append(errs, res.err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.Join(errs...)
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
//go:build qf
|
||||||
|
|
||||||
|
package ctrld
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
func (uc *UpstreamConfig) setupDOH3Transport() {}
|
||||||
|
|
||||||
|
func (uc *UpstreamConfig) doh3Transport(dnsType uint16) http.RoundTripper { return nil }
|
||||||
+68
-113
@@ -1,6 +1,8 @@
|
|||||||
package ctrld_test
|
package ctrld_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
@@ -24,10 +26,12 @@ func TestLoadConfig(t *testing.T) {
|
|||||||
assert.Contains(t, cfg.Network, "0")
|
assert.Contains(t, cfg.Network, "0")
|
||||||
assert.Contains(t, cfg.Network, "1")
|
assert.Contains(t, cfg.Network, "1")
|
||||||
|
|
||||||
assert.Len(t, cfg.Upstream, 3)
|
assert.Len(t, cfg.Upstream, 4)
|
||||||
assert.Contains(t, cfg.Upstream, "0")
|
assert.Contains(t, cfg.Upstream, "0")
|
||||||
assert.Contains(t, cfg.Upstream, "1")
|
assert.Contains(t, cfg.Upstream, "1")
|
||||||
assert.Contains(t, cfg.Upstream, "2")
|
assert.Contains(t, cfg.Upstream, "2")
|
||||||
|
assert.Contains(t, cfg.Upstream, "3")
|
||||||
|
assert.NotNil(t, cfg.Upstream["3"].SendClientInfo)
|
||||||
|
|
||||||
assert.Len(t, cfg.Listener, 2)
|
assert.Len(t, cfg.Listener, 2)
|
||||||
assert.Contains(t, cfg.Listener, "0")
|
assert.Contains(t, cfg.Listener, "0")
|
||||||
@@ -42,6 +46,8 @@ func TestLoadConfig(t *testing.T) {
|
|||||||
assert.Len(t, cfg.Listener["0"].Policy.Rules, 2)
|
assert.Len(t, cfg.Listener["0"].Policy.Rules, 2)
|
||||||
assert.Contains(t, cfg.Listener["0"].Policy.Rules[0], "*.ru")
|
assert.Contains(t, cfg.Listener["0"].Policy.Rules[0], "*.ru")
|
||||||
assert.Contains(t, cfg.Listener["0"].Policy.Rules[1], "*.local.host")
|
assert.Contains(t, cfg.Listener["0"].Policy.Rules[1], "*.local.host")
|
||||||
|
|
||||||
|
assert.True(t, cfg.HasUpstreamSendClientInfo())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadDefaultConfig(t *testing.T) {
|
func TestLoadDefaultConfig(t *testing.T) {
|
||||||
@@ -52,6 +58,20 @@ func TestLoadDefaultConfig(t *testing.T) {
|
|||||||
assert.Len(t, cfg.Upstream, 2)
|
assert.Len(t, cfg.Upstream, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfigOverride(t *testing.T) {
|
||||||
|
v := viper.NewWithOptions(viper.KeyDelimiter("::"))
|
||||||
|
ctrld.InitConfig(v, "test_load_config")
|
||||||
|
v.SetConfigType("toml")
|
||||||
|
require.NoError(t, v.ReadConfig(strings.NewReader(testhelper.SampleConfigStr(t))))
|
||||||
|
cfg := ctrld.Config{Listener: map[string]*ctrld.ListenerConfig{
|
||||||
|
"0": {IP: "127.0.0.1", Port: 53},
|
||||||
|
}}
|
||||||
|
require.NoError(t, v.Unmarshal(&cfg))
|
||||||
|
|
||||||
|
assert.Equal(t, "10.10.42.69", cfg.Listener["1"].IP)
|
||||||
|
assert.Equal(t, 1337, cfg.Listener["1"].Port)
|
||||||
|
}
|
||||||
|
|
||||||
func TestConfigValidation(t *testing.T) {
|
func TestConfigValidation(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -61,6 +81,7 @@ func TestConfigValidation(t *testing.T) {
|
|||||||
{"invalid Config", &ctrld.Config{}, true},
|
{"invalid Config", &ctrld.Config{}, true},
|
||||||
{"default Config", defaultConfig(t), false},
|
{"default Config", defaultConfig(t), false},
|
||||||
{"sample Config", testhelper.SampleConfig(t), false},
|
{"sample Config", testhelper.SampleConfig(t), false},
|
||||||
|
{"empty listener IP", emptyListenerIP(t), false},
|
||||||
{"invalid cidr", invalidNetworkConfig(t), true},
|
{"invalid cidr", invalidNetworkConfig(t), true},
|
||||||
{"invalid upstream type", invalidUpstreamType(t), true},
|
{"invalid upstream type", invalidUpstreamType(t), true},
|
||||||
{"invalid upstream timeout", invalidUpstreamTimeout(t), true},
|
{"invalid upstream timeout", invalidUpstreamTimeout(t), true},
|
||||||
@@ -70,6 +91,11 @@ func TestConfigValidation(t *testing.T) {
|
|||||||
{"os upstream", configWithOsUpstream(t), false},
|
{"os upstream", configWithOsUpstream(t), false},
|
||||||
{"invalid rules", configWithInvalidRules(t), true},
|
{"invalid rules", configWithInvalidRules(t), true},
|
||||||
{"invalid dns rcodes", configWithInvalidRcodes(t), true},
|
{"invalid dns rcodes", configWithInvalidRcodes(t), true},
|
||||||
|
{"invalid max concurrent requests", configWithInvalidMaxConcurrentRequests(t), true},
|
||||||
|
{"non-existed lease file", configWithNonExistedLeaseFile(t), true},
|
||||||
|
{"lease file format required if lease file exist", configWithExistedLeaseFile(t), true},
|
||||||
|
{"invalid lease file format", configWithInvalidLeaseFileFormat(t), true},
|
||||||
|
{"invalid doh/doh3 endpoint", configWithInvalidDoHEndpoint(t), true},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
@@ -130,9 +156,15 @@ func invalidListenerIP(t *testing.T) *ctrld.Config {
|
|||||||
return cfg
|
return cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func emptyListenerIP(t *testing.T) *ctrld.Config {
|
||||||
|
cfg := defaultConfig(t)
|
||||||
|
cfg.Listener["0"].IP = ""
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
func invalidListenerPort(t *testing.T) *ctrld.Config {
|
func invalidListenerPort(t *testing.T) *ctrld.Config {
|
||||||
cfg := defaultConfig(t)
|
cfg := defaultConfig(t)
|
||||||
cfg.Listener["0"].Port = 0
|
cfg.Listener["0"].Port = -1
|
||||||
return cfg
|
return cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,115 +198,38 @@ func configWithInvalidRcodes(t *testing.T) *ctrld.Config {
|
|||||||
return cfg
|
return cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUpstreamConfig_Init(t *testing.T) {
|
func configWithInvalidMaxConcurrentRequests(t *testing.T) *ctrld.Config {
|
||||||
tests := []struct {
|
cfg := defaultConfig(t)
|
||||||
name string
|
n := -1
|
||||||
uc *ctrld.UpstreamConfig
|
cfg.Service.MaxConcurrentRequests = &n
|
||||||
expected *ctrld.UpstreamConfig
|
return cfg
|
||||||
}{
|
}
|
||||||
{
|
|
||||||
"doh+doh3",
|
func configWithNonExistedLeaseFile(t *testing.T) *ctrld.Config {
|
||||||
&ctrld.UpstreamConfig{
|
cfg := defaultConfig(t)
|
||||||
Name: "doh",
|
cfg.Service.DHCPLeaseFile = "non-existed"
|
||||||
Type: "doh",
|
return cfg
|
||||||
Endpoint: "https://example.com",
|
}
|
||||||
BootstrapIP: "",
|
|
||||||
Domain: "",
|
func configWithExistedLeaseFile(t *testing.T) *ctrld.Config {
|
||||||
Timeout: 0,
|
cfg := defaultConfig(t)
|
||||||
},
|
exe, err := os.Executable()
|
||||||
&ctrld.UpstreamConfig{
|
if err != nil {
|
||||||
Name: "doh",
|
t.Fatal(err)
|
||||||
Type: "doh",
|
}
|
||||||
Endpoint: "https://example.com",
|
cfg.Service.DHCPLeaseFile = exe
|
||||||
BootstrapIP: "",
|
return cfg
|
||||||
Domain: "example.com",
|
}
|
||||||
Timeout: 0,
|
|
||||||
},
|
func configWithInvalidLeaseFileFormat(t *testing.T) *ctrld.Config {
|
||||||
},
|
cfg := defaultConfig(t)
|
||||||
{
|
cfg.Service.DHCPLeaseFileFormat = "invalid"
|
||||||
"dot+doq",
|
return cfg
|
||||||
&ctrld.UpstreamConfig{
|
}
|
||||||
Name: "dot",
|
|
||||||
Type: "dot",
|
func configWithInvalidDoHEndpoint(t *testing.T) *ctrld.Config {
|
||||||
Endpoint: "freedns.controld.com:8853",
|
cfg := defaultConfig(t)
|
||||||
BootstrapIP: "",
|
cfg.Upstream["0"].Endpoint = "1.1.1.1"
|
||||||
Domain: "",
|
cfg.Upstream["0"].Type = ctrld.ResolverTypeDOH
|
||||||
Timeout: 0,
|
return cfg
|
||||||
},
|
|
||||||
&ctrld.UpstreamConfig{
|
|
||||||
Name: "dot",
|
|
||||||
Type: "dot",
|
|
||||||
Endpoint: "freedns.controld.com:8853",
|
|
||||||
BootstrapIP: "",
|
|
||||||
Domain: "freedns.controld.com",
|
|
||||||
Timeout: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dot+doq without port",
|
|
||||||
&ctrld.UpstreamConfig{
|
|
||||||
Name: "dot",
|
|
||||||
Type: "dot",
|
|
||||||
Endpoint: "freedns.controld.com",
|
|
||||||
BootstrapIP: "",
|
|
||||||
Domain: "",
|
|
||||||
Timeout: 0,
|
|
||||||
},
|
|
||||||
&ctrld.UpstreamConfig{
|
|
||||||
Name: "dot",
|
|
||||||
Type: "dot",
|
|
||||||
Endpoint: "freedns.controld.com:853",
|
|
||||||
BootstrapIP: "",
|
|
||||||
Domain: "freedns.controld.com",
|
|
||||||
Timeout: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"legacy",
|
|
||||||
&ctrld.UpstreamConfig{
|
|
||||||
Name: "legacy",
|
|
||||||
Type: "legacy",
|
|
||||||
Endpoint: "1.2.3.4:53",
|
|
||||||
BootstrapIP: "",
|
|
||||||
Domain: "",
|
|
||||||
Timeout: 0,
|
|
||||||
},
|
|
||||||
&ctrld.UpstreamConfig{
|
|
||||||
Name: "legacy",
|
|
||||||
Type: "legacy",
|
|
||||||
Endpoint: "1.2.3.4:53",
|
|
||||||
BootstrapIP: "1.2.3.4",
|
|
||||||
Domain: "1.2.3.4",
|
|
||||||
Timeout: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"legacy without port",
|
|
||||||
&ctrld.UpstreamConfig{
|
|
||||||
Name: "legacy",
|
|
||||||
Type: "legacy",
|
|
||||||
Endpoint: "1.2.3.4",
|
|
||||||
BootstrapIP: "",
|
|
||||||
Domain: "",
|
|
||||||
Timeout: 0,
|
|
||||||
},
|
|
||||||
&ctrld.UpstreamConfig{
|
|
||||||
Name: "legacy",
|
|
||||||
Type: "legacy",
|
|
||||||
Endpoint: "1.2.3.4:53",
|
|
||||||
BootstrapIP: "1.2.3.4",
|
|
||||||
Domain: "1.2.3.4",
|
|
||||||
Timeout: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
tc.uc.Init()
|
|
||||||
assert.Equal(t, tc.expected, tc.uc)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+124
-26
@@ -14,10 +14,15 @@ The config file allows for advanced configuration of the `ctrld` utility to cove
|
|||||||
|
|
||||||
|
|
||||||
## Config Location
|
## Config Location
|
||||||
`ctrld` uses [TOML](toml_link) format for its configuration file. Default configuration file is `config.toml` found in following order:
|
`ctrld` uses [TOML](toml_link) format for its configuration file. Default configuration file is `ctrld.toml` found in following order:
|
||||||
|
|
||||||
- `$HOME/.ctrld`
|
- `/etc/controld` on *nix.
|
||||||
- Current directory
|
- User's home directory on Windows.
|
||||||
|
- Same directory with `ctrld` binary on these routers:
|
||||||
|
- `ddwrt`
|
||||||
|
- `merlin`
|
||||||
|
- `freshtomato`
|
||||||
|
- Current directory.
|
||||||
|
|
||||||
The user can choose to override default value using command line `--config` or `-c`:
|
The user can choose to override default value using command line `--config` or `-c`:
|
||||||
|
|
||||||
@@ -38,6 +43,8 @@ if it's existed.
|
|||||||
log_path = ""
|
log_path = ""
|
||||||
cache_enable = true
|
cache_enable = true
|
||||||
cache_size = 4096
|
cache_size = 4096
|
||||||
|
cache_ttl_override = 60
|
||||||
|
cache_serve_stale = true
|
||||||
|
|
||||||
[network.0]
|
[network.0]
|
||||||
cidrs = ["0.0.0.0/0"]
|
cidrs = ["0.0.0.0/0"]
|
||||||
@@ -53,6 +60,7 @@ if it's existed.
|
|||||||
name = "Control D - Anti-Malware"
|
name = "Control D - Anti-Malware"
|
||||||
timeout = 5000
|
timeout = 5000
|
||||||
type = "doh"
|
type = "doh"
|
||||||
|
ip_stack = "both"
|
||||||
|
|
||||||
[upstream.1]
|
[upstream.1]
|
||||||
bootstrap_ip = "76.76.2.11"
|
bootstrap_ip = "76.76.2.11"
|
||||||
@@ -60,6 +68,7 @@ if it's existed.
|
|||||||
name = "Control D - No Ads"
|
name = "Control D - No Ads"
|
||||||
timeout = 5000
|
timeout = 5000
|
||||||
type = "doq"
|
type = "doq"
|
||||||
|
ip_stack = "split"
|
||||||
|
|
||||||
[upstream.2]
|
[upstream.2]
|
||||||
bootstrap_ip = "76.76.2.22"
|
bootstrap_ip = "76.76.2.22"
|
||||||
@@ -67,6 +76,7 @@ if it's existed.
|
|||||||
name = "Control D - Private"
|
name = "Control D - Private"
|
||||||
timeout = 5000
|
timeout = 5000
|
||||||
type = "dot"
|
type = "dot"
|
||||||
|
ip_stack = "v4"
|
||||||
|
|
||||||
[listener.0]
|
[listener.0]
|
||||||
ip = "127.0.0.1"
|
ip = "127.0.0.1"
|
||||||
@@ -104,8 +114,8 @@ Logging level you wish to enable.
|
|||||||
|
|
||||||
- Type: string
|
- Type: string
|
||||||
- Required: no
|
- Required: no
|
||||||
- Valid values: `debug`, `info`, `warn`, `error`, `fatal`, `panic`
|
- Valid values: `debug`, `info`, `warn`, `notice`, `error`, `fatal`, `panic`
|
||||||
- Default: `info`
|
- Default: `notice`
|
||||||
|
|
||||||
|
|
||||||
### log_path
|
### log_path
|
||||||
@@ -113,12 +123,14 @@ Relative or absolute path of the log file.
|
|||||||
|
|
||||||
- Type: string
|
- Type: string
|
||||||
- Required: no
|
- Required: no
|
||||||
|
- Default: ""
|
||||||
|
|
||||||
### cache_enable
|
### cache_enable
|
||||||
When `cache_enable = true`, all resolved DNS query responses will be cached for duration of the upstream record TTLs.
|
When `cache_enable = true`, all resolved DNS query responses will be cached for duration of the upstream record TTLs.
|
||||||
|
|
||||||
- Type: boolean
|
- Type: boolean
|
||||||
- Required: no
|
- Required: no
|
||||||
|
- Default: false
|
||||||
|
|
||||||
### cache_size
|
### cache_size
|
||||||
The number of cached records, must be a positive integer. Tweaking this value with care depends on your available RAM.
|
The number of cached records, must be a positive integer. Tweaking this value with care depends on your available RAM.
|
||||||
@@ -128,29 +140,73 @@ An invalid `cache_size` value will disable the cache, regardless of `cache_enabl
|
|||||||
|
|
||||||
- Type: int
|
- Type: int
|
||||||
- Required: no
|
- Required: no
|
||||||
|
- Default: 4096
|
||||||
|
|
||||||
### cache_ttl_override
|
### cache_ttl_override
|
||||||
When `cache_ttl_override` is set to a positive value (in seconds), TTLs are overridden to this value and cached for this long.
|
When `cache_ttl_override` is set to a positive value (in seconds), TTLs are overridden to this value and cached for this long.
|
||||||
|
|
||||||
- Type: int
|
- Type: int
|
||||||
- Required: no
|
- Required: no
|
||||||
|
- Default: 0
|
||||||
|
|
||||||
### cache_serve_stale
|
### cache_serve_stale
|
||||||
When `cache_serve_stale = true`, in cases of upstream failures (upstreams not reachable), `ctrld` will keep serving
|
When `cache_serve_stale = true`, in cases of upstream failures (upstreams not reachable), `ctrld` will keep serving
|
||||||
stale cached records (regardless of their TTLs) until upstream comes online.
|
stale cached records (regardless of their TTLs) until upstream comes online.
|
||||||
|
|
||||||
The above config will look like this at query time.
|
- Type: boolean
|
||||||
|
- Required: no
|
||||||
|
- Default: false
|
||||||
|
|
||||||
```
|
### max_concurrent_requests
|
||||||
2022-11-14T22:18:53.808 INF Setting bootstrap IP for upstream.0 bootstrap_ip=76.76.2.11
|
The number of concurrent requests that will be handled, must be a non-negative integer.
|
||||||
2022-11-14T22:18:53.808 INF Starting DNS server on listener.0: 127.0.0.1:53
|
Tweaking this value depends on the capacity of your system.
|
||||||
2022-11-14T22:18:56.381 DBG [9fd5d3] 127.0.0.1:53978 -> listener.0: 127.0.0.1:53: received query: verify.controld.com
|
|
||||||
2022-11-14T22:18:56.381 INF [9fd5d3] no policy, no network, no rule -> [upstream.0]
|
- Type: number
|
||||||
2022-11-14T22:18:56.381 DBG [9fd5d3] sending query to upstream.0: Control D - DOH Free
|
- Required: no
|
||||||
2022-11-14T22:18:56.381 DBG [9fd5d3] debug dial context freedns.controld.com:443 - tcp - 76.76.2.0
|
- Default: 256
|
||||||
2022-11-14T22:18:56.381 DBG [9fd5d3] sending doh request to: 76.76.2.11:443
|
|
||||||
2022-11-14T22:18:56.420 DBG [9fd5d3] received response of 118 bytes in 39.662597ms
|
### discover_mdns
|
||||||
```
|
Perform LAN client discovery using mDNS. This will spawn a listener on port 5353.
|
||||||
|
|
||||||
|
- Type: boolean
|
||||||
|
- Required: no
|
||||||
|
- Default: true
|
||||||
|
|
||||||
|
### discover_arp
|
||||||
|
Perform LAN client discovery using ARP.
|
||||||
|
|
||||||
|
- Type: boolean
|
||||||
|
- Required: no
|
||||||
|
- Default: true
|
||||||
|
|
||||||
|
### discover_dhcp
|
||||||
|
Perform LAN client discovery using DHCP leases files. Common file locations are auto-discovered.
|
||||||
|
|
||||||
|
- Type: boolean
|
||||||
|
- Required: no
|
||||||
|
- Default: true
|
||||||
|
|
||||||
|
### discover_ptr
|
||||||
|
Perform LAN client discovery using PTR queries.
|
||||||
|
|
||||||
|
- Type: boolean
|
||||||
|
- Required: no
|
||||||
|
- Default: true
|
||||||
|
|
||||||
|
### dhcp_lease_file_path
|
||||||
|
Relative or absolute path to a custom DHCP leases file location.
|
||||||
|
|
||||||
|
- Type: string
|
||||||
|
- Required: no
|
||||||
|
- Default: ""
|
||||||
|
|
||||||
|
### dhcp_lease_file_format
|
||||||
|
DHCP leases file format.
|
||||||
|
|
||||||
|
- Type: string
|
||||||
|
- Required: no
|
||||||
|
- Valid values: `dnsmasq`, `isc-dhcp`
|
||||||
|
- Default: ""
|
||||||
|
|
||||||
## Upstream
|
## Upstream
|
||||||
The `[upstream]` section specifies the DNS upstream servers that `ctrld` will forward DNS requests to.
|
The `[upstream]` section specifies the DNS upstream servers that `ctrld` will forward DNS requests to.
|
||||||
@@ -162,6 +218,7 @@ The `[upstream]` section specifies the DNS upstream servers that `ctrld` will fo
|
|||||||
name = "Control D - DOH"
|
name = "Control D - DOH"
|
||||||
timeout = 5000
|
timeout = 5000
|
||||||
type = "doh"
|
type = "doh"
|
||||||
|
ip_stack = "split"
|
||||||
|
|
||||||
[upstream.1]
|
[upstream.1]
|
||||||
bootstrap_ip = ""
|
bootstrap_ip = ""
|
||||||
@@ -169,6 +226,7 @@ The `[upstream]` section specifies the DNS upstream servers that `ctrld` will fo
|
|||||||
name = "Control D - DOH3"
|
name = "Control D - DOH3"
|
||||||
timeout = 5000
|
timeout = 5000
|
||||||
type = "doh3"
|
type = "doh3"
|
||||||
|
ip_stack = "both"
|
||||||
|
|
||||||
[upstream.2]
|
[upstream.2]
|
||||||
bootstrap_ip = ""
|
bootstrap_ip = ""
|
||||||
@@ -176,6 +234,7 @@ The `[upstream]` section specifies the DNS upstream servers that `ctrld` will fo
|
|||||||
name = "Controld D - DOT"
|
name = "Controld D - DOT"
|
||||||
timeout = 5000
|
timeout = 5000
|
||||||
type = "dot"
|
type = "dot"
|
||||||
|
ip_stack = "v4"
|
||||||
|
|
||||||
[upstream.3]
|
[upstream.3]
|
||||||
bootstrap_ip = ""
|
bootstrap_ip = ""
|
||||||
@@ -183,6 +242,7 @@ The `[upstream]` section specifies the DNS upstream servers that `ctrld` will fo
|
|||||||
name = "Controld D - DOT"
|
name = "Controld D - DOT"
|
||||||
timeout = 5000
|
timeout = 5000
|
||||||
type = "doq"
|
type = "doq"
|
||||||
|
ip_stack = "v6"
|
||||||
|
|
||||||
[upstream.4]
|
[upstream.4]
|
||||||
bootstrap_ip = ""
|
bootstrap_ip = ""
|
||||||
@@ -190,6 +250,7 @@ The `[upstream]` section specifies the DNS upstream servers that `ctrld` will fo
|
|||||||
name = "Control D - Ad Blocking"
|
name = "Control D - Ad Blocking"
|
||||||
timeout = 5000
|
timeout = 5000
|
||||||
type = "legacy"
|
type = "legacy"
|
||||||
|
ip_stack = "both"
|
||||||
```
|
```
|
||||||
|
|
||||||
### bootstrap_ip
|
### bootstrap_ip
|
||||||
@@ -200,6 +261,7 @@ If `bootstrap_ip` is empty, `ctrld` will resolve this itself using its own boots
|
|||||||
|
|
||||||
- type: ip address string
|
- type: ip address string
|
||||||
- required: no
|
- required: no
|
||||||
|
- Default: ""
|
||||||
|
|
||||||
### endpoint
|
### endpoint
|
||||||
IP address, hostname or URL of upstream DNS. Used together with `Type` of the endpoint.
|
IP address, hostname or URL of upstream DNS. Used together with `Type` of the endpoint.
|
||||||
@@ -214,6 +276,7 @@ Human-readable name of the upstream.
|
|||||||
|
|
||||||
- Type: string
|
- Type: string
|
||||||
- Required: no
|
- Required: no
|
||||||
|
- Default: ""
|
||||||
|
|
||||||
### timeout
|
### timeout
|
||||||
Timeout in milliseconds before request failsover to the next upstream (if defined).
|
Timeout in milliseconds before request failsover to the next upstream (if defined).
|
||||||
@@ -221,15 +284,34 @@ Timeout in milliseconds before request failsover to the next upstream (if define
|
|||||||
Value `0` means no timeout.
|
Value `0` means no timeout.
|
||||||
|
|
||||||
- Type: number
|
- Type: number
|
||||||
- required: no
|
- Required: no
|
||||||
|
- Default: 0
|
||||||
|
|
||||||
### type
|
### type
|
||||||
The protocol that `ctrld` will use to send DNS requests to upstream.
|
The protocol that `ctrld` will use to send DNS requests to upstream.
|
||||||
|
|
||||||
- Type: string
|
- Type: string
|
||||||
- required: yes
|
- Required: yes
|
||||||
- Valid values: `doh`, `doh3`, `dot`, `doq`, `legacy`, `os`
|
- Valid values: `doh`, `doh3`, `dot`, `doq`, `legacy`, `os`
|
||||||
|
|
||||||
|
### ip_stack
|
||||||
|
Specifying what kind of ip stack that `ctrld` will use to connect to upstream.
|
||||||
|
|
||||||
|
- Type: string
|
||||||
|
- Required: no
|
||||||
|
- Valid values:
|
||||||
|
- `both`: using either ipv4 or ipv6.
|
||||||
|
- `v4`: only dial upstream via IPv4, never dial IPv6.
|
||||||
|
- `v6`: only dial upstream via IPv6, never dial IPv4.
|
||||||
|
- `split`:
|
||||||
|
- If `A` record is requested -> dial via ipv4.
|
||||||
|
- If `AAAA` or any other record is requested -> dial ipv6 (if available, otherwise ipv4)
|
||||||
|
|
||||||
|
If `ip_stack` is empty, or undefined:
|
||||||
|
|
||||||
|
- Default value is `both` for non-Control D resolvers.
|
||||||
|
- Default value is `split` for Control D resolvers.
|
||||||
|
|
||||||
## Network
|
## Network
|
||||||
The `[network]` section defines networks from which DNS queries can originate from. These are used in policies. You can define multiple networks, and each one can have multiple cidrs.
|
The `[network]` section defines networks from which DNS queries can originate from. These are used in policies. You can define multiple networks, and each one can have multiple cidrs.
|
||||||
|
|
||||||
@@ -248,12 +330,14 @@ Name of the network.
|
|||||||
|
|
||||||
- Type: string
|
- Type: string
|
||||||
- Required: no
|
- Required: no
|
||||||
|
- Default: ""
|
||||||
|
|
||||||
### cidrs
|
### cidrs
|
||||||
Specifies the network addresses that the `listener` will accept requests from. You will see more details in the listener policy section.
|
Specifies the network addresses that the `listener` will accept requests from. You will see more details in the listener policy section.
|
||||||
|
|
||||||
- Type: array of network CIDR string
|
- Type: array of network CIDR string
|
||||||
- Required: no
|
- Required: no
|
||||||
|
- Default: []
|
||||||
|
|
||||||
|
|
||||||
## listener
|
## listener
|
||||||
@@ -271,22 +355,25 @@ The `[listener]` section specifies the ip and port of the local DNS server. You
|
|||||||
```
|
```
|
||||||
|
|
||||||
### ip
|
### ip
|
||||||
IP address that serves the incoming requests.
|
IP address that serves the incoming requests. If `ip` is empty, ctrld will listen on all available addresses.
|
||||||
|
|
||||||
- Type: string
|
- Type: ip address string
|
||||||
- Required: yes
|
- Required: no
|
||||||
|
- Default: "0.0.0.0" or RFC1918 addess or "127.0.0.1" (depending on platform)
|
||||||
|
|
||||||
### port
|
### port
|
||||||
Port number that the listener will listen on for incoming requests.
|
Port number that the listener will listen on for incoming requests. If `port` is `0`, a random available port will be chosen.
|
||||||
|
|
||||||
- Type: number
|
- Type: number
|
||||||
- Required: yes
|
- Required: no
|
||||||
|
- Default: 0 or 53 or 5354 (depending on platform)
|
||||||
|
|
||||||
### restricted
|
### restricted
|
||||||
If set to `true` makes the listener `REFUSE` DNS queries from all source IP addresses that are not explicitly defined in the policy using a `network`.
|
If set to `true` makes the listener `REFUSE` DNS queries from all source IP addresses that are not explicitly defined in the policy using a `network`.
|
||||||
|
|
||||||
- Type: bool
|
- Type: bool
|
||||||
- Required: no
|
- Required: no
|
||||||
|
- Default: false
|
||||||
|
|
||||||
### policy
|
### policy
|
||||||
Allows `ctrld` to set policy rules to determine which upstreams the requests will be forwarded to.
|
Allows `ctrld` to set policy rules to determine which upstreams the requests will be forwarded to.
|
||||||
@@ -330,19 +417,30 @@ rules = [
|
|||||||
|
|
||||||
- Type: string
|
- Type: string
|
||||||
- Required: no
|
- Required: no
|
||||||
|
- Default: ""
|
||||||
|
|
||||||
### networks:
|
### networks:
|
||||||
`networks` is the list of network rules of the policy.
|
`networks` is the list of network rules of the policy.
|
||||||
|
|
||||||
- type: array of networks
|
- Type: array of networks
|
||||||
|
- Required: no
|
||||||
|
- Default: []
|
||||||
|
|
||||||
### rules:
|
### rules:
|
||||||
`rules` is the list of domain rules within the policy. Domain can be either FQDN or wildcard domain.
|
`rules` is the list of domain rules within the policy. Domain can be either FQDN or wildcard domain.
|
||||||
|
|
||||||
- type: array of rule
|
- Type: array of rule
|
||||||
|
- Required: no
|
||||||
|
- Default: []
|
||||||
|
|
||||||
### failover_rcodes
|
### failover_rcodes
|
||||||
For non success response, `failover_rcodes` allows the request to be forwarded to next upstream, if the response `RCODE` matches any value defined in `failover_rcodes`. For example:
|
For non success response, `failover_rcodes` allows the request to be forwarded to next upstream, if the response `RCODE` matches any value defined in `failover_rcodes`.
|
||||||
|
|
||||||
|
- Type: array of string
|
||||||
|
- Required: no
|
||||||
|
- Default: []
|
||||||
|
-
|
||||||
|
For example:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[listener.0.policy]
|
[listener.0.policy]
|
||||||
|
|||||||
@@ -3,29 +3,39 @@ package ctrld
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
"github.com/lucas-clemente/quic-go/http3"
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
dohMacHeader = "x-cd-mac"
|
||||||
|
dohIPHeader = "x-cd-ip"
|
||||||
|
dohHostHeader = "x-cd-host"
|
||||||
|
headerApplicationDNS = "application/dns-message"
|
||||||
|
)
|
||||||
|
|
||||||
func newDohResolver(uc *UpstreamConfig) *dohResolver {
|
func newDohResolver(uc *UpstreamConfig) *dohResolver {
|
||||||
r := &dohResolver{
|
r := &dohResolver{
|
||||||
endpoint: uc.Endpoint,
|
endpoint: uc.u,
|
||||||
isDoH3: uc.Type == ResolverTypeDOH3,
|
isDoH3: uc.Type == ResolverTypeDOH3,
|
||||||
transport: uc.transport,
|
|
||||||
http3RoundTripper: uc.http3RoundTripper,
|
http3RoundTripper: uc.http3RoundTripper,
|
||||||
|
sendClientInfo: uc.UpstreamSendClientInfo(),
|
||||||
|
uc: uc,
|
||||||
}
|
}
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
type dohResolver struct {
|
type dohResolver struct {
|
||||||
endpoint string
|
uc *UpstreamConfig
|
||||||
|
endpoint *url.URL
|
||||||
isDoH3 bool
|
isDoH3 bool
|
||||||
transport *http.Transport
|
http3RoundTripper http.RoundTripper
|
||||||
http3RoundTripper *http3.RoundTripper
|
sendClientInfo bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *dohResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) {
|
func (r *dohResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) {
|
||||||
@@ -33,23 +43,36 @@ func (r *dohResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, erro
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
enc := base64.RawURLEncoding.EncodeToString(data)
|
enc := base64.RawURLEncoding.EncodeToString(data)
|
||||||
url := fmt.Sprintf("%s?dns=%s", r.endpoint, enc)
|
query := r.endpoint.Query()
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
query.Add("dns", enc)
|
||||||
|
|
||||||
|
endpoint := *r.endpoint
|
||||||
|
endpoint.RawQuery = query.Encode()
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not create request: %w", err)
|
return nil, fmt.Errorf("could not create request: %w", err)
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", "application/dns-message")
|
addHeader(ctx, req, r.sendClientInfo)
|
||||||
req.Header.Set("Accept", "application/dns-message")
|
dnsTyp := uint16(0)
|
||||||
|
if len(msg.Question) > 0 {
|
||||||
c := http.Client{Transport: r.transport}
|
dnsTyp = msg.Question[0].Qtype
|
||||||
|
}
|
||||||
|
c := http.Client{Transport: r.uc.dohTransport(dnsTyp)}
|
||||||
if r.isDoH3 {
|
if r.isDoH3 {
|
||||||
c.Transport = r.http3RoundTripper
|
transport := r.uc.doh3Transport(dnsTyp)
|
||||||
|
if transport == nil {
|
||||||
|
return nil, errors.New("DoH3 is not supported")
|
||||||
|
}
|
||||||
|
c.Transport = transport
|
||||||
}
|
}
|
||||||
resp, err := c.Do(req)
|
resp, err := c.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if r.isDoH3 {
|
if r.isDoH3 {
|
||||||
r.http3RoundTripper.Close()
|
if closer, ok := c.Transport.(io.Closer); ok {
|
||||||
|
closer.Close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("could not perform request: %w", err)
|
return nil, fmt.Errorf("could not perform request: %w", err)
|
||||||
}
|
}
|
||||||
@@ -65,5 +88,27 @@ func (r *dohResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
answer := new(dns.Msg)
|
answer := new(dns.Msg)
|
||||||
return answer, answer.Unpack(buf)
|
if err := answer.Unpack(buf); err != nil {
|
||||||
|
return nil, fmt.Errorf("answer.Unpack: %w", err)
|
||||||
|
}
|
||||||
|
return answer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addHeader(ctx context.Context, req *http.Request, sendClientInfo bool) {
|
||||||
|
req.Header.Set("Content-Type", headerApplicationDNS)
|
||||||
|
req.Header.Set("Accept", headerApplicationDNS)
|
||||||
|
if sendClientInfo {
|
||||||
|
if ci, ok := ctx.Value(ClientInfoCtxKey{}).(*ClientInfo); ok && ci != nil {
|
||||||
|
if ci.Mac != "" {
|
||||||
|
req.Header.Set(dohMacHeader, ci.Mac)
|
||||||
|
}
|
||||||
|
if ci.IP != "" {
|
||||||
|
req.Header.Set(dohIPHeader, ci.IP)
|
||||||
|
}
|
||||||
|
if ci.Hostname != "" {
|
||||||
|
req.Header.Set(dohHostHeader, ci.Hostname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log(ctx, ProxyLogger.Load().Debug().Interface("header", req.Header), "sending request header")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build !qf
|
||||||
|
|
||||||
package ctrld
|
package ctrld
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -7,8 +9,8 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/lucas-clemente/quic-go"
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
|
"github.com/quic-go/quic-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
type doqResolver struct {
|
type doqResolver struct {
|
||||||
@@ -18,11 +20,17 @@ type doqResolver struct {
|
|||||||
func (r *doqResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) {
|
func (r *doqResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) {
|
||||||
endpoint := r.uc.Endpoint
|
endpoint := r.uc.Endpoint
|
||||||
tlsConfig := &tls.Config{NextProtos: []string{"doq"}}
|
tlsConfig := &tls.Config{NextProtos: []string{"doq"}}
|
||||||
if r.uc.BootstrapIP != "" {
|
ip := r.uc.BootstrapIP
|
||||||
tlsConfig.ServerName = r.uc.Domain
|
if ip == "" {
|
||||||
_, port, _ := net.SplitHostPort(endpoint)
|
dnsTyp := uint16(0)
|
||||||
endpoint = net.JoinHostPort(r.uc.BootstrapIP, port)
|
if msg != nil && len(msg.Question) > 0 {
|
||||||
|
dnsTyp = msg.Question[0].Qtype
|
||||||
|
}
|
||||||
|
ip = r.uc.bootstrapIPForDNSType(dnsTyp)
|
||||||
}
|
}
|
||||||
|
tlsConfig.ServerName = r.uc.Domain
|
||||||
|
_, port, _ := net.SplitHostPort(endpoint)
|
||||||
|
endpoint = net.JoinHostPort(ip, port)
|
||||||
return resolve(ctx, msg, endpoint, tlsConfig)
|
return resolve(ctx, msg, endpoint, tlsConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
//go:build qf
|
||||||
|
|
||||||
|
package ctrld
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
type doqResolver struct {
|
||||||
|
uc *UpstreamConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *doqResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) {
|
||||||
|
return nil, errors.New("DoQ is not supported")
|
||||||
|
}
|
||||||
@@ -14,18 +14,26 @@ type dotResolver struct {
|
|||||||
|
|
||||||
func (r *dotResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) {
|
func (r *dotResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) {
|
||||||
// The dialer is used to prevent bootstrapping cycle.
|
// The dialer is used to prevent bootstrapping cycle.
|
||||||
// If r.endpoing is set to dns.controld.dev, we need to resolve
|
// If r.endpoint is set to dns.controld.dev, we need to resolve
|
||||||
// dns.controld.dev first. By using a dialer with custom resolver,
|
// dns.controld.dev first. By using a dialer with custom resolver,
|
||||||
// we ensure that we can always resolve the bootstrap domain
|
// we ensure that we can always resolve the bootstrap domain
|
||||||
// regardless of the machine DNS status.
|
// regardless of the machine DNS status.
|
||||||
dialer := newDialer(net.JoinHostPort(bootstrapDNS, "53"))
|
dialer := newDialer(net.JoinHostPort(bootstrapDNS, "53"))
|
||||||
|
dnsTyp := uint16(0)
|
||||||
|
if msg != nil && len(msg.Question) > 0 {
|
||||||
|
dnsTyp = msg.Question[0].Qtype
|
||||||
|
}
|
||||||
|
|
||||||
|
tcpNet, _ := r.uc.netForDNSType(dnsTyp)
|
||||||
dnsClient := &dns.Client{
|
dnsClient := &dns.Client{
|
||||||
Net: "tcp-tls",
|
Net: tcpNet,
|
||||||
Dialer: dialer,
|
Dialer: dialer,
|
||||||
|
TLSConfig: &tls.Config{RootCAs: r.uc.certPool},
|
||||||
}
|
}
|
||||||
endpoint := r.uc.Endpoint
|
endpoint := r.uc.Endpoint
|
||||||
if r.uc.BootstrapIP != "" {
|
if r.uc.BootstrapIP != "" {
|
||||||
dnsClient.TLSConfig = &tls.Config{ServerName: r.uc.Domain}
|
dnsClient.TLSConfig.ServerName = r.uc.Domain
|
||||||
|
dnsClient.Net = "tcp-tls"
|
||||||
_, port, _ := net.SplitHostPort(endpoint)
|
_, port, _ := net.SplitHostPort(endpoint)
|
||||||
endpoint = net.JoinHostPort(r.uc.BootstrapIP, port)
|
endpoint = net.JoinHostPort(r.uc.BootstrapIP, port)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,76 +1,85 @@
|
|||||||
module github.com/Control-D-Inc/ctrld
|
module github.com/Control-D-Inc/ctrld
|
||||||
|
|
||||||
go 1.19
|
go 1.20
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534
|
github.com/coreos/go-systemd/v22 v22.5.0
|
||||||
github.com/frankban/quicktest v1.14.3
|
github.com/cuonglm/osinfo v0.0.0-20230329055532-c513f836da19
|
||||||
|
github.com/frankban/quicktest v1.14.5
|
||||||
|
github.com/fsnotify/fsnotify v1.6.0
|
||||||
github.com/go-playground/validator/v10 v10.11.1
|
github.com/go-playground/validator/v10 v10.11.1
|
||||||
github.com/godbus/dbus/v5 v5.0.6
|
github.com/godbus/dbus/v5 v5.1.0
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.1
|
github.com/hashicorp/golang-lru/v2 v2.0.1
|
||||||
github.com/illarion/gonotify v1.0.1
|
github.com/illarion/gonotify v1.0.1
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20211209223715-7d93572ebe8e
|
github.com/insomniacslk/dhcp v0.0.0-20230407062729-974c6f05fe16
|
||||||
|
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86
|
||||||
github.com/kardianos/service v1.2.1
|
github.com/kardianos/service v1.2.1
|
||||||
github.com/lucas-clemente/quic-go v0.29.1
|
github.com/miekg/dns v1.1.55
|
||||||
github.com/miekg/dns v1.1.50
|
github.com/olekukonko/tablewriter v0.0.5
|
||||||
github.com/pelletier/go-toml/v2 v2.0.6
|
github.com/pelletier/go-toml/v2 v2.0.8
|
||||||
|
github.com/quic-go/quic-go v0.32.0
|
||||||
github.com/rs/zerolog v1.28.0
|
github.com/rs/zerolog v1.28.0
|
||||||
github.com/spf13/cobra v1.4.0
|
github.com/spf13/cobra v1.7.0
|
||||||
github.com/spf13/viper v1.14.0
|
github.com/spf13/pflag v1.0.5
|
||||||
github.com/stretchr/testify v1.8.1
|
github.com/spf13/viper v1.16.0
|
||||||
golang.org/x/sys v0.4.0
|
github.com/stretchr/testify v1.8.3
|
||||||
|
github.com/vishvananda/netlink v1.2.1-beta.2
|
||||||
|
go4.org/mem v0.0.0-20220726221520-4f986261bf13
|
||||||
|
golang.org/x/net v0.10.0
|
||||||
|
golang.org/x/sync v0.2.0
|
||||||
|
golang.org/x/sys v0.8.1-0.20230609144347-5059a07aa46a
|
||||||
golang.zx2c4.com/wireguard/windows v0.5.3
|
golang.zx2c4.com/wireguard/windows v0.5.3
|
||||||
tailscale.com v1.34.1
|
tailscale.com v1.44.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect
|
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
|
||||||
github.com/go-playground/locales v0.14.0 // indirect
|
github.com/go-playground/locales v0.14.0 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
|
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
|
||||||
github.com/golang/mock v1.6.0 // indirect
|
github.com/golang/mock v1.6.0 // indirect
|
||||||
github.com/google/go-cmp v0.5.9 // indirect
|
github.com/google/go-cmp v0.5.9 // indirect
|
||||||
|
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/josharian/native v1.0.0 // indirect
|
github.com/jsimonetti/rtnetlink v1.3.2 // indirect
|
||||||
github.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b // indirect
|
github.com/kr/pretty v0.3.1 // indirect
|
||||||
github.com/kr/pretty v0.3.0 // indirect
|
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/leodido/go-urn v1.2.1 // indirect
|
github.com/leodido/go-urn v1.2.1 // indirect
|
||||||
github.com/magiconair/properties v1.8.7 // indirect
|
github.com/magiconair/properties v1.8.7 // indirect
|
||||||
github.com/marten-seemann/qpack v0.2.1 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect
|
github.com/mattn/go-isatty v0.0.18 // indirect
|
||||||
github.com/marten-seemann/qtls-go1-19 v0.1.0 // indirect
|
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
|
||||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
|
||||||
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 // indirect
|
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 // indirect
|
||||||
github.com/mdlayher/netlink v1.6.0 // indirect
|
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||||
github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 // indirect
|
github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 // indirect
|
||||||
github.com/mdlayher/socket v0.2.3 // indirect
|
github.com/mdlayher/socket v0.4.1 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/nxadm/tail v1.4.8 // indirect
|
github.com/onsi/ginkgo/v2 v2.2.0 // indirect
|
||||||
github.com/onsi/ginkgo v1.16.5 // indirect
|
github.com/pierrec/lz4/v4 v4.1.17 // indirect
|
||||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 // indirect
|
github.com/quic-go/qpack v0.4.0 // indirect
|
||||||
github.com/spf13/afero v1.9.3 // indirect
|
github.com/quic-go/qtls-go1-18 v0.2.0 // indirect
|
||||||
github.com/spf13/cast v1.5.0 // indirect
|
github.com/quic-go/qtls-go1-19 v0.2.0 // indirect
|
||||||
|
github.com/quic-go/qtls-go1-20 v0.1.0 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.4 // indirect
|
||||||
|
github.com/rogpeppe/go-internal v1.10.0 // indirect
|
||||||
|
github.com/spf13/afero v1.9.5 // indirect
|
||||||
|
github.com/spf13/cast v1.5.1 // indirect
|
||||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/subosito/gotenv v1.4.2 // indirect
|
||||||
github.com/subosito/gotenv v1.4.1 // indirect
|
github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 // indirect
|
||||||
github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4 // indirect
|
github.com/vishvananda/netns v0.0.4 // indirect
|
||||||
go4.org/mem v0.0.0-20210711025021-927187094b94 // indirect
|
golang.org/x/crypto v0.9.0 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 // indirect
|
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 // indirect
|
||||||
golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 // indirect
|
golang.org/x/mod v0.10.0 // indirect
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
|
golang.org/x/text v0.9.0 // indirect
|
||||||
golang.org/x/net v0.5.1-0.20230105164244-f8411da775a6 // indirect
|
golang.org/x/tools v0.9.1 // indirect
|
||||||
golang.org/x/sync v0.1.0 // indirect
|
|
||||||
golang.org/x/text v0.6.0 // indirect
|
|
||||||
golang.org/x/tools v0.1.12 // indirect
|
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
replace github.com/mr-karan/doggo => github.com/Windscribe/doggo v0.0.0-20220919152748-2c118fc391f8
|
||||||
|
|
||||||
|
replace github.com/rs/zerolog => github.com/Windscribe/zerolog v0.0.0-20230503170159-e6aa153233be
|
||||||
|
|||||||
@@ -38,22 +38,25 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f
|
|||||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
|
github.com/Windscribe/zerolog v0.0.0-20230503170159-e6aa153233be h1:qBKVRi7Mom5heOkyZ+NCIu9HZBiNCsRqrRe5t9pooik=
|
||||||
|
github.com/Windscribe/zerolog v0.0.0-20230503170159-e6aa153233be/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w=
|
||||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA=
|
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA=
|
||||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
github.com/cilium/ebpf v0.8.1 h1:bLSSEbBLqGPXxls55pGr5qWZaTqcmfDJHhou7t254ao=
|
github.com/cilium/ebpf v0.10.0 h1:nk5HPMeoBXtOzbkZBWym+ZWq1GIiHUsBFXxwewXAHLQ=
|
||||||
github.com/cilium/ebpf v0.8.1/go.mod h1:f5zLIM0FSNuAkSyLAN7X+Hy6yznlF1mNiWUMfxMtrgk=
|
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||||
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534 h1:rtAn27wIbmOGUs7RIbVgPEjb31ehTVniDwPGXyMxm5U=
|
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||||
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/cuonglm/osinfo v0.0.0-20230329055532-c513f836da19 h1:7P/f19Mr0oa3ug8BYt4JuRe/Zq3dF4Mrr4m8+Kw+Hcs=
|
||||||
|
github.com/cuonglm/osinfo v0.0.0-20230329055532-c513f836da19/go.mod h1:G45410zMgmnSjLVKCq4f6GpbYAzoP2plX9rPwgx6C24=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -63,12 +66,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
|
|||||||
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc=
|
github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA=
|
||||||
github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og=
|
github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
|
|
||||||
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
|
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
|
||||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||||
@@ -85,8 +84,8 @@ github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4
|
|||||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
|
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
|
||||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro=
|
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||||
github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
@@ -114,7 +113,7 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
|
|||||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
@@ -126,8 +125,6 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||||
@@ -143,130 +140,124 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf
|
|||||||
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
|
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
|
||||||
|
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||||
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
|
||||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.1 h1:5pv5N1lT1fjLg2VQ5KWc7kmucp2x/kvFOnxuVTqZ6x4=
|
github.com/hashicorp/golang-lru/v2 v2.0.1 h1:5pv5N1lT1fjLg2VQ5KWc7kmucp2x/kvFOnxuVTqZ6x4=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.1/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
github.com/hashicorp/golang-lru/v2 v2.0.1/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
|
||||||
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8=
|
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8=
|
||||||
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis=
|
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
github.com/illarion/gonotify v1.0.1 h1:F1d+0Fgbq/sDWjj/r66ekjDG+IDeecQKUFH4wNwsoio=
|
github.com/illarion/gonotify v1.0.1 h1:F1d+0Fgbq/sDWjj/r66ekjDG+IDeecQKUFH4wNwsoio=
|
||||||
github.com/illarion/gonotify v1.0.1/go.mod h1:zt5pmDofZpU1f8aqlK0+95eQhoEAn/d4G4B/FjVW4jE=
|
github.com/illarion/gonotify v1.0.1/go.mod h1:zt5pmDofZpU1f8aqlK0+95eQhoEAn/d4G4B/FjVW4jE=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20211209223715-7d93572ebe8e h1:IQpunlq7T+NiJJMO7ODYV2YWBiv/KnObR3gofX0mWOo=
|
github.com/insomniacslk/dhcp v0.0.0-20230407062729-974c6f05fe16 h1:+aAGyK41KRn8jbF2Q7PLL0Sxwg6dShGcQSeCC7nZQ8E=
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20211209223715-7d93572ebe8e/go.mod h1:h+MxyHxRg9NH3terB1nfRIUaQEcI0XOVkdR9LNBlp8E=
|
github.com/insomniacslk/dhcp v0.0.0-20230407062729-974c6f05fe16/go.mod h1:IKrnDWs3/Mqq5n0lI+RxA2sB7MvN/vbMBP3ehXg65UI=
|
||||||
github.com/josharian/native v1.0.0 h1:Ts/E8zCSEsG17dUqv7joXJFybuMLjQfWE04tsBODTxk=
|
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||||
github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 h1:elKwZS1OcdQ0WwEDBeqxKwb7WB62QX8bvZ/FJnVXIfk=
|
||||||
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
|
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86/go.mod h1:aFAMtuldEgx/4q7iSGazk22+IcgvtiC+HIimFO9XlS8=
|
||||||
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
|
github.com/jsimonetti/rtnetlink v1.3.2 h1:dcn0uWkfxycEEyNy0IGfx3GrhQ38LH7odjxAghimsVI=
|
||||||
github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
|
github.com/jsimonetti/rtnetlink v1.3.2/go.mod h1:BBu4jZCpTjP6Gk0/wfrO8qcqymnN3g0hoFqObRmUo6U=
|
||||||
github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c/go.mod h1:huN4d1phzjhlOsNIjFsw2SVRbwIHj3fJDMEU2SDPTmg=
|
|
||||||
github.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b h1:Yws7RV6kZr2O7PPdT+RkbSmmOponA8i/1DuGHe8BRsM=
|
|
||||||
github.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b/go.mod h1:TzDCVOZKUa79z6iXbbXqhtAflVgUKaFkZ21M5tK5tzY=
|
|
||||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
|
||||||
github.com/kardianos/service v1.2.1 h1:AYndMsehS+ywIS6RB9KOlcXzteWUzxgMgBymJD7+BYk=
|
github.com/kardianos/service v1.2.1 h1:AYndMsehS+ywIS6RB9KOlcXzteWUzxgMgBymJD7+BYk=
|
||||||
github.com/kardianos/service v1.2.1/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
|
github.com/kardianos/service v1.2.1/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
|
||||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||||
github.com/lucas-clemente/quic-go v0.29.1 h1:Z+WMJ++qMLhvpFkRZA+jl3BTxUjm415YBmWanXB8zP0=
|
|
||||||
github.com/lucas-clemente/quic-go v0.29.1/go.mod h1:CTcNfLYJS2UuRNB+zcNlgvkjBhxX6Hm3WUxxAQx2mgE=
|
|
||||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||||
github.com/marten-seemann/qpack v0.2.1 h1:jvTsT/HpCn2UZJdP+UUB53FfUUgeOyG5K1ns0OJOGVs=
|
|
||||||
github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc=
|
|
||||||
github.com/marten-seemann/qtls-go1-18 v0.1.2 h1:JH6jmzbduz0ITVQ7ShevK10Av5+jBEKAHMntXmIV7kM=
|
|
||||||
github.com/marten-seemann/qtls-go1-18 v0.1.2/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4=
|
|
||||||
github.com/marten-seemann/qtls-go1-19 v0.1.0 h1:rLFKD/9mp/uq1SYGYuVZhm83wkmU95pK5df3GufyYYU=
|
|
||||||
github.com/marten-seemann/qtls-go1-19 v0.1.0/go.mod h1:5HTDWtVudo/WFsHKRNuOhWlbdjrfs5JHrYb0wIJqGpI=
|
|
||||||
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
|
||||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
|
||||||
|
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
|
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||||
|
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 h1:lez6TS6aAau+8wXUP3G9I3TGlmPFEq2CTxBaRqY6AGE=
|
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 h1:lez6TS6aAau+8wXUP3G9I3TGlmPFEq2CTxBaRqY6AGE=
|
||||||
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y=
|
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y=
|
||||||
github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
|
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
|
||||||
github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
|
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
||||||
github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY=
|
|
||||||
github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o=
|
|
||||||
github.com/mdlayher/netlink v1.6.0 h1:rOHX5yl7qnlpiVkFWoqccueppMtXzeziFjWAjLg6sz0=
|
|
||||||
github.com/mdlayher/netlink v1.6.0/go.mod h1:0o3PlBmGst1xve7wQ7j/hwpNaFaH4qCRyWCdcZk8/vA=
|
|
||||||
github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
|
github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
|
||||||
github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 h1:aFkJ6lx4FPip+S+Uw4aTegFMct9shDvP+79PsSxpm3w=
|
github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 h1:aFkJ6lx4FPip+S+Uw4aTegFMct9shDvP+79PsSxpm3w=
|
||||||
github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
|
github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
|
||||||
github.com/mdlayher/socket v0.1.1/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5Awbj+qDs=
|
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
|
||||||
github.com/mdlayher/socket v0.2.3 h1:XZA2X2TjdOwNoNPVPclRCURoX/hokBY8nkTmRZFEheM=
|
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
|
||||||
github.com/mdlayher/socket v0.2.3/go.mod h1:bz12/FozYNH/VbvC3q7TRIK/Y6dH1kCKsXaUeXi/FmY=
|
github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
|
||||||
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
|
github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
|
||||||
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
|
|
||||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk=
|
||||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
|
||||||
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
|
||||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
|
||||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
|
||||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
|
||||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
|
||||||
github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q=
|
github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q=
|
||||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||||
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
|
github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||||
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
|
github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc=
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||||
|
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||||
|
github.com/quic-go/qtls-go1-18 v0.2.0 h1:5ViXqBZ90wpUcZS0ge79rf029yx0dYB0McyPJwqqj7U=
|
||||||
|
github.com/quic-go/qtls-go1-18 v0.2.0/go.mod h1:moGulGHK7o6O8lSPSZNoOwcLvJKJ85vVNc7oJFD65bc=
|
||||||
|
github.com/quic-go/qtls-go1-19 v0.2.0 h1:Cvn2WdhyViFUHoOqK52i51k4nDX8EwIh5VJiVM4nttk=
|
||||||
|
github.com/quic-go/qtls-go1-19 v0.2.0/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI=
|
||||||
|
github.com/quic-go/qtls-go1-20 v0.1.0 h1:d1PK3ErFy9t7zxKsG3NXBJXZjp/kMLoIb3y/kV54oAI=
|
||||||
|
github.com/quic-go/qtls-go1-20 v0.1.0/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM=
|
||||||
|
github.com/quic-go/quic-go v0.32.0 h1:lY02md31s1JgPiiyfqJijpu/UX/Iun304FI3yUqX7tA=
|
||||||
|
github.com/quic-go/quic-go v0.32.0/go.mod h1:/fCsKANhQIeD5l76c2JFU+07gVE3KaA0FP+0zMWwfwo=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||||
|
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||||
github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 h1:Ha8xCaq6ln1a+R91Km45Oq6lPXj2Mla6CRJYcuV2h1w=
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||||
github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY=
|
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
|
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
|
||||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
|
||||||
github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
|
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
|
||||||
github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
|
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
|
||||||
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
|
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
||||||
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
|
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
||||||
github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=
|
|
||||||
github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
|
|
||||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/viper v1.14.0 h1:Rg7d3Lo706X9tHsJMUjdiwMpHB7W8WnSVOssIY+JElU=
|
github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc=
|
||||||
github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As=
|
github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||||
@@ -278,13 +269,17 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
|||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
|
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
|
||||||
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||||
github.com/u-root/uio v0.0.0-20210528114334-82958018845c/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA=
|
github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 h1:YcojQL98T/OO+rybuzn2+5KrD5dBwXIvYBvQ2cD3Avg=
|
||||||
github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4 h1:hl6sK6aFgTLISijk6xIzeqnPzQcsLqqvL6vEfTPinME=
|
github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
|
||||||
github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA=
|
github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs=
|
||||||
|
github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
|
||||||
|
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
||||||
|
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
||||||
|
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
@@ -296,18 +291,18 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
|||||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||||
go4.org/mem v0.0.0-20210711025021-927187094b94 h1:OAAkygi2Js191AJP1Ds42MhJRgeofeKGjuoUqNp1QC4=
|
go4.org/mem v0.0.0-20220726221520-4f986261bf13 h1:CbZeCBZ0aZj8EfVgnqQcYZgf0lpZ3H9rmp5nkDTAst8=
|
||||||
go4.org/mem v0.0.0-20210711025021-927187094b94/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
go4.org/mem v0.0.0-20220726221520-4f986261bf13/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
|
||||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM=
|
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
||||||
|
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
@@ -318,8 +313,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
|||||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||||
golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 h1:tnebWN09GYg9OLPss1KXj8txwZc6X6uMr6VFdcGNbHw=
|
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o=
|
||||||
golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
@@ -344,11 +339,10 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|||||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
|
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
@@ -360,8 +354,6 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
|
|||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
@@ -372,25 +364,19 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/
|
|||||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
|
||||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
|
||||||
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
|
||||||
golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||||
golang.org/x/net v0.5.1-0.20230105164244-f8411da775a6 h1:pKt/LWZC6+FwNujj5E7DdVyWcbtQvKqPuN0GPKWMyB8=
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
golang.org/x/net v0.5.1-0.20230105164244-f8411da775a6/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
@@ -411,13 +397,11 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -426,18 +410,14 @@ golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -445,37 +425,33 @@ golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
|
golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.8.1-0.20230609144347-5059a07aa46a h1:qMsju+PNttu/NMbq8bQ9waDdxgJMu9QNoUDuhnBaYt0=
|
||||||
|
golang.org/x/sys v0.8.1-0.20230609144347-5059a07aa46a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
@@ -484,8 +460,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
|
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||||
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
@@ -495,7 +471,6 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3
|
|||||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
|
||||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
@@ -534,14 +509,12 @@ golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82u
|
|||||||
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
|
||||||
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo=
|
||||||
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
|
golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
@@ -636,22 +609,15 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
|
|||||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||||
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
|
||||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
@@ -666,5 +632,5 @@ honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
|
|||||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||||
tailscale.com v1.34.1 h1:tqm9Ww4ltyYp3IPe7vCGch6tT6j5G/WXPQ6BrVZ6pdI=
|
tailscale.com v1.44.0 h1:MPos9n30kJvdyfL52045gVFyNg93K+bwgDsr8gqKq2o=
|
||||||
tailscale.com v1.34.1/go.mod h1:ZsBP7rjzzB2rp+UCOumr9DAe0EQ6OPivwSXcz/BrekQ=
|
tailscale.com v1.44.0/go.mod h1:+iYwTdeHyVJuNDu42Zafwihq1Uqfh+pW7pRaY1GD328=
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
|||||||
|
package certs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
_ "embed"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed cacert.pem
|
||||||
|
caRoots []byte
|
||||||
|
caCertPoolOnce sync.Once
|
||||||
|
caCertPool *x509.CertPool
|
||||||
|
)
|
||||||
|
|
||||||
|
func CACertPool() *x509.CertPool {
|
||||||
|
caCertPoolOnce.Do(func() {
|
||||||
|
caCertPool = x509.NewCertPool()
|
||||||
|
caCertPool.AppendCertsFromPEM(caRoots)
|
||||||
|
})
|
||||||
|
return caCertPool
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package certs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCACertPool(t *testing.T) {
|
||||||
|
c := &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
RootCAs: CACertPool(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Timeout: 2 * time.Second,
|
||||||
|
}
|
||||||
|
resp, err := c.Get("https://freedns.controld.com/p1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if !resp.TLS.HandshakeComplete {
|
||||||
|
t.Error("TLS handshake is not complete")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package clientinfo
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
type arpDiscover struct {
|
||||||
|
mac sync.Map // ip => mac
|
||||||
|
ip sync.Map // mac => ip
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *arpDiscover) refresh() error {
|
||||||
|
a.scan()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *arpDiscover) LookupIP(mac string) string {
|
||||||
|
val, ok := a.ip.Load(mac)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return val.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *arpDiscover) LookupMac(ip string) string {
|
||||||
|
val, ok := a.mac.Load(ip)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return val.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *arpDiscover) String() string {
|
||||||
|
return "arp"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *arpDiscover) List() []string {
|
||||||
|
var ips []string
|
||||||
|
a.ip.Range(func(key, value any) bool {
|
||||||
|
ips = append(ips, value.(string))
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
a.mac.Range(func(key, value any) bool {
|
||||||
|
ips = append(ips, key.(string))
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return ips
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package clientinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const procNetArpFile = "/proc/net/arp"
|
||||||
|
|
||||||
|
func (a *arpDiscover) scan() {
|
||||||
|
f, err := os.Open(procNetArpFile)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
s := bufio.NewScanner(f)
|
||||||
|
s.Scan() // skip header
|
||||||
|
for s.Scan() {
|
||||||
|
line := s.Text()
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
ip := fields[0]
|
||||||
|
mac := fields[3]
|
||||||
|
a.mac.Store(ip, mac)
|
||||||
|
a.ip.Store(mac, ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package clientinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestArpScan(t *testing.T) {
|
||||||
|
a := &arpDiscover{}
|
||||||
|
a.scan()
|
||||||
|
|
||||||
|
for _, table := range []*sync.Map{&a.mac, &a.ip} {
|
||||||
|
count := 0
|
||||||
|
table.Range(func(key, value any) bool {
|
||||||
|
count++
|
||||||
|
t.Logf("%s => %s", key, value)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if count == 0 {
|
||||||
|
t.Error("empty result from arp scan")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
//go:build !linux && !windows
|
||||||
|
|
||||||
|
package clientinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a *arpDiscover) scan() {
|
||||||
|
data, err := exec.Command("arp", "-an").Output()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, line := range strings.Split(string(data), "\n") {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) <= 3 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// trim brackets
|
||||||
|
ip := strings.ReplaceAll(fields[1], "(", "")
|
||||||
|
ip = strings.ReplaceAll(ip, ")", "")
|
||||||
|
|
||||||
|
mac := fields[3]
|
||||||
|
a.mac.Store(ip, mac)
|
||||||
|
a.ip.Store(mac, ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package clientinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a *arpDiscover) scan() {
|
||||||
|
data, err := exec.Command("arp", "-a").Output()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
header := false
|
||||||
|
for _, line := range strings.Split(string(data), "\n") {
|
||||||
|
if len(line) == 0 {
|
||||||
|
continue // empty lines
|
||||||
|
}
|
||||||
|
if line[0] != ' ' {
|
||||||
|
header = true // "Interface:" lines, next is header line.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if header {
|
||||||
|
header = false // header lines
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ip := fields[0]
|
||||||
|
mac := strings.ReplaceAll(fields[1], "-", ":")
|
||||||
|
a.mac.Store(ip, mac)
|
||||||
|
a.ip.Store(mac, ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,346 @@
|
|||||||
|
package clientinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Control-D-Inc/ctrld"
|
||||||
|
"github.com/Control-D-Inc/ctrld/internal/controld"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IpResolver is the interface for retrieving IP from Mac.
|
||||||
|
type IpResolver interface {
|
||||||
|
fmt.Stringer
|
||||||
|
// LookupIP returns ip of the device with given mac.
|
||||||
|
LookupIP(mac string) string
|
||||||
|
}
|
||||||
|
|
||||||
|
// MacResolver is the interface for retrieving Mac from IP.
|
||||||
|
type MacResolver interface {
|
||||||
|
fmt.Stringer
|
||||||
|
// LookupMac returns mac of the device with given ip.
|
||||||
|
LookupMac(ip string) string
|
||||||
|
}
|
||||||
|
|
||||||
|
// HostnameByIpResolver is the interface for retrieving hostname from IP.
|
||||||
|
type HostnameByIpResolver interface {
|
||||||
|
// LookupHostnameByIP returns hostname of the given ip.
|
||||||
|
LookupHostnameByIP(ip string) string
|
||||||
|
}
|
||||||
|
|
||||||
|
// HostnameByMacResolver is the interface for retrieving hostname from Mac.
|
||||||
|
type HostnameByMacResolver interface {
|
||||||
|
// LookupHostnameByMac returns hostname of the device with given mac.
|
||||||
|
LookupHostnameByMac(mac string) string
|
||||||
|
}
|
||||||
|
|
||||||
|
// HostnameResolver is the interface for retrieving hostname from either IP or Mac.
|
||||||
|
type HostnameResolver interface {
|
||||||
|
fmt.Stringer
|
||||||
|
HostnameByIpResolver
|
||||||
|
HostnameByMacResolver
|
||||||
|
}
|
||||||
|
|
||||||
|
type refresher interface {
|
||||||
|
refresh() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type ipLister interface {
|
||||||
|
fmt.Stringer
|
||||||
|
// List returns list of ip known by the resolver.
|
||||||
|
List() []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
IP netip.Addr
|
||||||
|
Mac string
|
||||||
|
Hostname string
|
||||||
|
Source map[string]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Table struct {
|
||||||
|
ipResolvers []IpResolver
|
||||||
|
macResolvers []MacResolver
|
||||||
|
hostnameResolvers []HostnameResolver
|
||||||
|
refreshers []refresher
|
||||||
|
initOnce sync.Once
|
||||||
|
|
||||||
|
dhcp *dhcp
|
||||||
|
merlin *merlinDiscover
|
||||||
|
arp *arpDiscover
|
||||||
|
ptr *ptrDiscover
|
||||||
|
mdns *mdns
|
||||||
|
cfg *ctrld.Config
|
||||||
|
quitCh chan struct{}
|
||||||
|
selfIP string
|
||||||
|
cdUID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTable(cfg *ctrld.Config, selfIP, cdUID string) *Table {
|
||||||
|
return &Table{
|
||||||
|
cfg: cfg,
|
||||||
|
quitCh: make(chan struct{}),
|
||||||
|
selfIP: selfIP,
|
||||||
|
cdUID: cdUID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) AddLeaseFile(name string, format ctrld.LeaseFileFormat) {
|
||||||
|
if !t.discoverDHCP() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
clientInfoFiles[name] = format
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) RefreshLoop(stopCh chan struct{}) {
|
||||||
|
timer := time.NewTicker(time.Minute * 5)
|
||||||
|
defer timer.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-timer.C:
|
||||||
|
for _, r := range t.refreshers {
|
||||||
|
_ = r.refresh()
|
||||||
|
}
|
||||||
|
case <-stopCh:
|
||||||
|
close(t.quitCh)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) Init() {
|
||||||
|
t.initOnce.Do(t.init)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) init() {
|
||||||
|
if _, clientID := controld.ParseRawUID(t.cdUID); clientID != "" {
|
||||||
|
ctrld.ProxyLogger.Load().Debug().Msg("start self discovery")
|
||||||
|
t.dhcp = &dhcp{selfIP: t.selfIP}
|
||||||
|
t.dhcp.addSelf()
|
||||||
|
t.ipResolvers = append(t.ipResolvers, t.dhcp)
|
||||||
|
t.macResolvers = append(t.macResolvers, t.dhcp)
|
||||||
|
t.hostnameResolvers = append(t.hostnameResolvers, t.dhcp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if t.discoverDHCP() || t.discoverARP() {
|
||||||
|
t.merlin = &merlinDiscover{}
|
||||||
|
if err := t.merlin.refresh(); err != nil {
|
||||||
|
ctrld.ProxyLogger.Load().Error().Err(err).Msg("could not init Merlin discover")
|
||||||
|
} else {
|
||||||
|
t.hostnameResolvers = append(t.hostnameResolvers, t.merlin)
|
||||||
|
t.refreshers = append(t.refreshers, t.merlin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if t.discoverDHCP() {
|
||||||
|
t.dhcp = &dhcp{selfIP: t.selfIP}
|
||||||
|
ctrld.ProxyLogger.Load().Debug().Msg("start dhcp discovery")
|
||||||
|
if err := t.dhcp.init(); err != nil {
|
||||||
|
ctrld.ProxyLogger.Load().Error().Err(err).Msg("could not init DHCP discover")
|
||||||
|
} else {
|
||||||
|
t.ipResolvers = append(t.ipResolvers, t.dhcp)
|
||||||
|
t.macResolvers = append(t.macResolvers, t.dhcp)
|
||||||
|
t.hostnameResolvers = append(t.hostnameResolvers, t.dhcp)
|
||||||
|
}
|
||||||
|
go t.dhcp.watchChanges()
|
||||||
|
}
|
||||||
|
if t.discoverARP() {
|
||||||
|
t.arp = &arpDiscover{}
|
||||||
|
ctrld.ProxyLogger.Load().Debug().Msg("start arp discovery")
|
||||||
|
if err := t.arp.refresh(); err != nil {
|
||||||
|
ctrld.ProxyLogger.Load().Error().Err(err).Msg("could not init ARP discover")
|
||||||
|
} else {
|
||||||
|
t.ipResolvers = append(t.ipResolvers, t.arp)
|
||||||
|
t.macResolvers = append(t.macResolvers, t.arp)
|
||||||
|
t.refreshers = append(t.refreshers, t.arp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if t.discoverPTR() {
|
||||||
|
t.ptr = &ptrDiscover{resolver: ctrld.NewPrivateResolver()}
|
||||||
|
ctrld.ProxyLogger.Load().Debug().Msg("start ptr discovery")
|
||||||
|
if err := t.ptr.refresh(); err != nil {
|
||||||
|
ctrld.ProxyLogger.Load().Error().Err(err).Msg("could not init PTR discover")
|
||||||
|
} else {
|
||||||
|
t.hostnameResolvers = append(t.hostnameResolvers, t.ptr)
|
||||||
|
t.refreshers = append(t.refreshers, t.ptr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if t.discoverMDNS() {
|
||||||
|
t.mdns = &mdns{}
|
||||||
|
ctrld.ProxyLogger.Load().Debug().Msg("start mdns discovery")
|
||||||
|
if err := t.mdns.init(t.quitCh); err != nil {
|
||||||
|
ctrld.ProxyLogger.Load().Error().Err(err).Msg("could not init mDNS discover")
|
||||||
|
} else {
|
||||||
|
t.hostnameResolvers = append(t.hostnameResolvers, t.mdns)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) LookupIP(mac string) string {
|
||||||
|
t.initOnce.Do(t.init)
|
||||||
|
for _, r := range t.ipResolvers {
|
||||||
|
if ip := r.LookupIP(mac); ip != "" {
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) LookupMac(ip string) string {
|
||||||
|
t.initOnce.Do(t.init)
|
||||||
|
for _, r := range t.macResolvers {
|
||||||
|
if mac := r.LookupMac(ip); mac != "" {
|
||||||
|
return mac
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) LookupHostname(ip, mac string) string {
|
||||||
|
t.initOnce.Do(t.init)
|
||||||
|
for _, r := range t.hostnameResolvers {
|
||||||
|
if name := r.LookupHostnameByIP(ip); name != "" {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
if name := r.LookupHostnameByMac(mac); name != "" {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type macEntry struct {
|
||||||
|
mac string
|
||||||
|
src string
|
||||||
|
}
|
||||||
|
|
||||||
|
type hostnameEntry struct {
|
||||||
|
name string
|
||||||
|
src string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) lookupMacAll(ip string) []*macEntry {
|
||||||
|
var res []*macEntry
|
||||||
|
for _, r := range t.macResolvers {
|
||||||
|
res = append(res, &macEntry{mac: r.LookupMac(ip), src: r.String()})
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) lookupHostnameAll(ip, mac string) []*hostnameEntry {
|
||||||
|
var res []*hostnameEntry
|
||||||
|
for _, r := range t.hostnameResolvers {
|
||||||
|
src := r.String()
|
||||||
|
// For ptrDiscover, lookup hostname may block due to server unavailable,
|
||||||
|
// so only lookup from cache to prevent timeout reached.
|
||||||
|
if ptrResolver, ok := r.(*ptrDiscover); ok {
|
||||||
|
if name := ptrResolver.lookupHostnameFromCache(ip); name != "" {
|
||||||
|
res = append(res, &hostnameEntry{name: name, src: src})
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if name := r.LookupHostnameByIP(ip); name != "" {
|
||||||
|
res = append(res, &hostnameEntry{name: name, src: src})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if name := r.LookupHostnameByMac(mac); name != "" {
|
||||||
|
res = append(res, &hostnameEntry{name: name, src: src})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListClients returns list of clients discovered by ctrld.
|
||||||
|
func (t *Table) ListClients() []*Client {
|
||||||
|
for _, r := range t.refreshers {
|
||||||
|
_ = r.refresh()
|
||||||
|
}
|
||||||
|
ipMap := make(map[string]*Client)
|
||||||
|
il := []ipLister{t.dhcp, t.arp, t.ptr, t.mdns}
|
||||||
|
for _, ir := range il {
|
||||||
|
for _, ip := range ir.List() {
|
||||||
|
c, ok := ipMap[ip]
|
||||||
|
if !ok {
|
||||||
|
c = &Client{
|
||||||
|
IP: netip.MustParseAddr(ip),
|
||||||
|
Source: map[string]struct{}{ir.String(): {}},
|
||||||
|
}
|
||||||
|
ipMap[ip] = c
|
||||||
|
} else {
|
||||||
|
c.Source[ir.String()] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for ip := range ipMap {
|
||||||
|
c := ipMap[ip]
|
||||||
|
for _, e := range t.lookupMacAll(ip) {
|
||||||
|
if c.Mac == "" && e.mac != "" {
|
||||||
|
c.Mac = e.mac
|
||||||
|
}
|
||||||
|
if e.mac != "" {
|
||||||
|
c.Source[e.src] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, e := range t.lookupHostnameAll(ip, c.Mac) {
|
||||||
|
if c.Hostname == "" && e.name != "" {
|
||||||
|
c.Hostname = e.name
|
||||||
|
}
|
||||||
|
if e.name != "" {
|
||||||
|
c.Source[e.src] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clients := make([]*Client, 0, len(ipMap))
|
||||||
|
for _, c := range ipMap {
|
||||||
|
clients = append(clients, c)
|
||||||
|
}
|
||||||
|
return clients
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) discoverDHCP() bool {
|
||||||
|
if t.cfg.Service.DiscoverDHCP == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return *t.cfg.Service.DiscoverDHCP
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) discoverARP() bool {
|
||||||
|
if t.cfg.Service.DiscoverARP == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return *t.cfg.Service.DiscoverARP
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) discoverMDNS() bool {
|
||||||
|
if t.cfg.Service.DiscoverMDNS == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return *t.cfg.Service.DiscoverMDNS
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) discoverPTR() bool {
|
||||||
|
if t.cfg.Service.DiscoverPtr == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return *t.cfg.Service.DiscoverPtr
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalizeIP normalizes the ip parsed from dnsmasq/dhcpd lease file.
|
||||||
|
func normalizeIP(in string) string {
|
||||||
|
// dnsmasq may put ip with interface index in lease file, strip it here.
|
||||||
|
ip, _, found := strings.Cut(in, "%")
|
||||||
|
if found {
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
return in
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeHostname(name string) string {
|
||||||
|
if before, _, found := strings.Cut(name, "."); found {
|
||||||
|
return before // remove ".local.", ".lan.", ... suffix
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package clientinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_normalizeIP(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
in string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"v4", "127.0.0.1", "127.0.0.1"},
|
||||||
|
{"v4 with index", "127.0.0.1%lo", "127.0.0.1"},
|
||||||
|
{"v6", "fe80::1", "fe80::1"},
|
||||||
|
{"v6 with index", "fe80::1%22002", "fe80::1"},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if got := normalizeIP(tc.in); got != tc.want {
|
||||||
|
t.Errorf("normalizeIP() = %v, want %v", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,301 @@
|
|||||||
|
package clientinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
"tailscale.com/net/interfaces"
|
||||||
|
"tailscale.com/util/lineread"
|
||||||
|
|
||||||
|
"github.com/Control-D-Inc/ctrld"
|
||||||
|
"github.com/Control-D-Inc/ctrld/internal/router"
|
||||||
|
)
|
||||||
|
|
||||||
|
type dhcp struct {
|
||||||
|
mac2name sync.Map // mac => name
|
||||||
|
ip2name sync.Map // ip => name
|
||||||
|
ip sync.Map // mac => ip
|
||||||
|
mac sync.Map // ip => mac
|
||||||
|
|
||||||
|
watcher *fsnotify.Watcher
|
||||||
|
selfIP string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dhcp) init() error {
|
||||||
|
watcher, err := fsnotify.NewWatcher()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
d.addSelf()
|
||||||
|
d.watcher = watcher
|
||||||
|
for file, format := range clientInfoFiles {
|
||||||
|
// Ignore errors for default lease files.
|
||||||
|
_ = d.addLeaseFile(file, format)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dhcp) watchChanges() {
|
||||||
|
if d.watcher == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event, ok := <-d.watcher.Events:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if event.Has(fsnotify.Write) || event.Has(fsnotify.Rename) || event.Has(fsnotify.Chmod) || event.Has(fsnotify.Remove) {
|
||||||
|
format := clientInfoFiles[event.Name]
|
||||||
|
if err := d.readLeaseFile(event.Name, format); err != nil && !os.IsNotExist(err) {
|
||||||
|
ctrld.ProxyLogger.Load().Err(err).Str("file", event.Name).Msg("leases file changed but failed to update client info")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case err, ok := <-d.watcher.Errors:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctrld.ProxyLogger.Load().Err(err).Msg("could not watch client info file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dhcp) LookupIP(mac string) string {
|
||||||
|
val, ok := d.ip.Load(mac)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return val.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dhcp) LookupMac(ip string) string {
|
||||||
|
val, ok := d.mac.Load(ip)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return val.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dhcp) LookupHostnameByIP(ip string) string {
|
||||||
|
val, ok := d.ip2name.Load(ip)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return val.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dhcp) LookupHostnameByMac(mac string) string {
|
||||||
|
val, ok := d.mac2name.Load(mac)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return val.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dhcp) String() string {
|
||||||
|
return "dhcp"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dhcp) List() []string {
|
||||||
|
var ips []string
|
||||||
|
d.ip.Range(func(key, value any) bool {
|
||||||
|
ips = append(ips, value.(string))
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
d.mac.Range(func(key, value any) bool {
|
||||||
|
ips = append(ips, key.(string))
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return ips
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddLeaseFile adds given lease file for reading/watching clients info.
|
||||||
|
func (d *dhcp) addLeaseFile(name string, format ctrld.LeaseFileFormat) error {
|
||||||
|
if d.watcher == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := d.readLeaseFile(name, format); err != nil {
|
||||||
|
return fmt.Errorf("could not read lease file: %w", err)
|
||||||
|
}
|
||||||
|
clientInfoFiles[name] = format
|
||||||
|
return d.watcher.Add(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// readLeaseFile reads the lease file with given format, saving client information to dhcp table.
|
||||||
|
func (d *dhcp) readLeaseFile(name string, format ctrld.LeaseFileFormat) error {
|
||||||
|
switch format {
|
||||||
|
case ctrld.Dnsmasq:
|
||||||
|
return d.dnsmasqReadClientInfoFile(name)
|
||||||
|
case ctrld.IscDhcpd:
|
||||||
|
return d.iscDHCPReadClientInfoFile(name)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("unsupported format: %s, file: %s", format, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// dnsmasqReadClientInfoFile populates dhcp table with client info reading from dnsmasq lease file.
|
||||||
|
func (d *dhcp) dnsmasqReadClientInfoFile(name string) error {
|
||||||
|
f, err := os.Open(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
return d.dnsmasqReadClientInfoReader(f)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// dnsmasqReadClientInfoReader likes ctrld.Dnsmasq, but reading from an io.Reader instead of file.
|
||||||
|
func (d *dhcp) dnsmasqReadClientInfoReader(reader io.Reader) error {
|
||||||
|
return lineread.Reader(reader, func(line []byte) error {
|
||||||
|
fields := bytes.Fields(line)
|
||||||
|
if len(fields) < 4 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mac := string(fields[1])
|
||||||
|
if _, err := net.ParseMAC(mac); err != nil {
|
||||||
|
// The second field is not a dhcp, skip.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ip := normalizeIP(string(fields[2]))
|
||||||
|
if net.ParseIP(ip) == nil {
|
||||||
|
ctrld.ProxyLogger.Load().Warn().Msgf("invalid ip address entry: %q", ip)
|
||||||
|
ip = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
d.mac.Store(ip, mac)
|
||||||
|
d.ip.Store(mac, ip)
|
||||||
|
hostname := string(fields[3])
|
||||||
|
if hostname == "*" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
name := normalizeHostname(hostname)
|
||||||
|
d.mac2name.Store(mac, name)
|
||||||
|
d.ip2name.Store(ip, name)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// iscDHCPReadClientInfoFile populates dhcp table with client info reading from isc-dhcpd lease file.
|
||||||
|
func (d *dhcp) iscDHCPReadClientInfoFile(name string) error {
|
||||||
|
f, err := os.Open(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
return d.iscDHCPReadClientInfoReader(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// iscDHCPReadClientInfoReader likes ctrld.IscDhcpd, but reading from an io.Reader instead of file.
|
||||||
|
func (d *dhcp) iscDHCPReadClientInfoReader(reader io.Reader) error {
|
||||||
|
s := bufio.NewScanner(reader)
|
||||||
|
var ip, mac, hostname string
|
||||||
|
for s.Scan() {
|
||||||
|
line := s.Text()
|
||||||
|
if strings.HasPrefix(line, "}") {
|
||||||
|
d.mac.Store(ip, mac)
|
||||||
|
d.ip.Store(mac, ip)
|
||||||
|
if hostname != "" && hostname != "*" {
|
||||||
|
name := normalizeHostname(hostname)
|
||||||
|
d.mac2name.Store(mac, name)
|
||||||
|
d.ip2name.Store(ip, hostname)
|
||||||
|
ip, mac, hostname = "", "", ""
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch fields[0] {
|
||||||
|
case "lease":
|
||||||
|
ip = normalizeIP(strings.ToLower(fields[1]))
|
||||||
|
if net.ParseIP(ip) == nil {
|
||||||
|
ctrld.ProxyLogger.Load().Warn().Msgf("invalid ip address entry: %q", ip)
|
||||||
|
ip = ""
|
||||||
|
}
|
||||||
|
case "hardware":
|
||||||
|
if len(fields) >= 3 {
|
||||||
|
mac = strings.ToLower(strings.TrimRight(fields[2], ";"))
|
||||||
|
if _, err := net.ParseMAC(mac); err != nil {
|
||||||
|
// Invalid dhcp, skip.
|
||||||
|
mac = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "client-hostname":
|
||||||
|
hostname = strings.Trim(fields[1], `";`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// addSelf populates current host info to dhcp, so queries from
|
||||||
|
// the host itself can be attached with proper client info.
|
||||||
|
func (d *dhcp) addSelf() {
|
||||||
|
hostname, err := os.Hostname()
|
||||||
|
if err != nil {
|
||||||
|
ctrld.ProxyLogger.Load().Err(err).Msg("could not get hostname")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hostname = normalizeHostname(hostname)
|
||||||
|
d.ip2name.Store("127.0.0.1", hostname)
|
||||||
|
d.ip2name.Store("::1", hostname)
|
||||||
|
found := false
|
||||||
|
interfaces.ForeachInterface(func(i interfaces.Interface, prefixes []netip.Prefix) {
|
||||||
|
mac := i.HardwareAddr.String()
|
||||||
|
// Skip loopback interfaces, info was stored above.
|
||||||
|
if mac == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
addrs, _ := i.Addrs()
|
||||||
|
for _, addr := range addrs {
|
||||||
|
if found {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ipNet, ok := addr.(*net.IPNet)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ip := ipNet.IP
|
||||||
|
d.mac.Store(ip.String(), mac)
|
||||||
|
d.ip.Store(mac, ip.String())
|
||||||
|
if ip.To4() != nil {
|
||||||
|
d.mac.Store("127.0.0.1", mac)
|
||||||
|
} else {
|
||||||
|
d.mac.Store("::1", mac)
|
||||||
|
}
|
||||||
|
d.mac2name.Store(mac, hostname)
|
||||||
|
d.ip2name.Store(ip.String(), hostname)
|
||||||
|
// If we have self IP set, and this IP is it, use this IP only.
|
||||||
|
if ip.String() == d.selfIP {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
for _, netIface := range router.SelfInterfaces() {
|
||||||
|
mac := netIface.HardwareAddr.String()
|
||||||
|
if mac == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
d.mac2name.Store(mac, hostname)
|
||||||
|
addrs, _ := netIface.Addrs()
|
||||||
|
for _, addr := range addrs {
|
||||||
|
ipNet, ok := addr.(*net.IPNet)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ip := ipNet.IP
|
||||||
|
d.mac.LoadOrStore(ip.String(), mac)
|
||||||
|
d.ip.LoadOrStore(mac, ip.String())
|
||||||
|
d.ip2name.Store(ip.String(), hostname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package clientinfo
|
||||||
|
|
||||||
|
import "github.com/Control-D-Inc/ctrld"
|
||||||
|
|
||||||
|
// clientInfoFiles specifies client info files and how to read them on supported platforms.
|
||||||
|
var clientInfoFiles = map[string]ctrld.LeaseFileFormat{
|
||||||
|
"/tmp/dnsmasq.leases": ctrld.Dnsmasq, // ddwrt
|
||||||
|
"/tmp/dhcp.leases": ctrld.Dnsmasq, // openwrt
|
||||||
|
"/var/lib/misc/dnsmasq.leases": ctrld.Dnsmasq, // merlin
|
||||||
|
"/mnt/data/udapi-config/dnsmasq.lease": ctrld.Dnsmasq, // UDM Pro
|
||||||
|
"/data/udapi-config/dnsmasq.lease": ctrld.Dnsmasq, // UDR
|
||||||
|
"/etc/dhcpd/dhcpd-leases.log": ctrld.Dnsmasq, // Synology
|
||||||
|
"/tmp/var/lib/misc/dnsmasq.leases": ctrld.Dnsmasq, // Tomato
|
||||||
|
"/run/dnsmasq-dhcp.leases": ctrld.Dnsmasq, // EdgeOS
|
||||||
|
"/run/dhcpd.leases": ctrld.IscDhcpd, // EdgeOS
|
||||||
|
"/var/dhcpd/var/db/dhcpd.leases": ctrld.IscDhcpd, // Pfsense
|
||||||
|
"/home/pi/.router/run/dhcp/dnsmasq.leases": ctrld.Dnsmasq, // Firewalla
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package clientinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_readClientInfoReader(t *testing.T) {
|
||||||
|
d := &dhcp{}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
in string
|
||||||
|
readFunc func(r io.Reader) error
|
||||||
|
mac string
|
||||||
|
hostname string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"good dnsmasq",
|
||||||
|
`1683329857 e6:20:59:b8:c1:6d 192.168.1.186 host1 01:e6:20:59:b8:c1:6d
|
||||||
|
`,
|
||||||
|
d.dnsmasqReadClientInfoReader,
|
||||||
|
"e6:20:59:b8:c1:6d",
|
||||||
|
"host1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bad dnsmasq seen on UDMdream machine",
|
||||||
|
`1683329857 e6:20:59:b8:c1:6e 192.168.1.111 host1 01:e6:20:59:b8:c1:6e
|
||||||
|
duid 00:01:00:01:2b:e4:2e:2c:52:52:14:26:dc:1c
|
||||||
|
1683322985 117442354 2600:4040:b0e6:b700::111 ASDASD 00:01:00:01:2a:d0:b9:81:00:07:32:4c:1c:07
|
||||||
|
`,
|
||||||
|
d.dnsmasqReadClientInfoReader,
|
||||||
|
"e6:20:59:b8:c1:6e",
|
||||||
|
"host1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"isc-dhcpd good",
|
||||||
|
`lease 192.168.1.1 {
|
||||||
|
hardware ethernet 00:00:00:00:00:01;
|
||||||
|
client-hostname "host-1";
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
d.iscDHCPReadClientInfoReader,
|
||||||
|
"00:00:00:00:00:01",
|
||||||
|
"host-1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"isc-dhcpd bad dhcp",
|
||||||
|
`lease 192.168.1.1 {
|
||||||
|
hardware ethernet invalid-dhcp;
|
||||||
|
client-hostname "host-1";
|
||||||
|
}
|
||||||
|
|
||||||
|
lease 192.168.1.2 {
|
||||||
|
hardware ethernet 00:00:00:00:00:02;
|
||||||
|
client-hostname "host-2";
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
d.iscDHCPReadClientInfoReader,
|
||||||
|
"00:00:00:00:00:02",
|
||||||
|
"host-2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"",
|
||||||
|
`1685794060 00:00:00:00:00:04 192.168.0.209 example 00:00:00:00:00:04 9`,
|
||||||
|
d.dnsmasqReadClientInfoReader,
|
||||||
|
"00:00:00:00:00:04",
|
||||||
|
"example",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
d.mac2name.Delete(tc.mac)
|
||||||
|
if err := tc.readFunc(strings.NewReader(tc.in)); err != nil {
|
||||||
|
t.Errorf("readClientInfoReader() error = %v", err)
|
||||||
|
}
|
||||||
|
val, existed := d.mac2name.Load(tc.mac)
|
||||||
|
if !existed {
|
||||||
|
t.Error("client info missing")
|
||||||
|
}
|
||||||
|
hostname := val.(string)
|
||||||
|
if existed && hostname != tc.hostname {
|
||||||
|
t.Errorf("hostname mismatched, want: %q, got: %q", tc.hostname, hostname)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
package clientinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"tailscale.com/logtail/backoff"
|
||||||
|
|
||||||
|
"github.com/Control-D-Inc/ctrld"
|
||||||
|
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
mdnsV4Addr = &net.UDPAddr{
|
||||||
|
IP: net.ParseIP("224.0.0.251"),
|
||||||
|
Port: 5353,
|
||||||
|
}
|
||||||
|
mdnsV6Addr = &net.UDPAddr{
|
||||||
|
IP: net.ParseIP("ff02::fb"),
|
||||||
|
Port: 5353,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type mdns struct {
|
||||||
|
name sync.Map // ip => hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mdns) LookupHostnameByIP(ip string) string {
|
||||||
|
val, ok := m.name.Load(ip)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return val.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mdns) LookupHostnameByMac(mac string) string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mdns) String() string {
|
||||||
|
return "mdns"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mdns) List() []string {
|
||||||
|
var ips []string
|
||||||
|
m.name.Range(func(key, value any) bool {
|
||||||
|
ips = append(ips, key.(string))
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return ips
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mdns) init(quitCh chan struct{}) error {
|
||||||
|
ifaces, err := multicastInterfaces()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
v4ConnList := make([]*net.UDPConn, 0, len(ifaces))
|
||||||
|
v6ConnList := make([]*net.UDPConn, 0, len(ifaces))
|
||||||
|
for _, iface := range ifaces {
|
||||||
|
if iface.Flags&net.FlagLoopback != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if conn, err := net.ListenMulticastUDP("udp4", &iface, mdnsV4Addr); err == nil {
|
||||||
|
v4ConnList = append(v4ConnList, conn)
|
||||||
|
go m.readLoop(conn)
|
||||||
|
}
|
||||||
|
if ctrldnet.IPv6Available(context.Background()) {
|
||||||
|
if conn, err := net.ListenMulticastUDP("udp6", &iface, mdnsV6Addr); err == nil {
|
||||||
|
v6ConnList = append(v6ConnList, conn)
|
||||||
|
go m.readLoop(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go m.probeLoop(v4ConnList, mdnsV4Addr, quitCh)
|
||||||
|
go m.probeLoop(v6ConnList, mdnsV6Addr, quitCh)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// probeLoop performs mdns probe actively to get hostname updates.
|
||||||
|
func (m *mdns) probeLoop(conns []*net.UDPConn, remoteAddr net.Addr, quitCh chan struct{}) {
|
||||||
|
bo := backoff.NewBackoff("mdns probe", func(format string, args ...any) {}, time.Second*30)
|
||||||
|
for {
|
||||||
|
err := m.probe(conns, remoteAddr)
|
||||||
|
if isErrNetUnreachableOrInvalid(err) {
|
||||||
|
ctrld.ProxyLogger.Load().Warn().Msgf("stop probing %q: network unreachable or invalid", remoteAddr)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
ctrld.ProxyLogger.Load().Warn().Err(err).Msg("error while probing mdns")
|
||||||
|
bo.BackOff(context.Background(), errors.New("mdns probe backoff"))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
<-quitCh
|
||||||
|
for _, conn := range conns {
|
||||||
|
_ = conn.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// readLoop reads from mdns connection, save/update any hostnames found.
|
||||||
|
func (m *mdns) readLoop(conn *net.UDPConn) {
|
||||||
|
defer conn.Close()
|
||||||
|
buf := make([]byte, dns.MaxMsgSize)
|
||||||
|
|
||||||
|
for {
|
||||||
|
_ = conn.SetReadDeadline(time.Now().Add(time.Second * 30))
|
||||||
|
n, _, err := conn.ReadFromUDP(buf)
|
||||||
|
if err != nil {
|
||||||
|
if err, ok := err.(*net.OpError); ok && (err.Timeout() || err.Temporary()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ctrld.ProxyLogger.Load().Debug().Err(err).Msg("mdns readLoop error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var msg dns.Msg
|
||||||
|
if err := msg.Unpack(buf[:n]); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var ip, name string
|
||||||
|
rrs := make([]dns.RR, 0, len(msg.Answer)+len(msg.Extra))
|
||||||
|
rrs = append(rrs, msg.Answer...)
|
||||||
|
rrs = append(rrs, msg.Extra...)
|
||||||
|
for _, rr := range rrs {
|
||||||
|
switch ar := rr.(type) {
|
||||||
|
case *dns.A:
|
||||||
|
ip, name = ar.A.String(), ar.Hdr.Name
|
||||||
|
case *dns.AAAA:
|
||||||
|
ip, name = ar.AAAA.String(), ar.Hdr.Name
|
||||||
|
}
|
||||||
|
if ip != "" && name != "" {
|
||||||
|
name = normalizeHostname(name)
|
||||||
|
if val, loaded := m.name.LoadOrStore(ip, name); !loaded {
|
||||||
|
ctrld.ProxyLogger.Load().Debug().Msgf("found hostname: %q, ip: %q via mdns", name, ip)
|
||||||
|
} else {
|
||||||
|
old := val.(string)
|
||||||
|
if old != name {
|
||||||
|
ctrld.ProxyLogger.Load().Debug().Msgf("update hostname: %q, ip: %q, old: %q via mdns", name, ip, old)
|
||||||
|
m.name.Store(ip, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ip, name = "", ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// probe performs mdns queries with known services.
|
||||||
|
func (m *mdns) probe(conns []*net.UDPConn, remoteAddr net.Addr) error {
|
||||||
|
msg := new(dns.Msg)
|
||||||
|
msg.Question = make([]dns.Question, len(services))
|
||||||
|
msg.Compress = true
|
||||||
|
for i, service := range services {
|
||||||
|
msg.Question[i] = dns.Question{
|
||||||
|
Name: dns.CanonicalName(service),
|
||||||
|
Qtype: dns.TypePTR,
|
||||||
|
Qclass: dns.ClassINET,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := msg.Pack()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, conn := range conns {
|
||||||
|
_ = conn.SetWriteDeadline(time.Now().Add(time.Second * 30))
|
||||||
|
if _, werr := conn.WriteTo(buf, remoteAddr); werr != nil {
|
||||||
|
err = werr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func multicastInterfaces() ([]net.Interface, error) {
|
||||||
|
ifaces, err := net.Interfaces()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
interfaces := make([]net.Interface, 0, len(ifaces))
|
||||||
|
for _, ifi := range ifaces {
|
||||||
|
if (ifi.Flags & net.FlagUp) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (ifi.Flags & net.FlagMulticast) > 0 {
|
||||||
|
interfaces = append(interfaces, ifi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return interfaces, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isErrNetUnreachableOrInvalid(err error) bool {
|
||||||
|
var se *os.SyscallError
|
||||||
|
if errors.As(err, &se) {
|
||||||
|
return se.Err == syscall.ENETUNREACH || se.Err == syscall.EINVAL
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package clientinfo
|
||||||
|
|
||||||
|
var services = [...]string{
|
||||||
|
// From: https://jonathanmumm.com/tech-it/mdns-bonjour-bible-common-service-strings-for-various-vendors/
|
||||||
|
"_afpovertcp._tcp.local.",
|
||||||
|
"_airdroid._tcp.local.",
|
||||||
|
"_airdrop._tcp.local.",
|
||||||
|
"_airplay._tcp.local.",
|
||||||
|
"_airport._tcp.local.",
|
||||||
|
"_amzn-wplay._tcp.local.",
|
||||||
|
"_sub._apple-mobdev2._tcp.local.",
|
||||||
|
"_apple-mobdev2._tcp.local.",
|
||||||
|
"_apple-sasl._tcp.local.",
|
||||||
|
"_atc._tcp.local.",
|
||||||
|
"_sketchmirror._tcp.local.",
|
||||||
|
"_bp2p._tcp.local.",
|
||||||
|
"_Friendly._sub._bp2p._tcp.local.",
|
||||||
|
"_invoke._sub._bp2p._tcp.local.",
|
||||||
|
"_webdav._sub._bp2p._tcp.local.",
|
||||||
|
"_device-info._tcp.local.",
|
||||||
|
"_distcc._tcp.local.",
|
||||||
|
"_dpap._tcp.local.",
|
||||||
|
"_eppc._tcp.local.",
|
||||||
|
"_esdevice._tcp.local.",
|
||||||
|
"_esfileshare._tcp.local.",
|
||||||
|
"_ftp._tcp.local.",
|
||||||
|
"_googlecast._tcp.local.",
|
||||||
|
"_googlezone._tcp.local.",
|
||||||
|
"_hap._tcp.local.",
|
||||||
|
"_homekit._tcp.local.",
|
||||||
|
"_home-sharing._tcp.local.",
|
||||||
|
"_http._tcp.local.",
|
||||||
|
"_hudson._tcp.local.",
|
||||||
|
"_ica-networking._tcp.local.",
|
||||||
|
"_print._sub._ipp._tcp.local.",
|
||||||
|
"_cups._sub._ipps._tcp.local.",
|
||||||
|
"_print._sub._ipps._tcp.local.",
|
||||||
|
"_jenkins._tcp.local.",
|
||||||
|
"_KeynoteControl._tcp.local.",
|
||||||
|
"_keynotepair._tcp.local.",
|
||||||
|
"_mediaremotetv._tcp.local.",
|
||||||
|
"_nfs._tcp.local.",
|
||||||
|
"_nvstream._tcp.local.",
|
||||||
|
"_androidtvremote._tcp.local.",
|
||||||
|
"_omnistate._tcp.local.",
|
||||||
|
"_photoshopserver._tcp.local.",
|
||||||
|
"_printer._tcp.local.",
|
||||||
|
"_raop._tcp.local.",
|
||||||
|
"_readynas._tcp.local.",
|
||||||
|
"_rfb._tcp.local.",
|
||||||
|
"_physicalweb._tcp.local.",
|
||||||
|
"_rsp._tcp.local.",
|
||||||
|
"_scanner._tcp.local.",
|
||||||
|
"_sftp-ssh._tcp.local.",
|
||||||
|
"_sleep-proxy._udp.local.",
|
||||||
|
"_smb._tcp.local.",
|
||||||
|
"_spotify-connect._tcp.local.",
|
||||||
|
"_ssh._tcp.local.",
|
||||||
|
"_teamviewer._tcp.local.",
|
||||||
|
"_telnet._tcp.local.",
|
||||||
|
"_touch-able._tcp.local.",
|
||||||
|
"_tunnel._tcp.local.",
|
||||||
|
"_webdav._tcp.local.",
|
||||||
|
"_webdav._tcp.local.",
|
||||||
|
"_workstation._tcp.local.",
|
||||||
|
"_xserveraid._tcp.local.",
|
||||||
|
|
||||||
|
// Merlin
|
||||||
|
"_alexa._tcp",
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package clientinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/Control-D-Inc/ctrld/internal/router"
|
||||||
|
"github.com/Control-D-Inc/ctrld/internal/router/merlin"
|
||||||
|
|
||||||
|
"github.com/Control-D-Inc/ctrld"
|
||||||
|
"github.com/Control-D-Inc/ctrld/internal/router/nvram"
|
||||||
|
)
|
||||||
|
|
||||||
|
const merlinNvramCustomClientListKey = "custom_clientlist"
|
||||||
|
|
||||||
|
type merlinDiscover struct {
|
||||||
|
hostname sync.Map // mac => hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *merlinDiscover) refresh() error {
|
||||||
|
if router.Name() != merlin.Name {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out, err := nvram.Run("get", merlinNvramCustomClientListKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctrld.ProxyLogger.Load().Debug().Msg("reading Merlin custom client list")
|
||||||
|
m.parseMerlinCustomClientList(out)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *merlinDiscover) LookupHostnameByIP(ip string) string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *merlinDiscover) LookupHostnameByMac(mac string) string {
|
||||||
|
val, ok := m.hostname.Load(mac)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return val.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// "nvram get custom_clientlist" output:
|
||||||
|
//
|
||||||
|
// <client 1>00:00:00:00:00:01>0>4>><client 2>00:00:00:00:00:02>0>24>>...
|
||||||
|
//
|
||||||
|
// So to parse it, do the following steps:
|
||||||
|
//
|
||||||
|
// - Split by "<" => entries
|
||||||
|
// - For each entry, split by ">" => parts
|
||||||
|
// - Empty parts => skip
|
||||||
|
// - Empty parts[0] => skip empty hostname
|
||||||
|
// - Empty parts[1] => skip empty MAC
|
||||||
|
func (m *merlinDiscover) parseMerlinCustomClientList(data string) {
|
||||||
|
entries := strings.Split(data, "<")
|
||||||
|
for _, entry := range entries {
|
||||||
|
parts := strings.SplitN(string(entry), ">", 3)
|
||||||
|
if len(parts) < 2 || len(parts[0]) == 0 || len(parts[1]) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
hostname := normalizeHostname(parts[0])
|
||||||
|
mac := strings.ToLower(parts[1])
|
||||||
|
m.hostname.Store(mac, hostname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *merlinDiscover) String() string {
|
||||||
|
return "merlin"
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package clientinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseMerlinCustomClientList(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
clientList string
|
||||||
|
macList []string
|
||||||
|
hostnameList []string
|
||||||
|
macNotPresentList []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"normal",
|
||||||
|
"<client1>00:00:00:00:00:01>0>4>>",
|
||||||
|
[]string{"00:00:00:00:00:01"},
|
||||||
|
[]string{"client1"},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"multiple clients",
|
||||||
|
"<client1>00:00:00:00:00:01>0>4>><client2>00:00:00:00:00:02>0>24>>",
|
||||||
|
[]string{"00:00:00:00:00:01", "00:00:00:00:00:02"},
|
||||||
|
[]string{"client1", "client2"},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"empty hostname",
|
||||||
|
"<client1>00:00:00:00:00:01>0>4>><>00:00:00:00:00:02>0>24>>",
|
||||||
|
[]string{"00:00:00:00:00:01"},
|
||||||
|
[]string{"client1"},
|
||||||
|
[]string{"00:00:00:00:00:02"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"empty dhcp",
|
||||||
|
"<client1>00:00:00:00:00:01>0>4>><client 1>>>",
|
||||||
|
[]string{"00:00:00:00:00:01"},
|
||||||
|
[]string{"client1"},
|
||||||
|
[]string{""},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"invalid",
|
||||||
|
"qwerty",
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"empty",
|
||||||
|
"",
|
||||||
|
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := &merlinDiscover{}
|
||||||
|
m.parseMerlinCustomClientList(tc.clientList)
|
||||||
|
for i, mac := range tc.macList {
|
||||||
|
val, ok := m.hostname.Load(mac)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("missing hostname: %s", mac)
|
||||||
|
}
|
||||||
|
hostname := val.(string)
|
||||||
|
if hostname != tc.hostnameList[i] {
|
||||||
|
t.Errorf("hostname mismatch, want: %q, got: %q", tc.hostnameList[i], hostname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, mac := range tc.macNotPresentList {
|
||||||
|
if _, ok := m.hostname.Load(mac); ok {
|
||||||
|
t.Errorf("mac2name address %q should not be present", mac)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package clientinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
|
||||||
|
"github.com/Control-D-Inc/ctrld"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ptrDiscover struct {
|
||||||
|
hostname sync.Map // ip => hostname
|
||||||
|
resolver ctrld.Resolver
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ptrDiscover) refresh() error {
|
||||||
|
p.hostname.Range(func(key, value any) bool {
|
||||||
|
ip := key.(string)
|
||||||
|
if name := p.lookupHostname(ip); name != "" {
|
||||||
|
p.hostname.Store(ip, name)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ptrDiscover) LookupHostnameByIP(ip string) string {
|
||||||
|
if val, ok := p.hostname.Load(ip); ok {
|
||||||
|
return val.(string)
|
||||||
|
}
|
||||||
|
return p.lookupHostname(ip)
|
||||||
|
}
|
||||||
|
func (p *ptrDiscover) LookupHostnameByMac(mac string) string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ptrDiscover) String() string {
|
||||||
|
return "ptr"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ptrDiscover) List() []string {
|
||||||
|
var ips []string
|
||||||
|
p.hostname.Range(func(key, value any) bool {
|
||||||
|
ips = append(ips, key.(string))
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return ips
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ptrDiscover) lookupHostnameFromCache(ip string) string {
|
||||||
|
if val, ok := p.hostname.Load(ip); ok {
|
||||||
|
return val.(string)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ptrDiscover) lookupHostname(ip string) string {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||||
|
defer cancel()
|
||||||
|
msg := new(dns.Msg)
|
||||||
|
addr, err := dns.ReverseAddr(ip)
|
||||||
|
if err != nil {
|
||||||
|
ctrld.ProxyLogger.Load().Error().Err(err).Msg("invalid ip address")
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
msg.SetQuestion(addr, dns.TypePTR)
|
||||||
|
ans, err := p.resolver.Resolve(ctx, msg)
|
||||||
|
if err != nil {
|
||||||
|
ctrld.ProxyLogger.Load().Error().Err(err).Msg("could not lookup IP")
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
for _, rr := range ans.Answer {
|
||||||
|
if ptr, ok := rr.(*dns.PTR); ok {
|
||||||
|
hostname := normalizeHostname(ptr.Ptr)
|
||||||
|
p.hostname.Store(ip, hostname)
|
||||||
|
return hostname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
+82
-22
@@ -3,36 +3,39 @@ package controld
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/Control-D-Inc/ctrld"
|
||||||
|
"github.com/Control-D-Inc/ctrld/internal/certs"
|
||||||
|
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
||||||
|
"github.com/Control-D-Inc/ctrld/internal/router"
|
||||||
|
"github.com/Control-D-Inc/ctrld/internal/router/ddwrt"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
resolverDataURL = "https://api.controld.com/utility"
|
apiDomainCom = "api.controld.com"
|
||||||
InvalidConfigCode = 40401
|
apiDomainDev = "api.controld.dev"
|
||||||
|
resolverDataURLCom = "https://api.controld.com/utility"
|
||||||
|
resolverDataURLDev = "https://api.controld.dev/utility"
|
||||||
|
InvalidConfigCode = 40401
|
||||||
)
|
)
|
||||||
|
|
||||||
const bootstrapDNS = "76.76.2.0:53"
|
|
||||||
|
|
||||||
var Dialer = &net.Dialer{
|
|
||||||
Resolver: &net.Resolver{
|
|
||||||
PreferGo: true,
|
|
||||||
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
|
||||||
d := net.Dialer{
|
|
||||||
Timeout: 10 * time.Second,
|
|
||||||
}
|
|
||||||
return d.DialContext(ctx, "udp", bootstrapDNS)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResolverConfig represents Control D resolver data.
|
// ResolverConfig represents Control D resolver data.
|
||||||
type ResolverConfig struct {
|
type ResolverConfig struct {
|
||||||
DOH string `json:"doh"`
|
DOH string `json:"doh"`
|
||||||
|
Ctrld struct {
|
||||||
|
CustomConfig string `json:"custom_config"`
|
||||||
|
} `json:"ctrld"`
|
||||||
Exclude []string `json:"exclude"`
|
Exclude []string `json:"exclude"`
|
||||||
|
UID string `json:"uid"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type utilityResponse struct {
|
type utilityResponse struct {
|
||||||
@@ -54,23 +57,70 @@ func (u UtilityErrorResponse) Error() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type utilityRequest struct {
|
type utilityRequest struct {
|
||||||
UID string `json:"uid"`
|
UID string `json:"uid"`
|
||||||
|
ClientID string `json:"client_id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type utilityOrgRequest struct {
|
||||||
|
ProvToken string `json:"prov_token"`
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetchResolverConfig fetch Control D config for given uid.
|
// FetchResolverConfig fetch Control D config for given uid.
|
||||||
func FetchResolverConfig(uid string) (*ResolverConfig, error) {
|
func FetchResolverConfig(rawUID, version string, cdDev bool) (*ResolverConfig, error) {
|
||||||
body, _ := json.Marshal(utilityRequest{UID: uid})
|
uid, clientID := ParseRawUID(rawUID)
|
||||||
req, err := http.NewRequest("POST", resolverDataURL, bytes.NewReader(body))
|
req := utilityRequest{UID: uid}
|
||||||
|
if clientID != "" {
|
||||||
|
req.ClientID = clientID
|
||||||
|
}
|
||||||
|
body, _ := json.Marshal(req)
|
||||||
|
return postUtilityAPI(version, cdDev, bytes.NewReader(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchResolverUID fetch resolver uid from provision token.
|
||||||
|
func FetchResolverUID(pt, version string, cdDev bool) (*ResolverConfig, error) {
|
||||||
|
hostname, _ := os.Hostname()
|
||||||
|
body, _ := json.Marshal(utilityOrgRequest{ProvToken: pt, Hostname: hostname})
|
||||||
|
return postUtilityAPI(version, cdDev, bytes.NewReader(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
func postUtilityAPI(version string, cdDev bool, body io.Reader) (*ResolverConfig, error) {
|
||||||
|
apiUrl := resolverDataURLCom
|
||||||
|
if cdDev {
|
||||||
|
apiUrl = resolverDataURLDev
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest("POST", apiUrl, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("http.NewRequest: %w", err)
|
return nil, fmt.Errorf("http.NewRequest: %w", err)
|
||||||
}
|
}
|
||||||
q := req.URL.Query()
|
q := req.URL.Query()
|
||||||
q.Set("platform", "ctrld")
|
q.Set("platform", "ctrld")
|
||||||
|
q.Set("version", version)
|
||||||
req.URL.RawQuery = q.Encode()
|
req.URL.RawQuery = q.Encode()
|
||||||
req.Header.Add("Content-Type", "application/json")
|
req.Header.Add("Content-Type", "application/json")
|
||||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||||
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
return Dialer.DialContext(ctx, network, addr)
|
apiDomain := apiDomainCom
|
||||||
|
if cdDev {
|
||||||
|
apiDomain = apiDomainDev
|
||||||
|
}
|
||||||
|
ips := ctrld.LookupIP(apiDomain)
|
||||||
|
if len(ips) == 0 {
|
||||||
|
ctrld.ProxyLogger.Load().Warn().Msgf("No IPs found for %s, connecting to %s", apiDomain, addr)
|
||||||
|
return ctrldnet.Dialer.DialContext(ctx, network, addr)
|
||||||
|
}
|
||||||
|
ctrld.ProxyLogger.Load().Debug().Msgf("API IPs: %v", ips)
|
||||||
|
_, port, _ := net.SplitHostPort(addr)
|
||||||
|
addrs := make([]string, len(ips))
|
||||||
|
for i := range ips {
|
||||||
|
addrs[i] = net.JoinHostPort(ips[i], port)
|
||||||
|
}
|
||||||
|
d := &ctrldnet.ParallelDialer{}
|
||||||
|
return d.DialContext(ctx, network, addrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
if router.Name() == ddwrt.Name {
|
||||||
|
transport.TLSClientConfig = &tls.Config{RootCAs: certs.CACertPool()}
|
||||||
}
|
}
|
||||||
client := http.Client{
|
client := http.Client{
|
||||||
Timeout: 10 * time.Second,
|
Timeout: 10 * time.Second,
|
||||||
@@ -96,3 +146,13 @@ func FetchResolverConfig(uid string) (*ResolverConfig, error) {
|
|||||||
}
|
}
|
||||||
return &ur.Body.Resolver, nil
|
return &ur.Body.Resolver, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseRawUID parse the input raw UID, returning real UID and ClientID.
|
||||||
|
// The raw UID can have 2 forms:
|
||||||
|
//
|
||||||
|
// - <uid>
|
||||||
|
// - <uid>/<client_id>
|
||||||
|
func ParseRawUID(rawUID string) (string, string) {
|
||||||
|
uid, clientID, _ := strings.Cut(rawUID, "/")
|
||||||
|
return uid, clientID
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//go:build controld
|
|
||||||
|
|
||||||
package controld
|
package controld
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -8,26 +6,26 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
const utilityURL = "https://api.controld.com/utility"
|
func Test_parseUID(t *testing.T) {
|
||||||
|
|
||||||
func TestFetchResolverConfig(t *testing.T) {
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
uid string
|
uid string
|
||||||
wantErr bool
|
wantUID string
|
||||||
|
wantClientID string
|
||||||
}{
|
}{
|
||||||
{"valid", "p2", false},
|
{"empty", "", "", ""},
|
||||||
{"invalid uid", "abcd1234", true},
|
{"only uid", "abcd1234", "abcd1234", ""},
|
||||||
|
{"with client id", "abcd1234/clientID", "abcd1234", "clientID"},
|
||||||
|
{"with empty clientID", "abcd1234/", "abcd1234", ""},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
tc := tc
|
tc := tc
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
got, err := FetchResolverConfig(tc.uid)
|
gotUID, gotClientID := ParseRawUID(tc.uid)
|
||||||
assert.False(t, (err != nil) != tc.wantErr)
|
assert.Equal(t, tc.wantUID, gotUID)
|
||||||
if !tc.wantErr {
|
assert.Equal(t, tc.wantClientID, gotClientID)
|
||||||
assert.NotEmpty(t, got.DOH)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
//go:build controld
|
||||||
|
|
||||||
|
package controld
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFetchResolverConfig(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
uid string
|
||||||
|
dev bool
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"valid com", "p2", false, false},
|
||||||
|
{"valid dev", "p2", true, false},
|
||||||
|
{"invalid uid", "abcd1234", false, true},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
got, err := FetchResolverConfig(tc.uid, "dev-test", tc.dev)
|
||||||
|
require.False(t, (err != nil) != tc.wantErr, err)
|
||||||
|
if !tc.wantErr {
|
||||||
|
assert.NotEmpty(t, got.DOH)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build linux || freebsd || openbsd
|
||||||
|
|
||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"tailscale.com/atomicfile"
|
||||||
|
"tailscale.com/types/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed resolvconf-workaround.sh
|
||||||
|
var workaroundScript []byte
|
||||||
|
|
||||||
|
// resolvconfConfigName is the name of the config submitted to
|
||||||
|
// resolvconf.
|
||||||
|
// The name starts with 'tun' in order to match the hardcoded
|
||||||
|
// interface order in debian resolvconf, which will place this
|
||||||
|
// configuration ahead of regular network links. In theory, this
|
||||||
|
// doesn't matter because we then fix things up to ensure our config
|
||||||
|
// is the only one in use, but in case that fails, this will make our
|
||||||
|
// configuration slightly preferred.
|
||||||
|
// The 'inet' suffix has no specific meaning, but conventionally
|
||||||
|
// resolvconf implementations encourage adding a suffix roughly
|
||||||
|
// indicating where the config came from, and "inet" is the "none of
|
||||||
|
// the above" value (rather than, say, "ppp" or "dhcp").
|
||||||
|
const resolvconfConfigName = "ctrld.inet"
|
||||||
|
|
||||||
|
// resolvconfLibcHookPath is the directory containing libc update
|
||||||
|
// scripts, which are run by Debian resolvconf when /etc/resolv.conf
|
||||||
|
// has been updated.
|
||||||
|
const resolvconfLibcHookPath = "/etc/resolvconf/update-libc.d"
|
||||||
|
|
||||||
|
// resolvconfHookPath is the name of the libc hook script we install
|
||||||
|
// to force Ctrld's DNS config to take effect.
|
||||||
|
var resolvconfHookPath = filepath.Join(resolvconfLibcHookPath, "ctrld")
|
||||||
|
|
||||||
|
// resolvconfManager manages DNS configuration using the Debian
|
||||||
|
// implementation of the `resolvconf` program, written by Thomas Hood.
|
||||||
|
type resolvconfManager struct {
|
||||||
|
logf logger.Logf
|
||||||
|
listRecordsPath string
|
||||||
|
interfacesDir string
|
||||||
|
scriptInstalled bool // libc update script has been installed
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ OSConfigurator = (*resolvconfManager)(nil)
|
||||||
|
|
||||||
|
func newDebianResolvconfManager(logf logger.Logf) (*resolvconfManager, error) {
|
||||||
|
ret := &resolvconfManager{
|
||||||
|
logf: logf,
|
||||||
|
listRecordsPath: "/lib/resolvconf/list-records",
|
||||||
|
interfacesDir: "/etc/resolvconf/run/interface", // panic fallback if nothing seems to work
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(ret.listRecordsPath); os.IsNotExist(err) {
|
||||||
|
// This might be a Debian system from before the big /usr
|
||||||
|
// merge, try /usr instead.
|
||||||
|
ret.listRecordsPath = "/usr" + ret.listRecordsPath
|
||||||
|
}
|
||||||
|
// The runtime directory is currently (2020-04) canonically
|
||||||
|
// /etc/resolvconf/run, but the manpage is making noise about
|
||||||
|
// switching to /run/resolvconf and dropping the /etc path. So,
|
||||||
|
// let's probe the possible directories and use the first one
|
||||||
|
// that works.
|
||||||
|
for _, path := range []string{
|
||||||
|
"/etc/resolvconf/run/interface",
|
||||||
|
"/run/resolvconf/interface",
|
||||||
|
"/var/run/resolvconf/interface",
|
||||||
|
} {
|
||||||
|
if _, err := os.Stat(path); err == nil {
|
||||||
|
ret.interfacesDir = path
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ret.interfacesDir == "" {
|
||||||
|
// None of the paths seem to work, use the canonical location
|
||||||
|
// that the current manpage says to use.
|
||||||
|
ret.interfacesDir = "/etc/resolvconf/run/interfaces"
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *resolvconfManager) deleteCtrldConfig() error {
|
||||||
|
cmd := exec.Command("resolvconf", "-d", resolvconfConfigName)
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("running %s: %s", cmd, out)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *resolvconfManager) SetDNS(config OSConfig) error {
|
||||||
|
if !m.scriptInstalled {
|
||||||
|
m.logf("injecting resolvconf workaround script")
|
||||||
|
if err := os.MkdirAll(resolvconfLibcHookPath, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := atomicfile.WriteFile(resolvconfHookPath, workaroundScript, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.scriptInstalled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.IsZero() {
|
||||||
|
if err := m.deleteCtrldConfig(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stdin := new(bytes.Buffer)
|
||||||
|
writeResolvConf(stdin, config.Nameservers, config.SearchDomains) // dns_direct.go
|
||||||
|
|
||||||
|
// This resolvconf implementation doesn't support exclusive
|
||||||
|
// mode or interface priorities, so it will end up blending
|
||||||
|
// our configuration with other sources. However, this will
|
||||||
|
// get fixed up by the script we injected above.
|
||||||
|
cmd := exec.Command("resolvconf", "-a", resolvconfConfigName)
|
||||||
|
cmd.Stdin = stdin
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("running %s: %s", cmd, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *resolvconfManager) Close() error {
|
||||||
|
if err := m.deleteCtrldConfig(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.scriptInstalled {
|
||||||
|
m.logf("removing resolvconf workaround script")
|
||||||
|
os.Remove(resolvconfHookPath) // Best-effort
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *resolvconfManager) Mode() string {
|
||||||
|
return "resolvconf"
|
||||||
|
}
|
||||||
@@ -2,7 +2,8 @@
|
|||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//lint:file-ignore U1000 satisfy CI.
|
//lint:file-ignore U1000 Ignore, this file is forked from upstream code.
|
||||||
|
//lint:file-ignore ST1005 Ignore, this file is forked from upstream code.
|
||||||
|
|
||||||
package dns
|
package dns
|
||||||
|
|
||||||
@@ -144,6 +145,10 @@ type directManager struct {
|
|||||||
lastWarnContents []byte // last resolv.conf contents that we warned about
|
lastWarnContents []byte // last resolv.conf contents that we warned about
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newDirectManager(logf logger.Logf) *directManager {
|
||||||
|
return newDirectManagerOnFS(logf, directFS{})
|
||||||
|
}
|
||||||
|
|
||||||
func newDirectManagerOnFS(logf logger.Logf, fs wholeFileFS) *directManager {
|
func newDirectManagerOnFS(logf logger.Logf, fs wholeFileFS) *directManager {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
m := &directManager{
|
m := &directManager{
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"tailscale.com/types/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewOSConfigurator(logf logger.Logf, _ string) (OSConfigurator, error) {
|
||||||
|
bs, err := os.ReadFile("/etc/resolv.conf")
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return newDirectManager(logf), nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("reading /etc/resolv.conf: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch resolvOwner(bs) {
|
||||||
|
case "resolvconf":
|
||||||
|
switch resolvconfStyle() {
|
||||||
|
case "":
|
||||||
|
return newDirectManager(logf), nil
|
||||||
|
case "debian":
|
||||||
|
return newDebianResolvconfManager(logf)
|
||||||
|
case "openresolv":
|
||||||
|
return newOpenresolvManager()
|
||||||
|
default:
|
||||||
|
logf("[unexpected] got unknown flavor of resolvconf %q, falling back to direct manager", resolvconfStyle())
|
||||||
|
return newDirectManager(logf), nil
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return newDirectManager(logf), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//lint:file-ignore U1000 Ignore this file, it's a copy.
|
||||||
|
|
||||||
package dns
|
package dns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -62,6 +64,10 @@ func NewOSConfigurator(logf logger.Logf, interfaceName string) (ret OSConfigurat
|
|||||||
return newResolvedManager(logf, interfaceName)
|
return newResolvedManager(logf, interfaceName)
|
||||||
case "network-manager":
|
case "network-manager":
|
||||||
return newNMManager(interfaceName)
|
return newNMManager(interfaceName)
|
||||||
|
case "debian-resolvconf":
|
||||||
|
return newDebianResolvconfManager(logf)
|
||||||
|
case "openresolv":
|
||||||
|
return newOpenresolvManager()
|
||||||
default:
|
default:
|
||||||
logf("[unexpected] detected unknown DNS mode %q, using direct manager as last resort", mode)
|
logf("[unexpected] detected unknown DNS mode %q, using direct manager as last resort", mode)
|
||||||
return newDirectManagerOnFS(logf, env.fs), nil
|
return newDirectManagerOnFS(logf, env.fs), nil
|
||||||
|
|||||||
+5
-3
@@ -14,8 +14,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
|
"github.com/josharian/native"
|
||||||
"tailscale.com/util/dnsname"
|
"tailscale.com/util/dnsname"
|
||||||
"tailscale.com/util/endian"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -31,6 +31,8 @@ type nmManager struct {
|
|||||||
dnsManager dbus.BusObject
|
dnsManager dbus.BusObject
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ OSConfigurator = (*nmManager)(nil)
|
||||||
|
|
||||||
func newNMManager(interfaceName string) (*nmManager, error) {
|
func newNMManager(interfaceName string) (*nmManager, error) {
|
||||||
conn, err := dbus.SystemBus()
|
conn, err := dbus.SystemBus()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -129,7 +131,7 @@ func (m *nmManager) trySet(ctx context.Context, config OSConfig) error {
|
|||||||
for _, ip := range config.Nameservers {
|
for _, ip := range config.Nameservers {
|
||||||
b := ip.As16()
|
b := ip.As16()
|
||||||
if ip.Is4() {
|
if ip.Is4() {
|
||||||
dnsv4 = append(dnsv4, endian.Native.Uint32(b[12:]))
|
dnsv4 = append(dnsv4, native.Endian.Uint32(b[12:]))
|
||||||
} else {
|
} else {
|
||||||
dnsv6 = append(dnsv6, b[:])
|
dnsv6 = append(dnsv6, b[:])
|
||||||
}
|
}
|
||||||
@@ -265,5 +267,5 @@ func (m *nmManager) Close() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *nmManager) Mode() string {
|
func (m *nmManager) Mode() string {
|
||||||
return "network-maanger"
|
return "network-manager"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build linux || freebsd || openbsd
|
||||||
|
|
||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
// openresolvManager manages DNS configuration using the openresolv
|
||||||
|
// implementation of the `resolvconf` program.
|
||||||
|
type openresolvManager struct{}
|
||||||
|
|
||||||
|
var _ OSConfigurator = (*openresolvManager)(nil)
|
||||||
|
|
||||||
|
func newOpenresolvManager() (openresolvManager, error) {
|
||||||
|
return openresolvManager{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m openresolvManager) deleteTailscaleConfig() error {
|
||||||
|
cmd := exec.Command("resolvconf", "-f", "-d", "ctrld")
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("running %s: %s", cmd, out)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m openresolvManager) SetDNS(config OSConfig) error {
|
||||||
|
if config.IsZero() {
|
||||||
|
return m.deleteTailscaleConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
var stdin bytes.Buffer
|
||||||
|
writeResolvConf(&stdin, config.Nameservers, config.SearchDomains)
|
||||||
|
|
||||||
|
cmd := exec.Command("resolvconf", "-m", "0", "-x", "-a", "ctrld")
|
||||||
|
cmd.Stdin = &stdin
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("running %s: %s", cmd, out)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m openresolvManager) Close() error {
|
||||||
|
return m.deleteTailscaleConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m openresolvManager) Mode() string {
|
||||||
|
return "resolvconf"
|
||||||
|
}
|
||||||
@@ -13,6 +13,8 @@ import (
|
|||||||
"tailscale.com/util/dnsname"
|
"tailscale.com/util/dnsname"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var _ OSConfigurator = (*directManager)(nil)
|
||||||
|
|
||||||
// An OSConfigurator applies DNS settings to the operating system.
|
// An OSConfigurator applies DNS settings to the operating system.
|
||||||
type OSConfigurator interface {
|
type OSConfigurator interface {
|
||||||
// SetDNS updates the OS's DNS configuration to match cfg.
|
// SetDNS updates the OS's DNS configuration to match cfg.
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||||
|
# Use of this source code is governed by a BSD-style
|
||||||
|
# license that can be found in the LICENSE file.
|
||||||
|
#
|
||||||
|
# This script is a workaround for a vpn-unfriendly behavior of the
|
||||||
|
# original resolvconf by Thomas Hood. Unlike the `openresolv`
|
||||||
|
# implementation (whose binary is also called resolvconf,
|
||||||
|
# confusingly), the original resolvconf lacks a way to specify
|
||||||
|
# "exclusive mode" for a provider configuration. In practice, this
|
||||||
|
# means that if Ctrld wants to install a DNS configuration, that
|
||||||
|
# config will get "blended" with the configs from other sources,
|
||||||
|
# rather than override those other sources.
|
||||||
|
#
|
||||||
|
# This script gets installed at /etc/resolvconf/update-libc.d, which
|
||||||
|
# is a directory of hook scripts that get run after resolvconf's libc
|
||||||
|
# helper has finished rewriting /etc/resolv.conf. It's meant to notify
|
||||||
|
# consumers of resolv.conf of a new configuration.
|
||||||
|
#
|
||||||
|
# Instead, we use that hook mechanism to reach into resolvconf's
|
||||||
|
# stuff, and rewrite the libc-generated resolv.conf to exclusively
|
||||||
|
# contain Ctrld's configuration - effectively implementing
|
||||||
|
# exclusive mode ourselves in post-production.
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ -n "$CTRLD_RESOLVCONF_HOOK_LOOP" ]; then
|
||||||
|
# Hook script being invoked by itself, skip.
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f ctrld.inet ]; then
|
||||||
|
# Ctrld isn't trying to manage DNS, do nothing.
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! grep resolvconf /etc/resolv.conf >/dev/null; then
|
||||||
|
# resolvconf isn't managing /etc/resolv.conf, do nothing.
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Write out a modified /etc/resolv.conf containing just our config.
|
||||||
|
(
|
||||||
|
if [ -f /etc/resolvconf/resolv.conf.d/head ]; then
|
||||||
|
cat /etc/resolvconf/resolv.conf.d/head
|
||||||
|
fi
|
||||||
|
echo "# Ctrld workaround applied to set exclusive DNS configuration."
|
||||||
|
cat tun-tailscale.inet
|
||||||
|
if [ -f /etc/resolvconf/resolv.conf.d/base ]; then
|
||||||
|
# Keep options and sortlist, discard other base things since
|
||||||
|
# they're the things we're trying to override.
|
||||||
|
grep -e 'sortlist ' -e 'options ' /etc/resolvconf/resolv.conf.d/base || true
|
||||||
|
fi
|
||||||
|
if [ -f /etc/resolvconf/resolv.conf.d/tail ]; then
|
||||||
|
cat /etc/resolvconf/resolv.conf.d/tail
|
||||||
|
fi
|
||||||
|
) >/etc/resolv.conf
|
||||||
|
|
||||||
|
if [ -d /etc/resolvconf/update-libc.d ] ; then
|
||||||
|
# Re-notify libc watchers that we've changed resolv.conf again.
|
||||||
|
export CTRLD_RESOLVCONF_HOOK_LOOP=1
|
||||||
|
exec run-parts /etc/resolvconf/update-libc.d
|
||||||
|
fi
|
||||||
@@ -15,7 +15,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"tailscale.com/util/dnsname"
|
"tailscale.com/util/dnsname"
|
||||||
"tailscale.com/util/strs"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Path is the canonical location of resolv.conf.
|
// Path is the canonical location of resolv.conf.
|
||||||
@@ -63,7 +62,7 @@ func Parse(r io.Reader) (*Config, error) {
|
|||||||
line, _, _ = strings.Cut(line, "#") // remove any comments
|
line, _, _ = strings.Cut(line, "#") // remove any comments
|
||||||
line = strings.TrimSpace(line)
|
line = strings.TrimSpace(line)
|
||||||
|
|
||||||
if s, ok := strs.CutPrefix(line, "nameserver"); ok {
|
if s, ok := strings.CutPrefix(line, "nameserver"); ok {
|
||||||
nameserver := strings.TrimSpace(s)
|
nameserver := strings.TrimSpace(s)
|
||||||
if len(nameserver) == len(s) {
|
if len(nameserver) == len(s) {
|
||||||
return nil, fmt.Errorf("missing space after \"nameserver\" in %q", line)
|
return nil, fmt.Errorf("missing space after \"nameserver\" in %q", line)
|
||||||
@@ -76,7 +75,7 @@ func Parse(r io.Reader) (*Config, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if s, ok := strs.CutPrefix(line, "search"); ok {
|
if s, ok := strings.CutPrefix(line, "search"); ok {
|
||||||
domains := strings.TrimSpace(s)
|
domains := strings.TrimSpace(s)
|
||||||
if len(domains) == len(s) {
|
if len(domains) == len(s) {
|
||||||
// No leading space?!
|
// No leading space?!
|
||||||
|
|||||||
+90
-20
@@ -37,14 +37,45 @@ const reconfigTimeout = time.Second
|
|||||||
// Clients connect to the bus and walk that same hierarchy to invoke
|
// Clients connect to the bus and walk that same hierarchy to invoke
|
||||||
// RPCs, get/set properties, or listen for signals.
|
// RPCs, get/set properties, or listen for signals.
|
||||||
const (
|
const (
|
||||||
dbusResolvedObject = "org.freedesktop.resolve1"
|
dbusResolvedObject = "org.freedesktop.resolve1"
|
||||||
dbusResolvedPath dbus.ObjectPath = "/org/freedesktop/resolve1"
|
dbusNetworkdObject = "org.freedesktop.network1"
|
||||||
dbusResolvedInterface = "org.freedesktop.resolve1.Manager"
|
dbusResolvedPath dbus.ObjectPath = "/org/freedesktop/resolve1"
|
||||||
dbusPath dbus.ObjectPath = "/org/freedesktop/DBus"
|
dbusNetworkdPath dbus.ObjectPath = "/org/freedesktop/network1"
|
||||||
dbusInterface = "org.freedesktop.DBus"
|
dbusResolvedInterface = "org.freedesktop.resolve1.Manager"
|
||||||
dbusOwnerSignal = "NameOwnerChanged" // broadcast when a well-known name's owning process changes.
|
dbusNetworkdInterface = "org.freedesktop.network1.Manager"
|
||||||
|
dbusPath dbus.ObjectPath = "/org/freedesktop/DBus"
|
||||||
|
dbusInterface = "org.freedesktop.DBus"
|
||||||
|
dbusOwnerSignal = "NameOwnerChanged" // broadcast when a well-known name's owning process changes.
|
||||||
|
dbusResolvedErrorLinkBusy = "org.freedesktop.resolve1.LinkBusy"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
dbusSetLinkDNS string
|
||||||
|
dbusSetLinkDomains string
|
||||||
|
dbusSetLinkDefaultRoute string
|
||||||
|
dbusSetLinkLLMNR string
|
||||||
|
dbusSetLinkMulticastDNS string
|
||||||
|
dbusSetLinkDNSSEC string
|
||||||
|
dbusSetLinkDNSOverTLS string
|
||||||
|
dbusFlushCaches string
|
||||||
|
dbusRevertLink string
|
||||||
|
)
|
||||||
|
|
||||||
|
func setDbusMethods(dbusInterface string) {
|
||||||
|
dbusSetLinkDNS = dbusInterface + ".SetLinkDNS"
|
||||||
|
dbusSetLinkDomains = dbusInterface + ".SetLinkDomains"
|
||||||
|
dbusSetLinkDefaultRoute = dbusInterface + ".SetLinkDefaultRoute"
|
||||||
|
dbusSetLinkLLMNR = dbusInterface + ".SetLinkLLMNR"
|
||||||
|
dbusSetLinkMulticastDNS = dbusInterface + ".SetLinkMulticastDNS"
|
||||||
|
dbusSetLinkDNSSEC = dbusInterface + ".SetLinkDNSSEC"
|
||||||
|
dbusSetLinkDNSOverTLS = dbusInterface + ".SetLinkDNSOverTLS"
|
||||||
|
dbusFlushCaches = dbusInterface + ".FlushCaches"
|
||||||
|
dbusRevertLink = dbusInterface + ".RevertLink"
|
||||||
|
if dbusInterface == dbusNetworkdInterface {
|
||||||
|
dbusRevertLink = dbusInterface + ".RevertLinkDNS"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type resolvedLinkNameserver struct {
|
type resolvedLinkNameserver struct {
|
||||||
Family int32
|
Family int32
|
||||||
Address []byte
|
Address []byte
|
||||||
@@ -70,8 +101,11 @@ type resolvedManager struct {
|
|||||||
ifidx int
|
ifidx int
|
||||||
|
|
||||||
configCR chan changeRequest // tracks OSConfigs changes and error responses
|
configCR chan changeRequest // tracks OSConfigs changes and error responses
|
||||||
|
revertCh chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ OSConfigurator = (*resolvedManager)(nil)
|
||||||
|
|
||||||
func newResolvedManager(logf logger.Logf, interfaceName string) (*resolvedManager, error) {
|
func newResolvedManager(logf logger.Logf, interfaceName string) (*resolvedManager, error) {
|
||||||
iface, err := net.InterfaceByName(interfaceName)
|
iface, err := net.InterfaceByName(interfaceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -89,6 +123,7 @@ func newResolvedManager(logf logger.Logf, interfaceName string) (*resolvedManage
|
|||||||
ifidx: iface.Index,
|
ifidx: iface.Index,
|
||||||
|
|
||||||
configCR: make(chan changeRequest),
|
configCR: make(chan changeRequest),
|
||||||
|
revertCh: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
go mgr.run(ctx)
|
go mgr.run(ctx)
|
||||||
@@ -117,6 +152,16 @@ func (m *resolvedManager) SetDNS(config OSConfig) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newResolvedObject(conn *dbus.Conn) dbus.BusObject {
|
||||||
|
setDbusMethods(dbusResolvedInterface)
|
||||||
|
return conn.Object(dbusResolvedObject, dbusResolvedPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNetworkdObject(conn *dbus.Conn) dbus.BusObject {
|
||||||
|
setDbusMethods(dbusNetworkdInterface)
|
||||||
|
return conn.Object(dbusNetworkdObject, dbusNetworkdPath)
|
||||||
|
}
|
||||||
|
|
||||||
func (m *resolvedManager) run(ctx context.Context) {
|
func (m *resolvedManager) run(ctx context.Context) {
|
||||||
var (
|
var (
|
||||||
conn *dbus.Conn
|
conn *dbus.Conn
|
||||||
@@ -131,6 +176,22 @@ func (m *resolvedManager) run(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
newManager := newResolvedObject
|
||||||
|
func() {
|
||||||
|
conn, err := dbus.SystemBus()
|
||||||
|
if err != nil {
|
||||||
|
m.logf("dbus connection error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rManager = newManager(conn)
|
||||||
|
if call := rManager.CallWithContext(ctx, dbusRevertLink, 0, m.ifidx); call.Err != nil {
|
||||||
|
if dbusErr, ok := call.Err.(dbus.Error); ok && dbusErr.Name == dbusResolvedErrorLinkBusy {
|
||||||
|
m.logf("[v1] Using %s as manager", dbusNetworkdObject)
|
||||||
|
newManager = newNetworkdObject
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// Reconnect the systemBus if disconnected.
|
// Reconnect the systemBus if disconnected.
|
||||||
reconnect := func() error {
|
reconnect := func() error {
|
||||||
var err error
|
var err error
|
||||||
@@ -151,7 +212,7 @@ func (m *resolvedManager) run(ctx context.Context) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
rManager = conn.Object(dbusResolvedObject, dbus.ObjectPath(dbusResolvedPath))
|
rManager = newManager(conn)
|
||||||
|
|
||||||
// Only receive the DBus signals we need to resync our config on
|
// Only receive the DBus signals we need to resync our config on
|
||||||
// resolved restart. Failure to set filters isn't a fatal error,
|
// resolved restart. Failure to set filters isn't a fatal error,
|
||||||
@@ -160,6 +221,9 @@ func (m *resolvedManager) run(ctx context.Context) {
|
|||||||
if err = conn.AddMatchSignal(dbus.WithMatchObjectPath(dbusPath), dbus.WithMatchInterface(dbusInterface), dbus.WithMatchMember(dbusOwnerSignal), dbus.WithMatchArg(0, dbusResolvedObject)); err != nil {
|
if err = conn.AddMatchSignal(dbus.WithMatchObjectPath(dbusPath), dbus.WithMatchInterface(dbusInterface), dbus.WithMatchMember(dbusOwnerSignal), dbus.WithMatchArg(0, dbusResolvedObject)); err != nil {
|
||||||
m.logf("[v1] Setting DBus signal filter failed: %v", err)
|
m.logf("[v1] Setting DBus signal filter failed: %v", err)
|
||||||
}
|
}
|
||||||
|
if err = conn.AddMatchSignal(dbus.WithMatchObjectPath(dbusPath), dbus.WithMatchInterface(dbusInterface), dbus.WithMatchMember(dbusOwnerSignal), dbus.WithMatchArg(0, dbusNetworkdObject)); err != nil {
|
||||||
|
m.logf("[v1] Setting DBus signal filter failed: %v", err)
|
||||||
|
}
|
||||||
conn.Signal(signals)
|
conn.Signal(signals)
|
||||||
|
|
||||||
// Reset backoff and SetNSOSHealth after successful on reconnect.
|
// Reset backoff and SetNSOSHealth after successful on reconnect.
|
||||||
@@ -179,13 +243,15 @@ func (m *resolvedManager) run(ctx context.Context) {
|
|||||||
if rManager == nil {
|
if rManager == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
// RevertLink resets all per-interface settings on systemd-resolved to defaults.
|
// RevertLink resets all per-interface settings on systemd-resolved to defaults.
|
||||||
// When ctx goes away systemd-resolved auto reverts.
|
// When ctx goes away systemd-resolved auto reverts.
|
||||||
// Keeping for potential use in future refactor.
|
// Keeping for potential use in future refactor.
|
||||||
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".RevertLink", 0, m.ifidx); call.Err != nil {
|
if call := rManager.CallWithContext(ctx, dbusRevertLink, 0, m.ifidx); call.Err != nil {
|
||||||
m.logf("[v1] RevertLink: %v", call.Err)
|
m.logf("[v1] RevertLink: %v", call.Err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
cancel()
|
||||||
|
close(m.revertCh)
|
||||||
return
|
return
|
||||||
case configCR := <-m.configCR:
|
case configCR := <-m.configCR:
|
||||||
// Track and update sync with latest config change.
|
// Track and update sync with latest config change.
|
||||||
@@ -223,7 +289,7 @@ func (m *resolvedManager) run(ctx context.Context) {
|
|||||||
if len(signal.Body) != 3 {
|
if len(signal.Body) != 3 {
|
||||||
m.logf("[unexpected] DBus NameOwnerChanged len(Body) = %d, want 3")
|
m.logf("[unexpected] DBus NameOwnerChanged len(Body) = %d, want 3")
|
||||||
}
|
}
|
||||||
if name, ok := signal.Body[0].(string); !ok || name != dbusResolvedObject {
|
if name, ok := signal.Body[0].(string); !ok || (name != dbusResolvedObject && name != dbusNetworkdObject) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
newOwner, ok := signal.Body[2].(string)
|
newOwner, ok := signal.Body[2].(string)
|
||||||
@@ -271,7 +337,7 @@ func (m *resolvedManager) setConfigOverDBus(ctx context.Context, rManager dbus.B
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
err := rManager.CallWithContext(
|
err := rManager.CallWithContext(
|
||||||
ctx, dbusResolvedInterface+".SetLinkDNS", 0,
|
ctx, dbusSetLinkDNS, 0,
|
||||||
m.ifidx, linkNameservers,
|
m.ifidx, linkNameservers,
|
||||||
).Store()
|
).Store()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -311,14 +377,14 @@ func (m *resolvedManager) setConfigOverDBus(ctx context.Context, rManager dbus.B
|
|||||||
}
|
}
|
||||||
|
|
||||||
err = rManager.CallWithContext(
|
err = rManager.CallWithContext(
|
||||||
ctx, dbusResolvedInterface+".SetLinkDomains", 0,
|
ctx, dbusSetLinkDomains, 0,
|
||||||
m.ifidx, linkDomains,
|
m.ifidx, linkDomains,
|
||||||
).Store()
|
).Store()
|
||||||
if err != nil && err.Error() == "Argument list too long" { // TODO: better error match
|
if err != nil && err.Error() == "Argument list too long" { // TODO: better error match
|
||||||
// Issue 3188: older systemd-resolved had argument length limits.
|
// Issue 3188: older systemd-resolved had argument length limits.
|
||||||
// Trim out the *.arpa. entries and try again.
|
// Trim out the *.arpa. entries and try again.
|
||||||
err = rManager.CallWithContext(
|
err = rManager.CallWithContext(
|
||||||
ctx, dbusResolvedInterface+".SetLinkDomains", 0,
|
ctx, dbusSetLinkDomains, 0,
|
||||||
m.ifidx, linkDomainsWithoutReverseDNS(linkDomains),
|
m.ifidx, linkDomainsWithoutReverseDNS(linkDomains),
|
||||||
).Store()
|
).Store()
|
||||||
}
|
}
|
||||||
@@ -326,7 +392,7 @@ func (m *resolvedManager) setConfigOverDBus(ctx context.Context, rManager dbus.B
|
|||||||
return fmt.Errorf("setLinkDomains: %w", err)
|
return fmt.Errorf("setLinkDomains: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".SetLinkDefaultRoute", 0, m.ifidx, len(config.MatchDomains) == 0); call.Err != nil {
|
if call := rManager.CallWithContext(ctx, dbusSetLinkDefaultRoute, 0, m.ifidx, len(config.MatchDomains) == 0); call.Err != nil {
|
||||||
if dbusErr, ok := call.Err.(dbus.Error); ok && dbusErr.Name == dbus.ErrMsgUnknownMethod.Name {
|
if dbusErr, ok := call.Err.(dbus.Error); ok && dbusErr.Name == dbus.ErrMsgUnknownMethod.Name {
|
||||||
// on some older systems like Kubuntu 18.04.6 with systemd 237 method SetLinkDefaultRoute is absent,
|
// on some older systems like Kubuntu 18.04.6 with systemd 237 method SetLinkDefaultRoute is absent,
|
||||||
// but otherwise it's working good
|
// but otherwise it's working good
|
||||||
@@ -341,33 +407,37 @@ func (m *resolvedManager) setConfigOverDBus(ctx context.Context, rManager dbus.B
|
|||||||
// or something).
|
// or something).
|
||||||
|
|
||||||
// Disable LLMNR, we don't do multicast.
|
// Disable LLMNR, we don't do multicast.
|
||||||
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".SetLinkLLMNR", 0, m.ifidx, "no"); call.Err != nil {
|
if call := rManager.CallWithContext(ctx, dbusSetLinkLLMNR, 0, m.ifidx, "no"); call.Err != nil {
|
||||||
m.logf("[v1] failed to disable LLMNR: %v", call.Err)
|
m.logf("[v1] failed to disable LLMNR: %v", call.Err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable mdns.
|
// Disable mdns.
|
||||||
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".SetLinkMulticastDNS", 0, m.ifidx, "no"); call.Err != nil {
|
if call := rManager.CallWithContext(ctx, dbusSetLinkMulticastDNS, 0, m.ifidx, "no"); call.Err != nil {
|
||||||
m.logf("[v1] failed to disable mdns: %v", call.Err)
|
m.logf("[v1] failed to disable mdns: %v", call.Err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't support dnssec consistently right now, force it off to
|
// We don't support dnssec consistently right now, force it off to
|
||||||
// avoid partial failures when we split DNS internally.
|
// avoid partial failures when we split DNS internally.
|
||||||
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".SetLinkDNSSEC", 0, m.ifidx, "no"); call.Err != nil {
|
if call := rManager.CallWithContext(ctx, dbusSetLinkDNSSEC, 0, m.ifidx, "no"); call.Err != nil {
|
||||||
m.logf("[v1] failed to disable DNSSEC: %v", call.Err)
|
m.logf("[v1] failed to disable DNSSEC: %v", call.Err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".SetLinkDNSOverTLS", 0, m.ifidx, "no"); call.Err != nil {
|
if call := rManager.CallWithContext(ctx, dbusSetLinkDNSOverTLS, 0, m.ifidx, "no"); call.Err != nil {
|
||||||
m.logf("[v1] failed to disable DoT: %v", call.Err)
|
m.logf("[v1] failed to disable DoT: %v", call.Err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".FlushCaches", 0); call.Err != nil {
|
if rManager.Path() == dbusResolvedPath {
|
||||||
m.logf("failed to flush resolved DNS cache: %v", call.Err)
|
if call := rManager.CallWithContext(ctx, dbusFlushCaches, 0); call.Err != nil {
|
||||||
|
m.logf("failed to flush resolved DNS cache: %v", call.Err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *resolvedManager) Close() error {
|
func (m *resolvedManager) Close() error {
|
||||||
m.cancel() // stops the 'run' method goroutine
|
m.cancel() // stops the 'run' method goroutine
|
||||||
|
<-m.revertCh
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
package net
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"tailscale.com/logtail/backoff"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
controldIPv6Test = "ipv6.controld.io"
|
||||||
|
v4BootstrapDNS = "76.76.2.0:53"
|
||||||
|
v6BootstrapDNS = "[2606:1a40::]:53"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Dialer = &net.Dialer{
|
||||||
|
Resolver: &net.Resolver{
|
||||||
|
PreferGo: true,
|
||||||
|
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||||
|
d := ParallelDialer{}
|
||||||
|
d.Timeout = 10 * time.Second
|
||||||
|
return d.DialContext(ctx, "udp", []string{v4BootstrapDNS, v6BootstrapDNS})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const probeStackTimeout = 2 * time.Second
|
||||||
|
|
||||||
|
var probeStackDialer = &net.Dialer{
|
||||||
|
Resolver: Dialer.Resolver,
|
||||||
|
Timeout: probeStackTimeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
stackOnce atomic.Pointer[sync.Once]
|
||||||
|
canListenIPv6Local bool
|
||||||
|
hasNetworkUp bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
stackOnce.Store(new(sync.Once))
|
||||||
|
}
|
||||||
|
|
||||||
|
func supportIPv6(ctx context.Context) bool {
|
||||||
|
_, err := probeStackDialer.DialContext(ctx, "tcp6", net.JoinHostPort(controldIPv6Test, "443"))
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func supportListenIPv6Local() bool {
|
||||||
|
if ln, err := net.Listen("tcp6", "[::1]:0"); err == nil {
|
||||||
|
ln.Close()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func probeStack() {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
sigs := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-sigs
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
|
b := backoff.NewBackoff("probeStack", func(format string, args ...any) {}, 5*time.Second)
|
||||||
|
for {
|
||||||
|
if _, err := probeStackDialer.DialContext(ctx, "udp", v4BootstrapDNS); err == nil {
|
||||||
|
hasNetworkUp = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if _, err := probeStackDialer.DialContext(ctx, "udp", v6BootstrapDNS); err == nil {
|
||||||
|
hasNetworkUp = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
b.BackOff(context.Background(), errors.New("network is down"))
|
||||||
|
}
|
||||||
|
canListenIPv6Local = supportListenIPv6Local()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Up() bool {
|
||||||
|
stackOnce.Load().Do(probeStack)
|
||||||
|
return hasNetworkUp
|
||||||
|
}
|
||||||
|
|
||||||
|
func SupportsIPv6ListenLocal() bool {
|
||||||
|
stackOnce.Load().Do(probeStack)
|
||||||
|
return canListenIPv6Local
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPv6Available is like SupportsIPv6, but always do the check without caching.
|
||||||
|
func IPv6Available(ctx context.Context) bool {
|
||||||
|
return supportIPv6(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsIPv6 checks if the provided IP is v6.
|
||||||
|
//
|
||||||
|
//lint:ignore U1000 use in os_windows.go
|
||||||
|
func IsIPv6(ip string) bool {
|
||||||
|
parsedIP := net.ParseIP(ip)
|
||||||
|
return parsedIP != nil && parsedIP.To4() == nil && parsedIP.To16() != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type parallelDialerResult struct {
|
||||||
|
conn net.Conn
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type ParallelDialer struct {
|
||||||
|
net.Dialer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ParallelDialer) DialContext(ctx context.Context, network string, addrs []string) (net.Conn, error) {
|
||||||
|
if len(addrs) == 0 {
|
||||||
|
return nil, errors.New("empty addresses")
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
defer close(done)
|
||||||
|
ch := make(chan *parallelDialerResult, len(addrs))
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(len(addrs))
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(ch)
|
||||||
|
}()
|
||||||
|
|
||||||
|
for _, addr := range addrs {
|
||||||
|
go func(addr string) {
|
||||||
|
defer wg.Done()
|
||||||
|
conn, err := d.Dialer.DialContext(ctx, network, addr)
|
||||||
|
select {
|
||||||
|
case ch <- ¶llelDialerResult{conn: conn, err: err}:
|
||||||
|
case <-done:
|
||||||
|
if conn != nil {
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
errs := make([]error, 0, len(addrs))
|
||||||
|
for res := range ch {
|
||||||
|
if res.err == nil {
|
||||||
|
cancel()
|
||||||
|
return res.conn, res.err
|
||||||
|
}
|
||||||
|
errs = append(errs, res.err)
|
||||||
|
}
|
||||||
|
return nil, errors.Join(errs...)
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package net
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestProbeStackTimeout(t *testing.T) {
|
||||||
|
done := make(chan struct{})
|
||||||
|
started := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(done)
|
||||||
|
close(started)
|
||||||
|
supportIPv6(context.Background())
|
||||||
|
}()
|
||||||
|
|
||||||
|
<-started
|
||||||
|
select {
|
||||||
|
case <-time.After(probeStackTimeout + time.Second):
|
||||||
|
t.Error("probeStack timeout is not enforce")
|
||||||
|
case <-done:
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package ddwrt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/kardianos/service"
|
||||||
|
|
||||||
|
"github.com/Control-D-Inc/ctrld"
|
||||||
|
"github.com/Control-D-Inc/ctrld/internal/router/dnsmasq"
|
||||||
|
"github.com/Control-D-Inc/ctrld/internal/router/ntp"
|
||||||
|
"github.com/Control-D-Inc/ctrld/internal/router/nvram"
|
||||||
|
)
|
||||||
|
|
||||||
|
const Name = "ddwrt"
|
||||||
|
|
||||||
|
//lint:ignore ST1005 This error is for human.
|
||||||
|
var errDdwrtJffs2NotEnabled = errors.New(`could not install service without jffs, follow this guide to enable:
|
||||||
|
|
||||||
|
https://wiki.dd-wrt.com/wiki/index.php/Journalling_Flash_File_System
|
||||||
|
`)
|
||||||
|
|
||||||
|
var nvramKvMap = map[string]string{
|
||||||
|
"dns_dnsmasq": "1", // Make dnsmasq running but disable DNS ability, ctrld will replace it.
|
||||||
|
"dnsmasq_options": "", // Configuration of dnsmasq set by ctrld, filled by setupDDWrt.
|
||||||
|
"dns_crypt": "0", // Disable DNSCrypt.
|
||||||
|
"dnssec": "0", // Disable DNSSEC.
|
||||||
|
}
|
||||||
|
|
||||||
|
type Ddwrt struct {
|
||||||
|
cfg *ctrld.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a router.Router for configuring/setup/run ctrld on ddwrt routers.
|
||||||
|
func New(cfg *ctrld.Config) *Ddwrt {
|
||||||
|
return &Ddwrt{cfg: cfg}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Ddwrt) ConfigureService(config *service.Config) error {
|
||||||
|
if !ddwrtJff2Enabled() {
|
||||||
|
return errDdwrtJffs2NotEnabled
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Ddwrt) Install(_ *service.Config) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Ddwrt) Uninstall(_ *service.Config) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Ddwrt) PreRun() error {
|
||||||
|
_ = d.Cleanup()
|
||||||
|
return ntp.WaitNvram()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Ddwrt) Setup() error {
|
||||||
|
if d.cfg.FirstListener().IsDirectDnsListener() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Already setup.
|
||||||
|
if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := dnsmasq.ConfTmpl(dnsmasq.ConfigContentTmpl, d.cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
nvramKvMap["dnsmasq_options"] = data
|
||||||
|
if err := nvram.SetKV(nvramKvMap, nvram.CtrldSetupKey); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart dnsmasq service.
|
||||||
|
if err := restartDNSMasq(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Ddwrt) Cleanup() error {
|
||||||
|
if d.cfg.FirstListener().IsDirectDnsListener() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val != "1" {
|
||||||
|
return nil // was restored, nothing to do.
|
||||||
|
}
|
||||||
|
|
||||||
|
nvramKvMap["dnsmasq_options"] = ""
|
||||||
|
// Restore old configs.
|
||||||
|
if err := nvram.Restore(nvramKvMap, nvram.CtrldSetupKey); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart dnsmasq service.
|
||||||
|
if err := restartDNSMasq(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func restartDNSMasq() error {
|
||||||
|
if out, err := exec.Command("restart_dns").CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("restart_dns: %s, %w", string(out), err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ddwrtJff2Enabled() bool {
|
||||||
|
out, _ := nvram.Run("get", "enable_jffs2")
|
||||||
|
return out == "1"
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package dnsmasq
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func InterfaceNameFromConfig(filename string) (string, error) {
|
||||||
|
buf, err := os.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return interfaceNameFromReader(bytes.NewReader(buf))
|
||||||
|
}
|
||||||
|
|
||||||
|
func interfaceNameFromReader(r io.Reader) (string, error) {
|
||||||
|
scanner := bufio.NewScanner(r)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
after, found := strings.CutPrefix(line, "interface=")
|
||||||
|
if found {
|
||||||
|
return after, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", errors.New("not found")
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package dnsmasq
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_interfaceNameFromReader(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
in string
|
||||||
|
wantIface string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"good",
|
||||||
|
`interface=lo`,
|
||||||
|
"lo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"multiple",
|
||||||
|
`interface=lo
|
||||||
|
interface=eth0
|
||||||
|
`,
|
||||||
|
"lo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no iface",
|
||||||
|
`cache-size=100`,
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ifaceName, err := interfaceNameFromReader(strings.NewReader(tc.in))
|
||||||
|
if tc.wantIface != "" && err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tc.wantIface != ifaceName {
|
||||||
|
t.Errorf("mismatched, want: %q, got: %q", tc.wantIface, ifaceName)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user