mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-07-16 13:17:19 +02:00
Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
+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:
|
||||||
|
|||||||
@@ -9,11 +9,15 @@ builds:
|
|||||||
- -trimpath
|
- -trimpath
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w
|
- -s -w
|
||||||
|
- -X main.version={{.Version}}
|
||||||
|
- -X main.commit={{.Commit}}
|
||||||
goos:
|
goos:
|
||||||
- darwin
|
- darwin
|
||||||
- linux
|
- linux
|
||||||
- windows
|
- windows
|
||||||
goarch:
|
goarch:
|
||||||
|
- 386
|
||||||
|
- arm
|
||||||
- amd64
|
- amd64
|
||||||
- arm64
|
- arm64
|
||||||
tags:
|
tags:
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ builds:
|
|||||||
- -trimpath
|
- -trimpath
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w
|
- -s -w
|
||||||
|
- -X main.version={{.Version}}
|
||||||
|
- -X main.commit={{.Commit}}
|
||||||
goos:
|
goos:
|
||||||
- linux
|
- linux
|
||||||
- freebsd
|
- freebsd
|
||||||
@@ -17,6 +19,7 @@ builds:
|
|||||||
- 386
|
- 386
|
||||||
- arm
|
- arm
|
||||||
- mips
|
- mips
|
||||||
|
- mipsle
|
||||||
- amd64
|
- amd64
|
||||||
- arm64
|
- arm64
|
||||||
goarm:
|
goarm:
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ A highly configurable DNS forwarding proxy with support for:
|
|||||||
- 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
|
||||||
|
|
||||||
|
## 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`
|
||||||
- `DNS-over-HTTPS`
|
- `DNS-over-HTTPS`
|
||||||
@@ -17,7 +20,7 @@ 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.
|
||||||
@@ -28,12 +31,28 @@ All DNS protocols are supported, including:
|
|||||||
- Windows (386, amd64, arm)
|
- Windows (386, amd64, arm)
|
||||||
- Mac (amd64, arm64)
|
- Mac (amd64, arm64)
|
||||||
- Linux (386, amd64, arm, mips)
|
- Linux (386, amd64, arm, mips)
|
||||||
|
- 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
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
@@ -45,6 +64,10 @@ or
|
|||||||
$ go install github.com/Control-D-Inc/ctrld/cmd/ctrld@latest
|
$ go install github.com/Control-D-Inc/ctrld/cmd/ctrld@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
The cli is self documenting, so free free to run `--help` on any sub-command to get specific usages.
|
||||||
|
|
||||||
## Arguments
|
## Arguments
|
||||||
```
|
```
|
||||||
__ .__ .___
|
__ .__ .___
|
||||||
@@ -60,18 +83,29 @@ 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
|
||||||
|
setup Auto-setup Control D on a router.
|
||||||
|
|
||||||
|
Supported platforms:
|
||||||
|
|
||||||
|
ₒ ddwrt
|
||||||
|
ₒ merlin
|
||||||
|
ₒ openwrt
|
||||||
|
ₒ ubios
|
||||||
|
ₒ auto - detect the platform you are running on
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
```
|
```
|
||||||
@@ -85,12 +119,12 @@ 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 or Linux distibution, 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/` (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`.
|
In order to stop the service, and restore your DNS to original state, simply run `./ctrld stop`. If you wish to uninstall the service permanently, run `./ctrld service uninstall`.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
@@ -117,8 +151,20 @@ For granular control of the service, run the `service` command. Each sub-command
|
|||||||
Use "ctrld service [command] --help" for more information about a command.
|
Use "ctrld service [command] --help" for more information about a command.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Router Mode
|
||||||
|
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:
|
||||||
|
- OpenWRT
|
||||||
|
- DD-WRT
|
||||||
|
- Asus Merlin
|
||||||
|
- GL.iNet
|
||||||
|
- Ubiquiti
|
||||||
|
|
||||||
|
In order to start `ctrld` as a DNS provider, simply run `./ctrld setup auto` command.
|
||||||
|
|
||||||
|
In this mode, and when Control D upstreams are used, the router will [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.
|
||||||
|
|
||||||
### 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) or `setup` (router) 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.
|
||||||
|
|
||||||
@@ -126,22 +172,27 @@ 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:
|
You can do the same while starting in router mode:
|
||||||
|
```shell
|
||||||
|
./ctrld setup auto --cd abcd1234
|
||||||
|
```
|
||||||
|
|
||||||
|
Once you run the above commands (in service or router modes 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 `service 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
|
||||||
@@ -183,17 +234,16 @@ 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
|
||||||
|
- Support for more routers (let us know which ones)
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
+357
-146
@@ -3,10 +3,10 @@ package main
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/x509"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
@@ -15,10 +15,11 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/cuonglm/osinfo"
|
||||||
"github.com/fsnotify/fsnotify"
|
"github.com/fsnotify/fsnotify"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/kardianos/service"
|
"github.com/kardianos/service"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
@@ -29,16 +30,24 @@ import (
|
|||||||
"tailscale.com/net/interfaces"
|
"tailscale.com/net/interfaces"
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld"
|
"github.com/Control-D-Inc/ctrld"
|
||||||
|
"github.com/Control-D-Inc/ctrld/internal/certs"
|
||||||
"github.com/Control-D-Inc/ctrld/internal/controld"
|
"github.com/Control-D-Inc/ctrld/internal/controld"
|
||||||
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
||||||
|
"github.com/Control-D-Inc/ctrld/internal/router"
|
||||||
)
|
)
|
||||||
|
|
||||||
const selfCheckFQDN = "verify.controld.com"
|
const selfCheckFQDN = "verify.controld.com"
|
||||||
|
|
||||||
|
var (
|
||||||
|
version = "dev"
|
||||||
|
commit = "none"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
v = viper.NewWithOptions(viper.KeyDelimiter("::"))
|
v = viper.NewWithOptions(viper.KeyDelimiter("::"))
|
||||||
defaultConfigWritten = false
|
defaultConfigWritten = false
|
||||||
defaultConfigFile = "ctrld.toml"
|
defaultConfigFile = "ctrld.toml"
|
||||||
|
rootCertPool *x509.CertPool
|
||||||
)
|
)
|
||||||
|
|
||||||
var basicModeFlags = []string{"listen", "primary_upstream", "secondary_upstream", "domains"}
|
var basicModeFlags = []string{"listen", "primary_upstream", "secondary_upstream", "domains"}
|
||||||
@@ -61,23 +70,44 @@ _/ ___\ __\_ __ \ | / __ |
|
|||||||
\/ dns forwarding proxy \/
|
\/ dns forwarding proxy \/
|
||||||
`
|
`
|
||||||
|
|
||||||
|
var rootCmd = &cobra.Command{
|
||||||
|
Use: "ctrld",
|
||||||
|
Short: strings.TrimLeft(rootShortDesc, "\n"),
|
||||||
|
Version: curVersion(),
|
||||||
|
PreRun: func(cmd *cobra.Command, args []string) {
|
||||||
|
initConsoleLogging()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func curVersion() string {
|
||||||
|
if version != "dev" && !strings.HasPrefix(version, "v") {
|
||||||
|
version = "v" + version
|
||||||
|
}
|
||||||
|
if len(commit) > 7 {
|
||||||
|
commit = commit[:7]
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s-%s", version, commit)
|
||||||
|
}
|
||||||
|
|
||||||
func initCLI() {
|
func initCLI() {
|
||||||
// Enable opening via explorer.exe on Windows.
|
// Enable opening via explorer.exe on Windows.
|
||||||
// See: https://github.com/spf13/cobra/issues/844.
|
// See: https://github.com/spf13/cobra/issues/844.
|
||||||
cobra.MousetrapHelpText = ""
|
cobra.MousetrapHelpText = ""
|
||||||
cobra.EnableCommandSorting = false
|
cobra.EnableCommandSorting = false
|
||||||
|
|
||||||
rootCmd := &cobra.Command{
|
|
||||||
Use: "ctrld",
|
|
||||||
Short: strings.TrimLeft(rootShortDesc, "\n"),
|
|
||||||
Version: "1.1.2",
|
|
||||||
}
|
|
||||||
rootCmd.PersistentFlags().CountVarP(
|
rootCmd.PersistentFlags().CountVarP(
|
||||||
&verbose,
|
&verbose,
|
||||||
"verbose",
|
"verbose",
|
||||||
"v",
|
"v",
|
||||||
`verbose log output, "-v" basic logging, "-vv" debug level logging`,
|
`verbose log output, "-v" basic logging, "-vv" debug level logging`,
|
||||||
)
|
)
|
||||||
|
rootCmd.PersistentFlags().BoolVarP(
|
||||||
|
&silent,
|
||||||
|
"silent",
|
||||||
|
"s",
|
||||||
|
false,
|
||||||
|
`do not write any log output`,
|
||||||
|
)
|
||||||
rootCmd.SetHelpCommand(&cobra.Command{Hidden: true})
|
rootCmd.SetHelpCommand(&cobra.Command{Hidden: true})
|
||||||
rootCmd.CompletionOptions.HiddenDefaultCmd = true
|
rootCmd.CompletionOptions.HiddenDefaultCmd = true
|
||||||
|
|
||||||
@@ -85,45 +115,75 @@ func initCLI() {
|
|||||||
Use: "run",
|
Use: "run",
|
||||||
Short: "Run the DNS proxy server",
|
Short: "Run the DNS proxy server",
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
|
PreRun: func(cmd *cobra.Command, args []string) {
|
||||||
|
initConsoleLogging()
|
||||||
|
},
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
if daemon && runtime.GOOS == "windows" {
|
if daemon && runtime.GOOS == "windows" {
|
||||||
log.Fatal("Cannot run in daemon mode. Please install a Windows service.")
|
mainLog.Fatal().Msg("Cannot run in daemon mode. Please install a Windows service.")
|
||||||
|
}
|
||||||
|
|
||||||
|
waitCh := make(chan struct{})
|
||||||
|
stopCh := make(chan struct{})
|
||||||
|
if !daemon {
|
||||||
|
// We need to call s.Run() as soon as possible to response to the OS manager, so it
|
||||||
|
// can see ctrld is running and don't mark ctrld as failed service.
|
||||||
|
go func() {
|
||||||
|
p := &prog{
|
||||||
|
waitCh: waitCh,
|
||||||
|
stopCh: stopCh,
|
||||||
|
}
|
||||||
|
s, err := service.New(p, svcConfig)
|
||||||
|
if err != nil {
|
||||||
|
mainLog.Fatal().Err(err).Msg("failed create new service")
|
||||||
|
}
|
||||||
|
s = newService(s)
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
noConfigStart := isNoConfigStart(cmd)
|
noConfigStart := isNoConfigStart(cmd)
|
||||||
writeDefaultConfig := !noConfigStart && configBase64 == ""
|
writeDefaultConfig := !noConfigStart && configBase64 == ""
|
||||||
configs := []struct {
|
tryReadingConfig(writeDefaultConfig)
|
||||||
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()
|
readBase64Config(configBase64)
|
||||||
processNoConfigFlags(noConfigStart)
|
processNoConfigFlags(noConfigStart)
|
||||||
if err := v.Unmarshal(&cfg); err != nil {
|
if err := v.Unmarshal(&cfg); err != nil {
|
||||||
log.Fatalf("failed to unmarshal config: %v", err)
|
mainLog.Fatal().Msgf("failed to unmarshal config: %v", err)
|
||||||
}
|
}
|
||||||
fmt.Println("starting ctrld...")
|
|
||||||
|
mainLog.Info().Msgf("starting ctrld %s", curVersion())
|
||||||
|
oi := osinfo.New()
|
||||||
|
mainLog.Info().Msgf("os: %s", oi.String())
|
||||||
|
|
||||||
// Wait for network up.
|
// Wait for network up.
|
||||||
if !ctrldnet.Up() {
|
if !ctrldnet.Up() {
|
||||||
log.Fatal("network is not up yet")
|
mainLog.Fatal().Msg("network is not up yet")
|
||||||
}
|
}
|
||||||
processLogAndCacheFlags()
|
processLogAndCacheFlags()
|
||||||
// Log config do not have thing to validate, so it's safe to init log here,
|
// 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.
|
// so it's able to log information in processCDFlags.
|
||||||
initLogging()
|
initLogging()
|
||||||
|
|
||||||
|
if setupRouter {
|
||||||
|
if err := router.PreStart(); err != nil {
|
||||||
|
mainLog.Fatal().Err(err).Msg("failed to perform router pre-start check")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
processCDFlags()
|
processCDFlags()
|
||||||
if err := ctrld.ValidateConfig(validator.New(), &cfg); err != nil {
|
if err := ctrld.ValidateConfig(validator.New(), &cfg); err != nil {
|
||||||
log.Fatalf("invalid config: %v", err)
|
mainLog.Fatal().Msgf("invalid config: %v", err)
|
||||||
}
|
}
|
||||||
initCache()
|
initCache()
|
||||||
|
|
||||||
@@ -149,22 +209,26 @@ func initCLI() {
|
|||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
s, err := service.New(&prog{}, svcConfig)
|
if setupRouter {
|
||||||
if err != nil {
|
switch platform := router.Name(); {
|
||||||
mainLog.Fatal().Err(err).Msg("failed create new service")
|
case platform == router.DDWrt:
|
||||||
}
|
rootCertPool = certs.CACertPool()
|
||||||
serviceLogger, err := s.Logger(nil)
|
fallthrough
|
||||||
if err != nil {
|
case platform != "":
|
||||||
mainLog.Error().Err(err).Msg("failed to get service logger")
|
mainLog.Debug().Msg("Router setup")
|
||||||
return
|
err := router.Configure(&cfg)
|
||||||
|
if errors.Is(err, router.ErrNotSupported) {
|
||||||
|
unsupportedPlatformHelp(cmd)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
mainLog.Fatal().Err(err).Msg("failed to configure router")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.Run(); err != nil {
|
close(waitCh)
|
||||||
if sErr := serviceLogger.Error(err); sErr != nil {
|
<-stopCh
|
||||||
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().BoolVarP(&daemon, "daemon", "d", false, "Run as daemon")
|
||||||
@@ -181,14 +245,19 @@ func initCLI() {
|
|||||||
_ = runCmd.Flags().MarkHidden("homedir")
|
_ = runCmd.Flags().MarkHidden("homedir")
|
||||||
runCmd.Flags().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`)
|
runCmd.Flags().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`)
|
||||||
_ = runCmd.Flags().MarkHidden("iface")
|
_ = runCmd.Flags().MarkHidden("iface")
|
||||||
|
runCmd.Flags().BoolVarP(&setupRouter, "router", "", false, `setup for running on router platforms`)
|
||||||
|
_ = runCmd.Flags().MarkHidden("router")
|
||||||
|
|
||||||
rootCmd.AddCommand(runCmd)
|
rootCmd.AddCommand(runCmd)
|
||||||
|
|
||||||
startCmd := &cobra.Command{
|
startCmd := &cobra.Command{
|
||||||
PreRun: checkHasElevatedPrivilege,
|
PreRun: func(cmd *cobra.Command, args []string) {
|
||||||
Use: "start",
|
initConsoleLogging()
|
||||||
Short: "Install and start the ctrld service",
|
checkHasElevatedPrivilege()
|
||||||
Args: cobra.NoArgs,
|
},
|
||||||
|
Use: "start",
|
||||||
|
Short: "Install and start the ctrld service",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
sc := &service.Config{}
|
sc := &service.Config{}
|
||||||
*sc = *svcConfig
|
*sc = *svcConfig
|
||||||
@@ -198,6 +267,9 @@ func initCLI() {
|
|||||||
}
|
}
|
||||||
setDependencies(sc)
|
setDependencies(sc)
|
||||||
sc.Arguments = append([]string{"run"}, osArgs...)
|
sc.Arguments = append([]string{"run"}, osArgs...)
|
||||||
|
if err := router.ConfigureService(sc); err != nil {
|
||||||
|
mainLog.Fatal().Err(err).Msg("failed to configure service on router")
|
||||||
|
}
|
||||||
|
|
||||||
// No config path, generating config in HOME directory.
|
// No config path, generating config in HOME directory.
|
||||||
noConfigStart := isNoConfigStart(cmd)
|
noConfigStart := isNoConfigStart(cmd)
|
||||||
@@ -205,18 +277,18 @@ func initCLI() {
|
|||||||
if configPath != "" {
|
if configPath != "" {
|
||||||
v.SetConfigFile(configPath)
|
v.SetConfigFile(configPath)
|
||||||
}
|
}
|
||||||
if dir, err := os.UserHomeDir(); err == nil {
|
if dir, err := userHomeDir(); err == nil {
|
||||||
setWorkingDirectory(sc, dir)
|
setWorkingDirectory(sc, dir)
|
||||||
if configPath == "" && writeDefaultConfig {
|
if configPath == "" && writeDefaultConfig {
|
||||||
defaultConfigFile = filepath.Join(dir, defaultConfigFile)
|
defaultConfigFile = filepath.Join(dir, defaultConfigFile)
|
||||||
v.SetConfigFile(defaultConfigFile)
|
|
||||||
}
|
}
|
||||||
sc.Arguments = append(sc.Arguments, "--homedir="+dir)
|
sc.Arguments = append(sc.Arguments, "--homedir="+dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
readConfigFile(writeDefaultConfig && cdUID == "")
|
tryReadingConfig(writeDefaultConfig)
|
||||||
|
|
||||||
if err := v.Unmarshal(&cfg); err != nil {
|
if err := v.Unmarshal(&cfg); err != nil {
|
||||||
log.Fatalf("failed to unmarshal config: %v", err)
|
mainLog.Fatal().Msgf("failed to unmarshal config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logPath := cfg.Service.LogPath
|
logPath := cfg.Service.LogPath
|
||||||
@@ -225,20 +297,21 @@ func initCLI() {
|
|||||||
cfg.Service.LogPath = logPath
|
cfg.Service.LogPath = logPath
|
||||||
|
|
||||||
processCDFlags()
|
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.
|
// Explicitly passing config, so on system where home directory could not be obtained,
|
||||||
if runtime.GOOS == "windows" {
|
// or sub-process env is different with the parent, we still behave correctly and use
|
||||||
if configPath == "" {
|
// the expected config file.
|
||||||
sc.Arguments = append(sc.Arguments, "--config="+defaultConfigFile)
|
if configPath == "" {
|
||||||
}
|
sc.Arguments = append(sc.Arguments, "--config="+defaultConfigFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
prog := &prog{}
|
prog := &prog{}
|
||||||
s, err := service.New(prog, sc)
|
s, err := service.New(prog, sc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
stderrMsg(err.Error())
|
mainLog.Error().Msg(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
s = newService(s)
|
||||||
tasks := []task{
|
tasks := []task{
|
||||||
{s.Stop, false},
|
{s.Stop, false},
|
||||||
{s.Uninstall, false},
|
{s.Uninstall, false},
|
||||||
@@ -246,7 +319,11 @@ func initCLI() {
|
|||||||
{s.Start, true},
|
{s.Start, true},
|
||||||
}
|
}
|
||||||
if doTasks(tasks) {
|
if doTasks(tasks) {
|
||||||
status, err := s.Status()
|
if err := router.PostInstall(); err != nil {
|
||||||
|
mainLog.Warn().Err(err).Msg("post installation failed, please check system/service log for details error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
status, err := serviceStatus(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mainLog.Warn().Err(err).Msg("could not get service status")
|
mainLog.Warn().Err(err).Msg("could not get service status")
|
||||||
return
|
return
|
||||||
@@ -255,7 +332,7 @@ func initCLI() {
|
|||||||
status = selfCheckStatus(status)
|
status = selfCheckStatus(status)
|
||||||
switch status {
|
switch status {
|
||||||
case service.StatusRunning:
|
case service.StatusRunning:
|
||||||
mainLog.Info().Msg("Service started")
|
mainLog.Notice().Msg("Service started")
|
||||||
default:
|
default:
|
||||||
mainLog.Error().Msg("Service did not start, please check system/service log for details error")
|
mainLog.Error().Msg("Service did not start, please check system/service log for details error")
|
||||||
if runtime.GOOS == "linux" {
|
if runtime.GOOS == "linux" {
|
||||||
@@ -278,42 +355,52 @@ func initCLI() {
|
|||||||
startCmd.Flags().IntVarP(&cacheSize, "cache_size", "", 0, "Enable cache with size items")
|
startCmd.Flags().IntVarP(&cacheSize, "cache_size", "", 0, "Enable cache with size items")
|
||||||
startCmd.Flags().StringVarP(&cdUID, "cd", "", "", "Control D resolver uid")
|
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`)
|
startCmd.Flags().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`)
|
||||||
|
startCmd.Flags().BoolVarP(&setupRouter, "router", "", false, `setup for running on router platforms`)
|
||||||
|
_ = startCmd.Flags().MarkHidden("router")
|
||||||
|
|
||||||
stopCmd := &cobra.Command{
|
stopCmd := &cobra.Command{
|
||||||
PreRun: checkHasElevatedPrivilege,
|
PreRun: func(cmd *cobra.Command, args []string) {
|
||||||
Use: "stop",
|
initConsoleLogging()
|
||||||
Short: "Stop the ctrld service",
|
checkHasElevatedPrivilege()
|
||||||
Args: cobra.NoArgs,
|
},
|
||||||
|
Use: "stop",
|
||||||
|
Short: "Stop the ctrld service",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
prog := &prog{}
|
prog := &prog{}
|
||||||
s, err := service.New(prog, svcConfig)
|
s, err := service.New(prog, svcConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
stderrMsg(err.Error())
|
mainLog.Error().Msg(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
s = newService(s)
|
||||||
initLogging()
|
initLogging()
|
||||||
if doTasks([]task{{s.Stop, true}}) {
|
if doTasks([]task{{s.Stop, true}}) {
|
||||||
prog.resetDNS()
|
prog.resetDNS()
|
||||||
mainLog.Info().Msg("Service stopped")
|
mainLog.Notice().Msg("Service stopped")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
stopCmd.Flags().StringVarP(&iface, "iface", "", "", `Reset DNS setting for iface, "auto" means the default interface gateway`)
|
stopCmd.Flags().StringVarP(&iface, "iface", "", "", `Reset DNS setting for iface, "auto" means the default interface gateway`)
|
||||||
|
|
||||||
restartCmd := &cobra.Command{
|
restartCmd := &cobra.Command{
|
||||||
PreRun: checkHasElevatedPrivilege,
|
PreRun: func(cmd *cobra.Command, args []string) {
|
||||||
Use: "restart",
|
initConsoleLogging()
|
||||||
Short: "Restart the ctrld service",
|
checkHasElevatedPrivilege()
|
||||||
Args: cobra.NoArgs,
|
},
|
||||||
|
Use: "restart",
|
||||||
|
Short: "Restart the ctrld service",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
s, err := service.New(&prog{}, svcConfig)
|
s, err := service.New(&prog{}, svcConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
stderrMsg(err.Error())
|
mainLog.Error().Msg(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
s = newService(s)
|
||||||
initLogging()
|
initLogging()
|
||||||
if doTasks([]task{{s.Restart, true}}) {
|
if doTasks([]task{{s.Restart, true}}) {
|
||||||
stdoutMsg("Service restarted")
|
mainLog.Notice().Msg("Service restarted")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -322,41 +409,58 @@ func initCLI() {
|
|||||||
Use: "status",
|
Use: "status",
|
||||||
Short: "Show status of the ctrld service",
|
Short: "Show status of the ctrld service",
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
|
PreRun: func(cmd *cobra.Command, args []string) {
|
||||||
|
initConsoleLogging()
|
||||||
|
},
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
s, err := service.New(&prog{}, svcConfig)
|
s, err := service.New(&prog{}, svcConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
stderrMsg(err.Error())
|
mainLog.Error().Msg(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
status, err := s.Status()
|
s = newService(s)
|
||||||
|
status, err := serviceStatus(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
stderrMsg(err.Error())
|
mainLog.Error().Msg(err.Error())
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
switch status {
|
switch status {
|
||||||
case service.StatusUnknown:
|
case service.StatusUnknown:
|
||||||
stdoutMsg("Unknown status")
|
mainLog.Notice().Msg("Unknown status")
|
||||||
os.Exit(2)
|
os.Exit(2)
|
||||||
case service.StatusRunning:
|
case service.StatusRunning:
|
||||||
stdoutMsg("Service is running")
|
mainLog.Notice().Msg("Service is running")
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
case service.StatusStopped:
|
case service.StatusStopped:
|
||||||
stdoutMsg("Service is stopped")
|
mainLog.Notice().Msg("Service is stopped")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
if runtime.GOOS == "darwin" {
|
||||||
|
// On darwin, running status command without privileges may return wrong information.
|
||||||
|
statusCmd.PreRun = func(cmd *cobra.Command, args []string) {
|
||||||
|
initConsoleLogging()
|
||||||
|
checkHasElevatedPrivilege()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
uninstallCmd := &cobra.Command{
|
uninstallCmd := &cobra.Command{
|
||||||
PreRun: checkHasElevatedPrivilege,
|
PreRun: func(cmd *cobra.Command, args []string) {
|
||||||
Use: "uninstall",
|
initConsoleLogging()
|
||||||
Short: "Stop and uninstall the ctrld service",
|
checkHasElevatedPrivilege()
|
||||||
Args: cobra.NoArgs,
|
},
|
||||||
|
Use: "uninstall",
|
||||||
|
Short: "Stop and uninstall the ctrld service",
|
||||||
|
Long: `Stop and uninstall the ctrld service.
|
||||||
|
|
||||||
|
NOTE: Uninstalling will set DNS to values provided by DHCP.`,
|
||||||
|
Args: cobra.NoArgs,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
prog := &prog{}
|
prog := &prog{}
|
||||||
s, err := service.New(prog, svcConfig)
|
s, err := service.New(prog, svcConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
stderrMsg(err.Error())
|
mainLog.Error().Msg(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tasks := []task{
|
tasks := []task{
|
||||||
@@ -365,18 +469,28 @@ func initCLI() {
|
|||||||
}
|
}
|
||||||
initLogging()
|
initLogging()
|
||||||
if doTasks(tasks) {
|
if doTasks(tasks) {
|
||||||
|
if iface == "" {
|
||||||
|
iface = "auto"
|
||||||
|
}
|
||||||
prog.resetDNS()
|
prog.resetDNS()
|
||||||
mainLog.Info().Msg("Service uninstalled")
|
mainLog.Debug().Msg("Router cleanup")
|
||||||
|
if err := router.Cleanup(); err != nil {
|
||||||
|
mainLog.Warn().Err(err).Msg("could not cleanup router")
|
||||||
|
}
|
||||||
|
mainLog.Notice().Msg("Service uninstalled")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
uninstallCmd.Flags().StringVarP(&iface, "iface", "", "auto", `Reset DNS setting for iface, "auto" means the default interface gateway`)
|
uninstallCmd.Flags().StringVarP(&iface, "iface", "", "", `Reset DNS setting for iface, use "auto" for the default gateway interface`)
|
||||||
|
|
||||||
listIfacesCmd := &cobra.Command{
|
listIfacesCmd := &cobra.Command{
|
||||||
Use: "list",
|
Use: "list",
|
||||||
Short: "List network interfaces of the host",
|
Short: "List network interfaces of the host",
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
|
PreRun: func(cmd *cobra.Command, args []string) {
|
||||||
|
initConsoleLogging()
|
||||||
|
},
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
err := interfaces.ForeachInterface(func(i interfaces.Interface, prefixes []netip.Prefix) {
|
err := interfaces.ForeachInterface(func(i interfaces.Interface, prefixes []netip.Prefix) {
|
||||||
fmt.Printf("Index : %d\n", i.Index)
|
fmt.Printf("Index : %d\n", i.Index)
|
||||||
@@ -399,7 +513,7 @@ func initCLI() {
|
|||||||
println()
|
println()
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
stderrMsg(err.Error())
|
mainLog.Error().Msg(err.Error())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -434,9 +548,12 @@ func initCLI() {
|
|||||||
serviceCmd.AddCommand(interfacesCmd)
|
serviceCmd.AddCommand(interfacesCmd)
|
||||||
rootCmd.AddCommand(serviceCmd)
|
rootCmd.AddCommand(serviceCmd)
|
||||||
startCmdAlias := &cobra.Command{
|
startCmdAlias := &cobra.Command{
|
||||||
PreRun: checkHasElevatedPrivilege,
|
PreRun: func(cmd *cobra.Command, args []string) {
|
||||||
Use: "start",
|
initConsoleLogging()
|
||||||
Short: "Quick start service and configure DNS on interface",
|
checkHasElevatedPrivilege()
|
||||||
|
},
|
||||||
|
Use: "start",
|
||||||
|
Short: "Quick start service and configure DNS on interface",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
if !cmd.Flags().Changed("iface") {
|
if !cmd.Flags().Changed("iface") {
|
||||||
os.Args = append(os.Args, "--iface="+ifaceStartStop)
|
os.Args = append(os.Args, "--iface="+ifaceStartStop)
|
||||||
@@ -449,9 +566,12 @@ func initCLI() {
|
|||||||
startCmdAlias.Flags().AddFlagSet(startCmd.Flags())
|
startCmdAlias.Flags().AddFlagSet(startCmd.Flags())
|
||||||
rootCmd.AddCommand(startCmdAlias)
|
rootCmd.AddCommand(startCmdAlias)
|
||||||
stopCmdAlias := &cobra.Command{
|
stopCmdAlias := &cobra.Command{
|
||||||
PreRun: checkHasElevatedPrivilege,
|
PreRun: func(cmd *cobra.Command, args []string) {
|
||||||
Use: "stop",
|
initConsoleLogging()
|
||||||
Short: "Quick stop service and remove DNS from interface",
|
checkHasElevatedPrivilege()
|
||||||
|
},
|
||||||
|
Use: "stop",
|
||||||
|
Short: "Quick stop service and remove DNS from interface",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
if !cmd.Flags().Changed("iface") {
|
if !cmd.Flags().Changed("iface") {
|
||||||
os.Args = append(os.Args, "--iface="+ifaceStartStop)
|
os.Args = append(os.Args, "--iface="+ifaceStartStop)
|
||||||
@@ -463,11 +583,6 @@ func initCLI() {
|
|||||||
stopCmdAlias.Flags().StringVarP(&ifaceStartStop, "iface", "", "auto", `Reset DNS setting for iface, "auto" means the default interface gateway`)
|
stopCmdAlias.Flags().StringVarP(&ifaceStartStop, "iface", "", "auto", `Reset DNS setting for iface, "auto" means the default interface gateway`)
|
||||||
stopCmdAlias.Flags().AddFlagSet(stopCmd.Flags())
|
stopCmdAlias.Flags().AddFlagSet(stopCmd.Flags())
|
||||||
rootCmd.AddCommand(stopCmdAlias)
|
rootCmd.AddCommand(stopCmdAlias)
|
||||||
|
|
||||||
if err := rootCmd.Execute(); err != nil {
|
|
||||||
stderrMsg(err.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeConfigFile() error {
|
func writeConfigFile() error {
|
||||||
@@ -500,15 +615,8 @@ func readConfigFile(writeDefaultConfig bool) bool {
|
|||||||
// If err == nil, there's a config supplied via `--config`, no default config written.
|
// If err == nil, there's a config supplied via `--config`, no default config written.
|
||||||
err := v.ReadInConfig()
|
err := v.ReadInConfig()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
fmt.Println("loading config file from:", v.ConfigFileUsed())
|
mainLog.Info().Msg("loading config file from: " + v.ConfigFileUsed())
|
||||||
defaultConfigFile = v.ConfigFileUsed()
|
defaultConfigFile = v.ConfigFileUsed()
|
||||||
v.OnConfigChange(func(in fsnotify.Event) {
|
|
||||||
if err := v.UnmarshalKey("listener", &cfg.Listener); err != nil {
|
|
||||||
log.Printf("failed to unmarshal listener config: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
v.WatchConfig()
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -518,29 +626,36 @@ func readConfigFile(writeDefaultConfig bool) bool {
|
|||||||
|
|
||||||
// If error is viper.ConfigFileNotFoundError, write default config.
|
// If error is viper.ConfigFileNotFoundError, write default config.
|
||||||
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
|
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
|
||||||
|
if err := v.Unmarshal(&cfg); err != nil {
|
||||||
|
mainLog.Fatal().Msgf("failed to unmarshal default config: %v", err)
|
||||||
|
}
|
||||||
if err := writeConfigFile(); err != nil {
|
if err := writeConfigFile(); err != nil {
|
||||||
log.Fatalf("failed to write default config file: %v", err)
|
mainLog.Fatal().Msgf("failed to write default config file: %v", err)
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("writing default config file to: " + defaultConfigFile)
|
fp, err := filepath.Abs(defaultConfigFile)
|
||||||
|
if err != nil {
|
||||||
|
mainLog.Fatal().Msgf("failed to get default config file path: %v", err)
|
||||||
|
}
|
||||||
|
mainLog.Info().Msg("writing default config file to: " + fp)
|
||||||
}
|
}
|
||||||
defaultConfigWritten = true
|
defaultConfigWritten = true
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// Otherwise, report fatal error and exit.
|
// Otherwise, report fatal error and exit.
|
||||||
log.Fatalf("failed to decode config file: %v", err)
|
mainLog.Fatal().Msgf("failed to decode config file: %v", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func readBase64Config() {
|
func readBase64Config(configBase64 string) {
|
||||||
if configBase64 == "" {
|
if configBase64 == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
configStr, err := base64.StdEncoding.DecodeString(configBase64)
|
configStr, err := base64.StdEncoding.DecodeString(configBase64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("invalid base64 config: %v", err)
|
mainLog.Fatal().Msgf("invalid base64 config: %v", err)
|
||||||
}
|
}
|
||||||
if err := v.ReadConfig(bytes.NewReader(configStr)); err != nil {
|
if err := v.ReadConfig(bytes.NewReader(configStr)); err != nil {
|
||||||
log.Fatalf("failed to read base64 config: %v", err)
|
mainLog.Fatal().Msgf("failed to read base64 config: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -549,22 +664,30 @@ func processNoConfigFlags(noConfigStart bool) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if listenAddress == "" || primaryUpstream == "" {
|
if listenAddress == "" || primaryUpstream == "" {
|
||||||
log.Fatal(`"listen" and "primary_upstream" flags must be set in no config mode`)
|
mainLog.Fatal().Msg(`"listen" and "primary_upstream" flags must be set in no config mode`)
|
||||||
}
|
}
|
||||||
processListenFlag()
|
processListenFlag()
|
||||||
|
|
||||||
|
endpointAndTyp := func(endpoint string) (string, string) {
|
||||||
|
typ := ctrld.ResolverTypeFromEndpoint(endpoint)
|
||||||
|
return strings.TrimPrefix(endpoint, "quic://"), typ
|
||||||
|
}
|
||||||
|
pEndpoint, pType := endpointAndTyp(primaryUpstream)
|
||||||
upstream := map[string]*ctrld.UpstreamConfig{
|
upstream := map[string]*ctrld.UpstreamConfig{
|
||||||
"0": {
|
"0": {
|
||||||
Name: primaryUpstream,
|
Name: pEndpoint,
|
||||||
Endpoint: primaryUpstream,
|
Endpoint: pEndpoint,
|
||||||
Type: ctrld.ResolverTypeDOH,
|
Type: pType,
|
||||||
|
Timeout: 5000,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if secondaryUpstream != "" {
|
if secondaryUpstream != "" {
|
||||||
|
sEndpoint, sType := endpointAndTyp(secondaryUpstream)
|
||||||
upstream["1"] = &ctrld.UpstreamConfig{
|
upstream["1"] = &ctrld.UpstreamConfig{
|
||||||
Name: secondaryUpstream,
|
Name: sEndpoint,
|
||||||
Endpoint: secondaryUpstream,
|
Endpoint: sEndpoint,
|
||||||
Type: ctrld.ResolverTypeLegacy,
|
Type: sType,
|
||||||
|
Timeout: 5000,
|
||||||
}
|
}
|
||||||
rules := make([]ctrld.Rule, 0, len(domains))
|
rules := make([]ctrld.Rule, 0, len(domains))
|
||||||
for _, domain := range domains {
|
for _, domain := range domains {
|
||||||
@@ -585,7 +708,7 @@ func processCDFlags() {
|
|||||||
}
|
}
|
||||||
logger := mainLog.With().Str("mode", "cd").Logger()
|
logger := mainLog.With().Str("mode", "cd").Logger()
|
||||||
logger.Info().Msgf("fetching Controld D configuration from API: %s", cdUID)
|
logger.Info().Msgf("fetching Controld D configuration from API: %s", cdUID)
|
||||||
resolverConfig, err := controld.FetchResolverConfig(cdUID)
|
resolverConfig, err := controld.FetchResolverConfig(cdUID, rootCmd.Version)
|
||||||
if uer, ok := err.(*controld.UtilityErrorResponse); ok && uer.ErrorField.Code == controld.InvalidConfigCode {
|
if uer, ok := err.(*controld.UtilityErrorResponse); ok && uer.ErrorField.Code == controld.InvalidConfigCode {
|
||||||
s, err := service.New(&prog{}, svcConfig)
|
s, err := service.New(&prog{}, svcConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -617,34 +740,57 @@ func processCDFlags() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info().Msg("generating ctrld config from Controld-D configuration")
|
logger.Info().Msg("generating ctrld config from Control-D configuration")
|
||||||
cfg = ctrld.Config{}
|
if resolverConfig.Ctrld.CustomConfig != "" {
|
||||||
cfg.Network = make(map[string]*ctrld.NetworkConfig)
|
logger.Info().Msg("using defined custom config of Control-D resolver")
|
||||||
cfg.Network["0"] = &ctrld.NetworkConfig{
|
readBase64Config(resolverConfig.Ctrld.CustomConfig)
|
||||||
Name: "Network 0",
|
if err := v.Unmarshal(&cfg); err != nil {
|
||||||
Cidrs: []string{"0.0.0.0/0"},
|
mainLog.Fatal().Msgf("failed to unmarshal config: %v", err)
|
||||||
}
|
}
|
||||||
cfg.Upstream = make(map[string]*ctrld.UpstreamConfig)
|
for _, listener := range cfg.Listener {
|
||||||
cfg.Upstream["0"] = &ctrld.UpstreamConfig{
|
if listener.IP == "" {
|
||||||
Endpoint: resolverConfig.DOH,
|
listener.IP = randomLocalIP()
|
||||||
Type: ctrld.ResolverTypeDOH,
|
}
|
||||||
Timeout: 5000,
|
if listener.Port == 0 {
|
||||||
}
|
listener.Port = 53
|
||||||
rules := make([]ctrld.Rule, 0, len(resolverConfig.Exclude))
|
}
|
||||||
for _, domain := range resolverConfig.Exclude {
|
}
|
||||||
rules = append(rules, ctrld.Rule{domain: []string{}})
|
// On router, we want to keep the listener address point to dnsmasq listener, aka 127.0.0.1:53.
|
||||||
}
|
if router.Name() != "" {
|
||||||
cfg.Listener = make(map[string]*ctrld.ListenerConfig)
|
if lc := cfg.Listener["0"]; lc != nil {
|
||||||
cfg.Listener["0"] = &ctrld.ListenerConfig{
|
lc.IP = "127.0.0.1"
|
||||||
IP: "127.0.0.1",
|
lc.Port = 53
|
||||||
Port: 53,
|
}
|
||||||
Policy: &ctrld.ListenerPolicyConfig{
|
}
|
||||||
Name: "My Policy",
|
} else {
|
||||||
Rules: rules,
|
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
processLogAndCacheFlags()
|
||||||
}
|
}
|
||||||
|
|
||||||
processLogAndCacheFlags()
|
|
||||||
if err := writeConfigFile(); err != nil {
|
if err := writeConfigFile(); err != nil {
|
||||||
logger.Fatal().Err(err).Msg("failed to write config file")
|
logger.Fatal().Err(err).Msg("failed to write config file")
|
||||||
} else {
|
} else {
|
||||||
@@ -658,11 +804,11 @@ func processListenFlag() {
|
|||||||
}
|
}
|
||||||
host, portStr, err := net.SplitHostPort(listenAddress)
|
host, portStr, err := net.SplitHostPort(listenAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("invalid listener address: %v", err)
|
mainLog.Fatal().Msgf("invalid listener address: %v", err)
|
||||||
}
|
}
|
||||||
port, err := strconv.Atoi(portStr)
|
port, err := strconv.Atoi(portStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("invalid port number: %v", err)
|
mainLog.Fatal().Msgf("invalid port number: %v", err)
|
||||||
}
|
}
|
||||||
lc := &ctrld.ListenerConfig{
|
lc := &ctrld.ListenerConfig{
|
||||||
IP: host,
|
IP: host,
|
||||||
@@ -721,8 +867,26 @@ func selfCheckStatus(status service.Status) service.Status {
|
|||||||
err := errors.New("query failed")
|
err := errors.New("query failed")
|
||||||
maxAttempts := 20
|
maxAttempts := 20
|
||||||
mainLog.Debug().Msg("Performing self-check")
|
mainLog.Debug().Msg("Performing self-check")
|
||||||
|
var (
|
||||||
|
lcChanged map[string]*ctrld.ListenerConfig
|
||||||
|
mu sync.Mutex
|
||||||
|
)
|
||||||
|
v.OnConfigChange(func(in fsnotify.Event) {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
if err := v.UnmarshalKey("listener", &lcChanged); err != nil {
|
||||||
|
mainLog.Error().Msgf("failed to unmarshal listener config: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
v.WatchConfig()
|
||||||
for i := 0; i < maxAttempts; i++ {
|
for i := 0; i < maxAttempts; i++ {
|
||||||
lc := cfg.Listener["0"]
|
lc := cfg.Listener["0"]
|
||||||
|
mu.Lock()
|
||||||
|
if lcChanged != nil {
|
||||||
|
lc = lcChanged["0"]
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
m := new(dns.Msg)
|
m := new(dns.Msg)
|
||||||
m.SetQuestion(selfCheckFQDN+".", dns.TypeA)
|
m.SetQuestion(selfCheckFQDN+".", dns.TypeA)
|
||||||
m.RecursionDesired = true
|
m.RecursionDesired = true
|
||||||
@@ -736,3 +900,50 @@ func selfCheckStatus(status service.Status) service.Status {
|
|||||||
mainLog.Debug().Msgf("self-check against %q failed", selfCheckFQDN)
|
mainLog.Debug().Msgf("self-check against %q failed", selfCheckFQDN)
|
||||||
return service.StatusUnknown
|
return service.StatusUnknown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func unsupportedPlatformHelp(cmd *cobra.Command) {
|
||||||
|
mainLog.Error().Msg("Unsupported or incorrectly chosen router platform. Please open an issue and provide all relevant information: https://github.com/Control-D-Inc/ctrld/issues/new")
|
||||||
|
}
|
||||||
|
|
||||||
|
func userHomeDir() (string, error) {
|
||||||
|
switch router.Name() {
|
||||||
|
case router.DDWrt, router.Merlin:
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return filepath.Dir(exe), nil
|
||||||
|
}
|
||||||
|
// viper will expand for us.
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return os.UserHomeDir()
|
||||||
|
}
|
||||||
|
dir := "/etc/controld"
|
||||||
|
if err := os.MkdirAll(dir, 0750); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return dir, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func tryReadingConfig(writeDefaultConfig bool) {
|
||||||
|
configs := []struct {
|
||||||
|
name string
|
||||||
|
written bool
|
||||||
|
}{
|
||||||
|
// For compatibility, we check for config.toml first, but only read it if exists.
|
||||||
|
{"config", false},
|
||||||
|
{"ctrld", writeDefaultConfig},
|
||||||
|
}
|
||||||
|
|
||||||
|
dir, err := userHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
mainLog.Fatal().Msgf("failed to get config dir: %v", err)
|
||||||
|
}
|
||||||
|
for _, config := range configs {
|
||||||
|
ctrld.SetConfigNameWithPath(v, config.name, dir)
|
||||||
|
v.SetConfigFile(configPath)
|
||||||
|
if readConfigFile(config.written) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/Control-D-Inc/ctrld/internal/router"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initRouterCLI() {
|
||||||
|
validArgs := append(router.SupportedPlatforms(), "auto")
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString("Auto-setup Control D on a router.\n\nSupported platforms:\n\n")
|
||||||
|
for _, arg := range validArgs {
|
||||||
|
b.WriteString(" ₒ ")
|
||||||
|
b.WriteString(arg)
|
||||||
|
if arg == "auto" {
|
||||||
|
b.WriteString(" - detect the platform you are running on")
|
||||||
|
}
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
routerCmd := &cobra.Command{
|
||||||
|
Use: "setup",
|
||||||
|
Short: b.String(),
|
||||||
|
PreRun: func(cmd *cobra.Command, args []string) {
|
||||||
|
initConsoleLogging()
|
||||||
|
},
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
_ = cmd.Help()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(args) != 1 {
|
||||||
|
_ = cmd.Help()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
platform := args[0]
|
||||||
|
if platform == "auto" {
|
||||||
|
platform = router.Name()
|
||||||
|
}
|
||||||
|
switch platform {
|
||||||
|
case router.DDWrt, router.Merlin, router.OpenWrt, router.Ubios:
|
||||||
|
default:
|
||||||
|
unsupportedPlatformHelp(cmd)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
mainLog.Fatal().Msgf("could not find executable path: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdArgs := []string{"start"}
|
||||||
|
cmdArgs = append(cmdArgs, osArgs(platform)...)
|
||||||
|
cmdArgs = append(cmdArgs, "--router")
|
||||||
|
command := exec.Command(exe, cmdArgs...)
|
||||||
|
command.Stdout = os.Stdout
|
||||||
|
command.Stderr = os.Stderr
|
||||||
|
command.Stdin = os.Stdin
|
||||||
|
if err := command.Run(); err != nil {
|
||||||
|
mainLog.Fatal().Msg(err.Error())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
// Keep these flags in sync with startCmd, except for "--router".
|
||||||
|
routerCmd.Flags().StringVarP(&configPath, "config", "c", "", "Path to config file")
|
||||||
|
routerCmd.Flags().StringVarP(&configBase64, "base64_config", "", "", "Base64 encoded config")
|
||||||
|
routerCmd.Flags().StringVarP(&listenAddress, "listen", "", "", "Listener address and port, in format: address:port")
|
||||||
|
routerCmd.Flags().StringVarP(&primaryUpstream, "primary_upstream", "", "", "Primary upstream endpoint")
|
||||||
|
routerCmd.Flags().StringVarP(&secondaryUpstream, "secondary_upstream", "", "", "Secondary upstream endpoint")
|
||||||
|
routerCmd.Flags().StringSliceVarP(&domains, "domains", "", nil, "List of domain to apply in a split DNS policy")
|
||||||
|
routerCmd.Flags().StringVarP(&logPath, "log", "", "", "Path to log file")
|
||||||
|
routerCmd.Flags().IntVarP(&cacheSize, "cache_size", "", 0, "Enable cache with size items")
|
||||||
|
routerCmd.Flags().StringVarP(&cdUID, "cd", "", "", "Control D resolver uid")
|
||||||
|
routerCmd.Flags().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`)
|
||||||
|
|
||||||
|
tmpl := routerCmd.UsageTemplate()
|
||||||
|
tmpl = strings.Replace(tmpl, "{{.UseLine}}", "{{.UseLine}} [platform]", 1)
|
||||||
|
routerCmd.SetUsageTemplate(tmpl)
|
||||||
|
rootCmd.AddCommand(routerCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func osArgs(platform string) []string {
|
||||||
|
args := os.Args[2:]
|
||||||
|
n := 0
|
||||||
|
for _, x := range args {
|
||||||
|
if x != platform && x != "auto" {
|
||||||
|
args[n] = x
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return args[:n]
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
//go:build !linux
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
func initRouterCLI() {}
|
||||||
+149
-49
@@ -9,6 +9,7 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
@@ -17,9 +18,22 @@ import (
|
|||||||
"github.com/Control-D-Inc/ctrld"
|
"github.com/Control-D-Inc/ctrld"
|
||||||
"github.com/Control-D-Inc/ctrld/internal/dnscache"
|
"github.com/Control-D-Inc/ctrld/internal/dnscache"
|
||||||
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
||||||
|
"github.com/Control-D-Inc/ctrld/internal/router"
|
||||||
)
|
)
|
||||||
|
|
||||||
const staleTTL = 60 * time.Second
|
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 {
|
func (p *prog) serveDNS(listenerNum string) error {
|
||||||
listenerConfig := p.cfg.Listener[listenerNum]
|
listenerConfig := p.cfg.Listener[listenerNum]
|
||||||
@@ -56,40 +70,52 @@ func (p *prog) serveDNS(listenerNum string) error {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
g := new(errgroup.Group)
|
g, ctx := errgroup.WithContext(context.Background())
|
||||||
for _, proto := range []string{"udp", "tcp"} {
|
for _, proto := range []string{"udp", "tcp"} {
|
||||||
proto := proto
|
proto := proto
|
||||||
// On Windows, there's no easy way for disabling/removing IPv6 DNS resolver, so we check whether we can
|
if needLocalIPv6Listener() {
|
||||||
// listen on ::1, then spawn a listener for receiving DNS requests.
|
|
||||||
if runtime.GOOS == "windows" && ctrldnet.SupportsIPv6ListenLocal() {
|
|
||||||
g.Go(func() error {
|
g.Go(func() error {
|
||||||
s := &dns.Server{
|
s, errCh := runDNSServer(net.JoinHostPort("::1", strconv.Itoa(listenerConfig.Port)), proto, handler)
|
||||||
Addr: net.JoinHostPort("::1", strconv.Itoa(listenerConfig.Port)),
|
defer s.Shutdown()
|
||||||
Net: proto,
|
select {
|
||||||
Handler: handler,
|
case <-ctx.Done():
|
||||||
}
|
case err := <-errCh:
|
||||||
if err := s.ListenAndServe(); err != nil {
|
// Local ipv6 listener should not terminate ctrld.
|
||||||
mainLog.Error().Err(err).Msg("could not serving on ::1")
|
// It's a workaround for a quirk on Windows.
|
||||||
|
mainLog.Warn().Err(err).Msg("local ipv6 listener failed")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
g.Go(func() error {
|
g.Go(func() error {
|
||||||
s := &dns.Server{
|
s, errCh := runDNSServer(dnsListenAddress(listenerNum, listenerConfig), proto, handler)
|
||||||
Addr: net.JoinHostPort(listenerConfig.IP, strconv.Itoa(listenerConfig.Port)),
|
defer s.Shutdown()
|
||||||
Net: proto,
|
if listenerConfig.Port == 0 {
|
||||||
Handler: handler,
|
switch s.Net {
|
||||||
|
case "udp":
|
||||||
|
mainLog.Info().Msgf("Random port chosen for udp listener.%s: %s", listenerNum, s.PacketConn.LocalAddr())
|
||||||
|
case "tcp":
|
||||||
|
mainLog.Info().Msgf("Random port chosen for tcp listener.%s: %s", listenerNum, s.Listener.Addr())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err := s.ListenAndServe(); err != nil {
|
select {
|
||||||
mainLog.Error().Err(err).Msgf("could not listen and serve on: %s", s.Addr)
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
case err := <-errCh:
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return g.Wait()
|
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) {
|
func (p *prog) upstreamFor(ctx context.Context, defaultUpstreamNum string, lc *ctrld.ListenerConfig, addr net.Addr, domain string) ([]string, bool) {
|
||||||
upstreams := []string{"upstream." + defaultUpstreamNum}
|
upstreams := []string{"upstream." + defaultUpstreamNum}
|
||||||
matchedPolicy := "no policy"
|
matchedPolicy := "no policy"
|
||||||
@@ -113,11 +139,43 @@ func (p *prog) upstreamFor(ctx context.Context, defaultUpstreamNum string, lc *c
|
|||||||
upstreams = append([]string(nil), policyUpstreams...)
|
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 {
|
for _, rule := range lc.Policy.Rules {
|
||||||
// There's only one entry per rule, config validation ensures this.
|
// There's only one entry per rule, config validation ensures this.
|
||||||
for source, targets := range rule {
|
for source, targets := range rule {
|
||||||
if source == domain || wildcardMatches(source, domain) {
|
if source == domain || wildcardMatches(source, domain) {
|
||||||
matchedPolicy = lc.Policy.Name
|
matchedPolicy = lc.Policy.Name
|
||||||
|
if len(networkTargets) > 0 {
|
||||||
|
matchedNetwork += " (unenforced)"
|
||||||
|
}
|
||||||
matchedRule = source
|
matchedRule = source
|
||||||
do(targets)
|
do(targets)
|
||||||
matched = true
|
matched = true
|
||||||
@@ -126,31 +184,8 @@ func (p *prog) upstreamFor(ctx context.Context, defaultUpstreamNum string, lc *c
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var sourceIP net.IP
|
if matched {
|
||||||
switch addr := addr.(type) {
|
do(networkTargets)
|
||||||
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
|
return upstreams, matched
|
||||||
@@ -199,8 +234,16 @@ func (p *prog) proxy(ctx context.Context, upstreams []string, failoverRcodes []i
|
|||||||
return dnsResolver.Resolve(resolveCtx, msg)
|
return dnsResolver.Resolve(resolveCtx, msg)
|
||||||
}
|
}
|
||||||
resolve := func(n int, upstreamConfig *ctrld.UpstreamConfig, msg *dns.Msg) *dns.Msg {
|
resolve := func(n int, upstreamConfig *ctrld.UpstreamConfig, msg *dns.Msg) *dns.Msg {
|
||||||
|
if upstreamConfig.UpstreamSendClientInfo() {
|
||||||
|
ci := router.GetClientInfoByMac(macFromMsg(msg))
|
||||||
|
if ci != nil {
|
||||||
|
ctrld.Log(ctx, mainLog.Debug(), "including client info with the request")
|
||||||
|
ctx = context.WithValue(ctx, ctrld.ClientInfoCtxKey{}, ci)
|
||||||
|
}
|
||||||
|
}
|
||||||
answer, err := resolve1(n, upstreamConfig, msg)
|
answer, err := resolve1(n, upstreamConfig, msg)
|
||||||
if err != nil {
|
// Only do re-bootstrapping if bootstrap ip is not explicitly set by user.
|
||||||
|
if err != nil && upstreamConfig.BootstrapIP == "" {
|
||||||
ctrld.Log(ctx, mainLog.Debug().Err(err), "could not resolve query on first attempt, retrying...")
|
ctrld.Log(ctx, mainLog.Debug().Err(err), "could not resolve query on first attempt, retrying...")
|
||||||
// If any error occurred, re-bootstrap transport/ip, retry the request.
|
// If any error occurred, re-bootstrap transport/ip, retry the request.
|
||||||
upstreamConfig.ReBootstrap()
|
upstreamConfig.ReBootstrap()
|
||||||
@@ -214,6 +257,9 @@ func (p *prog) proxy(ctx context.Context, upstreams []string, failoverRcodes []i
|
|||||||
return answer
|
return answer
|
||||||
}
|
}
|
||||||
for n, upstreamConfig := range upstreamConfigs {
|
for n, upstreamConfig := range upstreamConfigs {
|
||||||
|
if upstreamConfig == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
answer := resolve(n, upstreamConfig, msg)
|
answer := resolve(n, upstreamConfig, msg)
|
||||||
if answer == nil {
|
if answer == nil {
|
||||||
if serveStaleCache && staleAnswer != nil {
|
if serveStaleCache && staleAnswer != nil {
|
||||||
@@ -228,6 +274,10 @@ func (p *prog) proxy(ctx context.Context, upstreams []string, failoverRcodes []i
|
|||||||
ctrld.Log(ctx, mainLog.Debug(), "failover rcode matched, process to next upstream")
|
ctrld.Log(ctx, mainLog.Debug(), "failover rcode matched, process to next upstream")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set compression, as it is not set by default when unpacking
|
||||||
|
answer.Compress = true
|
||||||
|
|
||||||
if p.cache != nil {
|
if p.cache != nil {
|
||||||
ttl := ttlFromMsg(answer)
|
ttl := ttlFromMsg(answer)
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
@@ -341,8 +391,58 @@ func ttlFromMsg(msg *dns.Msg) uint32 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
var osUpstreamConfig = &ctrld.UpstreamConfig{
|
func needLocalIPv6Listener() bool {
|
||||||
Name: "OS resolver",
|
// On Windows, there's no easy way for disabling/removing IPv6 DNS resolver, so we check whether we can
|
||||||
Type: ctrld.ResolverTypeOS,
|
// listen on ::1, then spawn a listener for receiving DNS requests.
|
||||||
Timeout: 2000,
|
return ctrldnet.SupportsIPv6ListenLocal() && runtime.GOOS == "windows"
|
||||||
|
}
|
||||||
|
|
||||||
|
func dnsListenAddress(lcNum string, lc *ctrld.ListenerConfig) string {
|
||||||
|
if addr := router.ListenAddress(); setupRouter && addr != "" && lcNum == "0" {
|
||||||
|
return addr
|
||||||
|
}
|
||||||
|
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 ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.Error().Err(err).Msgf("could not listen and serve on: %s", s.Addr)
|
||||||
|
errCh <- err
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
waitLock.Lock()
|
||||||
|
return s, errCh
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -152,3 +155,39 @@ func TestCache(t *testing.T) {
|
|||||||
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(selfCheckFQDN+".", 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+42
-17
@@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
@@ -26,18 +25,24 @@ var (
|
|||||||
cacheSize int
|
cacheSize int
|
||||||
cfg ctrld.Config
|
cfg ctrld.Config
|
||||||
verbose int
|
verbose int
|
||||||
|
silent bool
|
||||||
|
cdUID string
|
||||||
|
iface string
|
||||||
|
ifaceStartStop string
|
||||||
|
setupRouter bool
|
||||||
|
|
||||||
rootLogger = zerolog.New(io.Discard)
|
mainLog = zerolog.New(io.Discard)
|
||||||
mainLog = rootLogger
|
consoleWriter zerolog.ConsoleWriter
|
||||||
|
|
||||||
cdUID string
|
|
||||||
iface string
|
|
||||||
ifaceStartStop string
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
ctrld.InitConfig(v, "ctrld")
|
ctrld.InitConfig(v, "ctrld")
|
||||||
initCLI()
|
initCLI()
|
||||||
|
initRouterCLI()
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
mainLog.Error().Msg(err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeLogFilePath(logFilePath string) string {
|
func normalizeLogFilePath(logFilePath string) string {
|
||||||
@@ -47,45 +52,65 @@ func normalizeLogFilePath(logFilePath string) string {
|
|||||||
if homedir != "" {
|
if homedir != "" {
|
||||||
return filepath.Join(homedir, logFilePath)
|
return filepath.Join(homedir, logFilePath)
|
||||||
}
|
}
|
||||||
dir, _ := os.UserHomeDir()
|
dir, _ := userHomeDir()
|
||||||
if dir == "" {
|
if dir == "" {
|
||||||
return logFilePath
|
return logFilePath
|
||||||
}
|
}
|
||||||
return filepath.Join(dir, logFilePath)
|
return filepath.Join(dir, logFilePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func initConsoleLogging() {
|
||||||
|
consoleWriter = zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) {
|
||||||
|
w.TimeFormat = time.StampMilli
|
||||||
|
})
|
||||||
|
multi := zerolog.MultiLevelWriter(consoleWriter)
|
||||||
|
mainLog = mainLog.Output(multi).With().Timestamp().Logger()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func initLogging() {
|
func initLogging() {
|
||||||
writers := []io.Writer{io.Discard}
|
writers := []io.Writer{io.Discard}
|
||||||
if logFilePath := normalizeLogFilePath(cfg.Service.LogPath); logFilePath != "" {
|
if logFilePath := normalizeLogFilePath(cfg.Service.LogPath); logFilePath != "" {
|
||||||
// Create parent directory if necessary.
|
// Create parent directory if necessary.
|
||||||
if err := os.MkdirAll(filepath.Dir(logFilePath), 0750); err != nil {
|
if err := os.MkdirAll(filepath.Dir(logFilePath), 0750); err != nil {
|
||||||
log.Printf("failed to create log path: %v", err)
|
mainLog.Error().Msgf("failed to create log path: %v", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
// Backup old log file with .1 suffix.
|
// Backup old log file with .1 suffix.
|
||||||
if err := os.Rename(logFilePath, logFilePath+".1"); err != nil && !os.IsNotExist(err) {
|
if err := os.Rename(logFilePath, logFilePath+".1"); err != nil && !os.IsNotExist(err) {
|
||||||
log.Printf("could not backup old log file: %v", err)
|
mainLog.Error().Msgf("could not backup old log file: %v", err)
|
||||||
}
|
}
|
||||||
logFile, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_RDWR, os.FileMode(0o600))
|
logFile, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_RDWR, os.FileMode(0o600))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("failed to create log file: %v", err)
|
mainLog.Error().Msgf("failed to create log file: %v", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
writers = append(writers, logFile)
|
writers = append(writers, logFile)
|
||||||
}
|
}
|
||||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
|
|
||||||
consoleWriter := zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) {
|
|
||||||
w.TimeFormat = time.StampMilli
|
|
||||||
})
|
|
||||||
writers = append(writers, consoleWriter)
|
writers = append(writers, consoleWriter)
|
||||||
multi := zerolog.MultiLevelWriter(writers...)
|
multi := zerolog.MultiLevelWriter(writers...)
|
||||||
mainLog = mainLog.Output(multi).With().Timestamp().Logger()
|
mainLog = mainLog.Output(multi).With().Timestamp().Logger()
|
||||||
// TODO: find a better way.
|
// TODO: find a better way.
|
||||||
ctrld.ProxyLog = mainLog
|
ctrld.ProxyLog = mainLog
|
||||||
|
|
||||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
zerolog.SetGlobalLevel(zerolog.NoticeLevel)
|
||||||
logLevel := cfg.Service.LogLevel
|
logLevel := cfg.Service.LogLevel
|
||||||
if verbose > 1 {
|
switch {
|
||||||
|
case silent:
|
||||||
|
zerolog.SetGlobalLevel(zerolog.NoLevel)
|
||||||
|
return
|
||||||
|
case verbose == 1:
|
||||||
|
logLevel = "info"
|
||||||
|
case verbose > 1:
|
||||||
logLevel = "debug"
|
logLevel = "debug"
|
||||||
}
|
}
|
||||||
if logLevel == "" {
|
if logLevel == "" {
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
|
var logOutput strings.Builder
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
mainLog = zerolog.New(&logOutput)
|
||||||
|
os.Exit(m.Run())
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
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.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.Debug().Msgf("link state changed, re-bootstrapping")
|
||||||
|
for _, uc := range p.cfg.Upstream {
|
||||||
|
uc.ReBootstrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
//go:build !linux
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
func (p *prog) watchLinkState() {}
|
||||||
@@ -112,7 +112,7 @@ func resetDNS(iface *net.Interface) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO(cuonglm): handle DHCPv6 properly.
|
// TODO(cuonglm): handle DHCPv6 properly.
|
||||||
if ctrldnet.SupportsIPv6() {
|
if ctrldnet.IPv6Available(ctx) {
|
||||||
c := client6.NewClient()
|
c := client6.NewClient()
|
||||||
conversation, err := c.Exchange(iface.Name)
|
conversation, err := c.Exchange(iface.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+35
-3
@@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld"
|
"github.com/Control-D-Inc/ctrld"
|
||||||
"github.com/Control-D-Inc/ctrld/internal/dnscache"
|
"github.com/Control-D-Inc/ctrld/internal/dnscache"
|
||||||
|
"github.com/Control-D-Inc/ctrld/internal/router"
|
||||||
)
|
)
|
||||||
|
|
||||||
var logf = func(format string, args ...any) {
|
var logf = func(format string, args ...any) {
|
||||||
@@ -25,9 +26,14 @@ var errWindowsAddrInUse = syscall.Errno(0x2740)
|
|||||||
var svcConfig = &service.Config{
|
var svcConfig = &service.Config{
|
||||||
Name: "ctrld",
|
Name: "ctrld",
|
||||||
DisplayName: "Control-D Helper Service",
|
DisplayName: "Control-D Helper Service",
|
||||||
|
Option: service.KeyValue{},
|
||||||
}
|
}
|
||||||
|
|
||||||
type prog struct {
|
type prog struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
waitCh chan struct{}
|
||||||
|
stopCh chan struct{}
|
||||||
|
|
||||||
cfg *ctrld.Config
|
cfg *ctrld.Config
|
||||||
cache dnscache.Cacher
|
cache dnscache.Cacher
|
||||||
}
|
}
|
||||||
@@ -39,6 +45,8 @@ func (p *prog) Start(s service.Service) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *prog) run() {
|
func (p *prog) run() {
|
||||||
|
// Wait the caller to signal that we can do our logic.
|
||||||
|
<-p.waitCh
|
||||||
p.preRun()
|
p.preRun()
|
||||||
if p.cfg.Service.CacheEnable {
|
if p.cfg.Service.CacheEnable {
|
||||||
cacher, err := dnscache.NewLRUCache(p.cfg.Service.CacheSize)
|
cacher, err := dnscache.NewLRUCache(p.cfg.Service.CacheSize)
|
||||||
@@ -66,13 +74,16 @@ func (p *prog) run() {
|
|||||||
uc.Init()
|
uc.Init()
|
||||||
if uc.BootstrapIP == "" {
|
if uc.BootstrapIP == "" {
|
||||||
uc.SetupBootstrapIP()
|
uc.SetupBootstrapIP()
|
||||||
mainLog.Info().Str("bootstrap_ip", uc.BootstrapIP).Msgf("Setting bootstrap IP for upstream.%s", n)
|
mainLog.Info().Msgf("Bootstrap IPs for upstream.%s: %q", n, uc.BootstrapIPs())
|
||||||
} else {
|
} else {
|
||||||
mainLog.Info().Str("bootstrap_ip", uc.BootstrapIP).Msgf("Using bootstrap IP for upstream.%s", n)
|
mainLog.Info().Str("bootstrap_ip", uc.BootstrapIP).Msgf("Using bootstrap IP for upstream.%s", n)
|
||||||
}
|
}
|
||||||
|
uc.SetCertPool(rootCertPool)
|
||||||
uc.SetupTransport()
|
uc.SetupTransport()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
go p.watchLinkState()
|
||||||
|
|
||||||
for listenerNum := range p.cfg.Listener {
|
for listenerNum := range p.cfg.Listener {
|
||||||
p.cfg.Listener[listenerNum].Init()
|
p.cfg.Listener[listenerNum].Init()
|
||||||
go func(listenerNum string) {
|
go func(listenerNum string) {
|
||||||
@@ -80,8 +91,7 @@ func (p *prog) run() {
|
|||||||
listenerConfig := p.cfg.Listener[listenerNum]
|
listenerConfig := p.cfg.Listener[listenerNum]
|
||||||
upstreamConfig := p.cfg.Upstream[listenerNum]
|
upstreamConfig := p.cfg.Upstream[listenerNum]
|
||||||
if upstreamConfig == nil {
|
if upstreamConfig == nil {
|
||||||
mainLog.Error().Msgf("missing upstream config for: [listener.%s]", listenerNum)
|
mainLog.Warn().Msgf("no default upstream for: [listener.%s]", listenerNum)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
addr := net.JoinHostPort(listenerConfig.IP, strconv.Itoa(listenerConfig.Port))
|
addr := net.JoinHostPort(listenerConfig.IP, strconv.Itoa(listenerConfig.Port))
|
||||||
mainLog.Info().Msgf("Starting DNS server on listener.%s: %s", listenerNum, addr)
|
mainLog.Info().Msgf("Starting DNS server on listener.%s: %s", listenerNum, addr)
|
||||||
@@ -106,7 +116,9 @@ func (p *prog) run() {
|
|||||||
} else {
|
} else {
|
||||||
mainLog.Info().Msg("writing config file to: " + defaultConfigFile)
|
mainLog.Info().Msg("writing config file to: " + defaultConfigFile)
|
||||||
}
|
}
|
||||||
|
p.mu.Lock()
|
||||||
p.cfg.Service.AllocateIP = true
|
p.cfg.Service.AllocateIP = true
|
||||||
|
p.mu.Unlock()
|
||||||
p.preRun()
|
p.preRun()
|
||||||
mainLog.Info().Msgf("Starting DNS server on listener.%s: %s", listenerNum, net.JoinHostPort(ip, strconv.Itoa(port)))
|
mainLog.Info().Msgf("Starting DNS server on listener.%s: %s", listenerNum, net.JoinHostPort(ip, strconv.Itoa(port)))
|
||||||
if err := p.serveDNS(listenerNum); err != nil {
|
if err := p.serveDNS(listenerNum); err != nil {
|
||||||
@@ -127,11 +139,18 @@ func (p *prog) Stop(s service.Service) error {
|
|||||||
mainLog.Error().Err(err).Msg("de-allocate ip failed")
|
mainLog.Error().Err(err).Msg("de-allocate ip failed")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
p.preStop()
|
||||||
|
if err := router.Stop(); err != nil {
|
||||||
|
mainLog.Warn().Err(err).Msg("problem occurred while stopping router")
|
||||||
|
}
|
||||||
mainLog.Info().Msg("Service stopped")
|
mainLog.Info().Msg("Service stopped")
|
||||||
|
close(p.stopCh)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *prog) allocateIP(ip string) error {
|
func (p *prog) allocateIP(ip string) error {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
if !p.cfg.Service.AllocateIP {
|
if !p.cfg.Service.AllocateIP {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -139,6 +158,8 @@ func (p *prog) allocateIP(ip string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *prog) deAllocateIP() error {
|
func (p *prog) deAllocateIP() error {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
if !p.cfg.Service.AllocateIP {
|
if !p.cfg.Service.AllocateIP {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -151,6 +172,12 @@ func (p *prog) deAllocateIP() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *prog) setDNS() {
|
func (p *prog) setDNS() {
|
||||||
|
switch router.Name() {
|
||||||
|
case router.DDWrt, router.OpenWrt, router.Ubios:
|
||||||
|
// On router, ctrld run as a DNS forwarder, it does not have to change system DNS.
|
||||||
|
// Except for Merlin, which has WAN DNS setup on boot for NTP.
|
||||||
|
return
|
||||||
|
}
|
||||||
if cfg.Listener == nil || cfg.Listener["0"] == nil {
|
if cfg.Listener == nil || cfg.Listener["0"] == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -179,6 +206,11 @@ func (p *prog) setDNS() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *prog) resetDNS() {
|
func (p *prog) resetDNS() {
|
||||||
|
switch router.Name() {
|
||||||
|
case router.DDWrt, router.OpenWrt, router.Ubios:
|
||||||
|
// See comment in p.setDNS method.
|
||||||
|
return
|
||||||
|
}
|
||||||
if iface == "" {
|
if iface == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/kardianos/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *prog) preRun() {
|
||||||
|
if !service.Interactive() {
|
||||||
|
p.setDNS()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setDependencies(svc *service.Config) {}
|
||||||
|
|
||||||
|
func setWorkingDirectory(svc *service.Config, dir string) {
|
||||||
|
svc.WorkingDirectory = dir
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *prog) preStop() {
|
||||||
|
if !service.Interactive() {
|
||||||
|
p.resetDNS()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,3 +18,5 @@ func setDependencies(svc *service.Config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func setWorkingDirectory(svc *service.Config, dir string) {}
|
func setWorkingDirectory(svc *service.Config, dir string) {}
|
||||||
|
|
||||||
|
func (p *prog) preStop() {}
|
||||||
|
|||||||
@@ -22,3 +22,5 @@ func setDependencies(svc *service.Config) {
|
|||||||
func setWorkingDirectory(svc *service.Config, dir string) {
|
func setWorkingDirectory(svc *service.Config, dir string) {
|
||||||
svc.WorkingDirectory = dir
|
svc.WorkingDirectory = dir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *prog) preStop() {}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !linux && !freebsd
|
//go:build !linux && !freebsd && !darwin
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
@@ -12,3 +12,5 @@ func setWorkingDirectory(svc *service.Config, dir string) {
|
|||||||
// WorkingDirectory is not supported on Windows.
|
// WorkingDirectory is not supported on Windows.
|
||||||
svc.WorkingDirectory = dir
|
svc.WorkingDirectory = dir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *prog) preStop() {}
|
||||||
|
|||||||
+59
-10
@@ -1,18 +1,44 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"bytes"
|
||||||
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/kardianos/service"
|
||||||
|
|
||||||
|
"github.com/Control-D-Inc/ctrld/internal/router"
|
||||||
)
|
)
|
||||||
|
|
||||||
func stderrMsg(msg string) {
|
func newService(s service.Service) service.Service {
|
||||||
_, _ = fmt.Fprintln(os.Stderr, msg)
|
// TODO: unify for other SysV system.
|
||||||
|
if router.IsGLiNet() {
|
||||||
|
return &sysV{s}
|
||||||
|
}
|
||||||
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func stdoutMsg(msg string) {
|
// sysV wraps a service.Service, and provide start/stop/status command
|
||||||
_, _ = fmt.Fprintln(os.Stdout, msg)
|
// base on "/etc/init.d/<service_name>".
|
||||||
|
//
|
||||||
|
// Use this on system wherer "service" command is not available, like GL.iNET router.
|
||||||
|
type sysV struct {
|
||||||
|
service.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sysV) Start() error {
|
||||||
|
_, err := exec.Command("/etc/init.d/ctrld", "start").CombinedOutput()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sysV) Stop() error {
|
||||||
|
_, err := exec.Command("/etc/init.d/ctrld", "stop").CombinedOutput()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sysV) Status() (service.Status, error) {
|
||||||
|
return unixSystemVServiceStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
type task struct {
|
type task struct {
|
||||||
@@ -21,25 +47,48 @@ type task struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func doTasks(tasks []task) bool {
|
func doTasks(tasks []task) bool {
|
||||||
|
var prevErr error
|
||||||
for _, task := range tasks {
|
for _, task := range tasks {
|
||||||
if err := task.f(); err != nil {
|
if err := task.f(); err != nil {
|
||||||
if task.abortOnError {
|
if task.abortOnError {
|
||||||
stderrMsg(err.Error())
|
mainLog.Error().Msg(errors.Join(prevErr, err).Error())
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
prevErr = err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkHasElevatedPrivilege(cmd *cobra.Command, args []string) {
|
func checkHasElevatedPrivilege() {
|
||||||
ok, err := hasElevatedPrivilege()
|
ok, err := hasElevatedPrivilege()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("could not detect user privilege: %v", err)
|
mainLog.Error().Msgf("could not detect user privilege: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
fmt.Println("Please relaunch process with admin/root privilege.")
|
mainLog.Error().Msg("Please relaunch process with admin/root privilege.")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func serviceStatus(s service.Service) (service.Status, error) {
|
||||||
|
status, err := s.Status()
|
||||||
|
if err != nil && service.Platform() == "unix-systemv" {
|
||||||
|
return unixSystemVServiceStatus()
|
||||||
|
}
|
||||||
|
return status, err
|
||||||
|
}
|
||||||
|
|
||||||
|
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.TrimSpace(out)) {
|
||||||
|
case "running":
|
||||||
|
return service.StatusRunning, nil
|
||||||
|
default:
|
||||||
|
return service.StatusStopped, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,41 +2,58 @@ package ctrld
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"golang.org/x/sync/singleflight"
|
"golang.org/x/sync/singleflight"
|
||||||
|
"tailscale.com/logtail/backoff"
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/dnsrcode"
|
"github.com/Control-D-Inc/ctrld/internal/dnsrcode"
|
||||||
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SetConfigName set the config name that ctrld will look for.
|
const (
|
||||||
func SetConfigName(v *viper.Viper, name string) {
|
IpStackBoth = "both"
|
||||||
v.SetConfigName(name)
|
IpStackV4 = "v4"
|
||||||
|
IpStackV6 = "v6"
|
||||||
|
IpStackSplit = "split"
|
||||||
|
)
|
||||||
|
|
||||||
|
var controldParentDomains = []string{"controld.com", "controld.net", "controld.dev"}
|
||||||
|
|
||||||
|
// SetConfigName set the config name that ctrld will look for.
|
||||||
|
// DEPRECATED: use SetConfigNameWithPath instead.
|
||||||
|
func SetConfigName(v *viper.Viper, name string) {
|
||||||
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",
|
||||||
@@ -75,6 +92,17 @@ type Config struct {
|
|||||||
Upstream map[string]*UpstreamConfig `mapstructure:"upstream" toml:"upstream" validate:"min=1,dive"`
|
Upstream map[string]*UpstreamConfig `mapstructure:"upstream" toml:"upstream" 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
|
||||||
|
}
|
||||||
|
|
||||||
// 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"`
|
||||||
@@ -96,24 +124,36 @@ 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" validate:"required_unless=Type os"`
|
||||||
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 http.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
|
g singleflight.Group
|
||||||
bootstrapIPs []string
|
mu sync.Mutex
|
||||||
nextBootstrapIP atomic.Uint32
|
bootstrapIPs []string
|
||||||
|
bootstrapIPs4 []string
|
||||||
|
bootstrapIPs6 []string
|
||||||
|
transport *http.Transport
|
||||||
|
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"`
|
||||||
}
|
}
|
||||||
@@ -136,87 +176,87 @@ 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 == "" {
|
||||||
return
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !strings.Contains(uc.Endpoint, ":") {
|
// UpstreamSendClientInfo reports whether the upstream is
|
||||||
uc.Domain = uc.Endpoint
|
// configured to send client info to Control D DNS server.
|
||||||
uc.Endpoint = net.JoinHostPort(uc.Endpoint, defaultPortFor(uc.Type))
|
//
|
||||||
|
// Client info includes:
|
||||||
|
// - MAC
|
||||||
|
// - Lan IP
|
||||||
|
// - Hostname
|
||||||
|
func (uc *UpstreamConfig) UpstreamSendClientInfo() bool {
|
||||||
|
if uc.SendClientInfo != nil && !(*uc.SendClientInfo) {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
host, _, _ := net.SplitHostPort(uc.Endpoint)
|
if uc.SendClientInfo == nil {
|
||||||
uc.Domain = host
|
return true
|
||||||
if net.ParseIP(uc.Domain) != nil {
|
|
||||||
uc.BootstrapIP = uc.Domain
|
|
||||||
}
|
}
|
||||||
|
switch uc.Type {
|
||||||
|
case ResolverTypeDOH, ResolverTypeDOH3:
|
||||||
|
if uc.isControlD() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
// SetupBootstrapIP manually find all available IPs of the upstream.
|
||||||
// The first usable IP will be used as bootstrap IP of the upstream.
|
// The first usable IP will be used as bootstrap IP of the upstream.
|
||||||
func (uc *UpstreamConfig) SetupBootstrapIP() {
|
func (uc *UpstreamConfig) SetupBootstrapIP() {
|
||||||
bootstrapIP := func(record dns.RR) string {
|
uc.setupBootstrapIP(true)
|
||||||
switch ar := record.(type) {
|
}
|
||||||
case *dns.A:
|
|
||||||
return ar.A.String()
|
// SetupBootstrapIP manually find all available IPs of the upstream.
|
||||||
case *dns.AAAA:
|
// The first usable IP will be used as bootstrap IP of the upstream.
|
||||||
return ar.AAAA.String()
|
func (uc *UpstreamConfig) setupBootstrapIP(withBootstrapDNS bool) {
|
||||||
|
b := backoff.NewBackoff("setupBootstrapIP", func(format string, args ...any) {}, 2*time.Second)
|
||||||
|
for {
|
||||||
|
uc.bootstrapIPs = lookupIP(uc.Domain, uc.Timeout, withBootstrapDNS)
|
||||||
|
if len(uc.bootstrapIPs) > 0 {
|
||||||
|
break
|
||||||
}
|
}
|
||||||
return ""
|
ProxyLog.Warn().Msg("could not resolve bootstrap IPs, retrying...")
|
||||||
|
b.BackOff(context.Background(), errors.New("no bootstrap IPs"))
|
||||||
}
|
}
|
||||||
|
for _, ip := range uc.bootstrapIPs {
|
||||||
resolver := &osResolver{nameservers: availableNameservers()}
|
if ctrldnet.IsIPv6(ip) {
|
||||||
resolver.nameservers = append([]string{net.JoinHostPort(bootstrapDNS, "53")}, resolver.nameservers...)
|
uc.bootstrapIPs6 = append(uc.bootstrapIPs6, ip)
|
||||||
ProxyLog.Debug().Msgf("Resolving %q using bootstrap DNS %q", uc.Domain, resolver.nameservers)
|
} else {
|
||||||
timeoutMs := 2000
|
uc.bootstrapIPs4 = append(uc.bootstrapIPs4, ip)
|
||||||
if uc.Timeout > 0 && uc.Timeout < timeoutMs {
|
|
||||||
timeoutMs = uc.Timeout
|
|
||||||
}
|
|
||||||
do := func(dnsType uint16) {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutMs)*time.Millisecond)
|
|
||||||
defer cancel()
|
|
||||||
m := new(dns.Msg)
|
|
||||||
m.SetQuestion(uc.Domain+".", dnsType)
|
|
||||||
m.RecursionDesired = true
|
|
||||||
|
|
||||||
r, err := resolver.Resolve(ctx, m)
|
|
||||||
if err != nil {
|
|
||||||
ProxyLog.Error().Err(err).Str("type", dns.TypeToString[dnsType]).Msgf("could not resolve domain %s for upstream", uc.Domain)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if r.Rcode != dns.RcodeSuccess {
|
|
||||||
ProxyLog.Error().Msgf("could not resolve domain return code: %d, upstream", r.Rcode)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(r.Answer) == 0 {
|
|
||||||
ProxyLog.Error().Msg("no answer from bootstrap DNS server")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, a := range r.Answer {
|
|
||||||
ip := bootstrapIP(a)
|
|
||||||
if ip == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Storing the ip to uc.bootstrapIPs list, so it can be selected later
|
|
||||||
// when retrying failed request due to network stack changed.
|
|
||||||
uc.bootstrapIPs = append(uc.bootstrapIPs, ip)
|
|
||||||
if uc.BootstrapIP == "" {
|
|
||||||
// Remember what's the current IP in bootstrap IPs list,
|
|
||||||
// so we can select next one upon re-bootstrapping.
|
|
||||||
uc.nextBootstrapIP.Add(1)
|
|
||||||
|
|
||||||
// If this is an ipv6, and ipv6 is not available, don't use it as bootstrap ip.
|
|
||||||
if !ctrldnet.SupportsIPv6() && ctrldnet.IsIPv6(ip) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
uc.BootstrapIP = ip
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Find all A, AAAA records of the upstream.
|
|
||||||
for _, dnsType := range []uint16{dns.TypeAAAA, dns.TypeA} {
|
|
||||||
do(dnsType)
|
|
||||||
}
|
}
|
||||||
ProxyLog.Debug().Msgf("Bootstrap IPs: %v", uc.bootstrapIPs)
|
ProxyLog.Debug().Msgf("Bootstrap IPs: %v", uc.bootstrapIPs)
|
||||||
}
|
}
|
||||||
@@ -228,30 +268,8 @@ func (uc *UpstreamConfig) ReBootstrap() {
|
|||||||
default:
|
default:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, _, _ = uc.g.Do("rebootstrap", func() (any, error) {
|
_, _, _ = uc.g.Do("ReBootstrap", func() (any, error) {
|
||||||
ProxyLog.Debug().Msg("re-bootstrapping upstream ip")
|
ProxyLog.Debug().Msg("re-bootstrapping upstream ip")
|
||||||
n := uint32(len(uc.bootstrapIPs))
|
|
||||||
|
|
||||||
timeoutMs := 1000
|
|
||||||
if uc.Timeout > 0 && uc.Timeout < timeoutMs {
|
|
||||||
timeoutMs = uc.Timeout
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutMs)*time.Millisecond)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
hasIPv6 := ctrldnet.IPv6Available(ctx)
|
|
||||||
// Only attempt n times, because if there's no usable ip,
|
|
||||||
// the bootstrap ip will be kept as-is.
|
|
||||||
for i := uint32(0); i < n; i++ {
|
|
||||||
// Select the next ip in bootstrap ip list.
|
|
||||||
next := uc.nextBootstrapIP.Add(1)
|
|
||||||
ip := uc.bootstrapIPs[(next-1)%n]
|
|
||||||
if !hasIPv6 && ctrldnet.IsIPv6(ip) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
uc.BootstrapIP = ip
|
|
||||||
break
|
|
||||||
}
|
|
||||||
uc.setupTransportWithoutPingUpstream()
|
uc.setupTransportWithoutPingUpstream()
|
||||||
return true, nil
|
return true, nil
|
||||||
})
|
})
|
||||||
@@ -279,31 +297,65 @@ func (uc *UpstreamConfig) SetupTransport() {
|
|||||||
|
|
||||||
func (uc *UpstreamConfig) setupDOHTransport() {
|
func (uc *UpstreamConfig) setupDOHTransport() {
|
||||||
uc.setupDOHTransportWithoutPingUpstream()
|
uc.setupDOHTransportWithoutPingUpstream()
|
||||||
uc.pingUpstream()
|
go uc.pingUpstream()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uc *UpstreamConfig) setupDOHTransportWithoutPingUpstream() {
|
func (uc *UpstreamConfig) newDOHTransport(addrs []string) *http.Transport {
|
||||||
uc.transport = http.DefaultTransport.(*http.Transport).Clone()
|
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||||
uc.transport.IdleConnTimeout = 5 * time.Second
|
transport.IdleConnTimeout = 5 * time.Second
|
||||||
|
transport.TLSClientConfig = &tls.Config{RootCAs: uc.certPool}
|
||||||
|
|
||||||
dialerTimeoutMs := 2000
|
dialerTimeoutMs := 2000
|
||||||
if uc.Timeout > 0 && uc.Timeout < dialerTimeoutMs {
|
if uc.Timeout > 0 && uc.Timeout < dialerTimeoutMs {
|
||||||
dialerTimeoutMs = uc.Timeout
|
dialerTimeoutMs = uc.Timeout
|
||||||
}
|
}
|
||||||
dialerTimeout := time.Duration(dialerTimeoutMs) * time.Millisecond
|
dialerTimeout := time.Duration(dialerTimeoutMs) * time.Millisecond
|
||||||
uc.transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
dialer := &net.Dialer{
|
_, port, _ := net.SplitHostPort(addr)
|
||||||
Timeout: dialerTimeout,
|
|
||||||
KeepAlive: dialerTimeout,
|
|
||||||
}
|
|
||||||
// if we have a bootstrap ip set, use it to avoid DNS lookup
|
|
||||||
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, ProxyLog.Debug(), "sending doh request to: %s", addr)
|
||||||
|
return dialer.DialContext(ctx, network, addr)
|
||||||
}
|
}
|
||||||
Log(ctx, ProxyLog.Debug(), "sending doh request to: %s", addr)
|
pd := &ctrldnet.ParallelDialer{}
|
||||||
return dialer.DialContext(ctx, network, addr)
|
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 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
Log(ctx, ProxyLog.Debug(), "sending doh request to: %s", conn.RemoteAddr())
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
return transport
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uc *UpstreamConfig) setupDOHTransportWithoutPingUpstream() {
|
||||||
|
uc.mu.Lock()
|
||||||
|
defer uc.mu.Unlock()
|
||||||
|
switch uc.IPStack {
|
||||||
|
case IpStackBoth, "":
|
||||||
|
uc.transport = uc.newDOHTransport(uc.bootstrapIPs)
|
||||||
|
case IpStackV4:
|
||||||
|
uc.transport = uc.newDOHTransport(uc.bootstrapIPs4)
|
||||||
|
case IpStackV6:
|
||||||
|
uc.transport = uc.newDOHTransport(uc.bootstrapIPs6)
|
||||||
|
case IpStackSplit:
|
||||||
|
uc.transport4 = uc.newDOHTransport(uc.bootstrapIPs4)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if ctrldnet.IPv6Available(ctx) {
|
||||||
|
uc.transport6 = uc.newDOHTransport(uc.bootstrapIPs6)
|
||||||
|
} else {
|
||||||
|
uc.transport6 = uc.transport4
|
||||||
|
}
|
||||||
|
|
||||||
|
uc.transport = uc.newDOHTransport(uc.bootstrapIPs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,6 +374,76 @@ func (uc *UpstreamConfig) pingUpstream() {
|
|||||||
_, _ = dnsResolver.Resolve(ctx, msg)
|
_, _ = dnsResolver.Resolve(ctx, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.mu.Lock()
|
||||||
|
defer uc.mu.Unlock()
|
||||||
|
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:
|
||||||
|
return pick(uc.bootstrapIPs6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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:
|
||||||
|
return "tcp6-tls", "udp6"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "tcp-tls", "udp"
|
||||||
|
}
|
||||||
|
|
||||||
// Init initialized necessary values for an ListenerConfig.
|
// Init initialized necessary values for an ListenerConfig.
|
||||||
func (lc *ListenerConfig) Init() {
|
func (lc *ListenerConfig) Init() {
|
||||||
if lc.Policy != nil {
|
if lc.Policy != nil {
|
||||||
@@ -335,6 +457,8 @@ 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)
|
||||||
return validate.Struct(cfg)
|
return validate.Struct(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,6 +466,23 @@ 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 defaultPortFor(typ string) string {
|
func defaultPortFor(typ string) string {
|
||||||
switch typ {
|
switch typ {
|
||||||
case ResolverTypeDOH, ResolverTypeDOH3:
|
case ResolverTypeDOH, ResolverTypeDOH3:
|
||||||
@@ -354,17 +495,30 @@ func defaultPortFor(typ string) string {
|
|||||||
return "53"
|
return "53"
|
||||||
}
|
}
|
||||||
|
|
||||||
func availableNameservers() []string {
|
// ResolverTypeFromEndpoint tries guessing the resolver type with a given endpoint
|
||||||
nss := nameservers()
|
// using following rules:
|
||||||
n := 0
|
//
|
||||||
for _, ns := range nss {
|
// - If endpoint is an IP address -> ResolverTypeLegacy
|
||||||
ip, _, _ := net.SplitHostPort(ns)
|
// - If endpoint starts with "https://" -> ResolverTypeDOH
|
||||||
// skipping invalid entry or ipv6 nameserver if ipv6 not available.
|
// - If endpoint starts with "quic://" -> ResolverTypeDOQ
|
||||||
if ip == "" || (ctrldnet.IsIPv6(ip) && !ctrldnet.SupportsIPv6()) {
|
// - For anything else -> ResolverTypeDOT
|
||||||
continue
|
func ResolverTypeFromEndpoint(endpoint string) string {
|
||||||
}
|
switch {
|
||||||
nss[n] = ns
|
case strings.HasPrefix(endpoint, "https://"):
|
||||||
n++
|
return ResolverTypeDOH
|
||||||
|
case strings.HasPrefix(endpoint, "quic://"):
|
||||||
|
return ResolverTypeDOQ
|
||||||
}
|
}
|
||||||
return nss[:n]
|
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,195 @@
|
|||||||
|
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 ptrBool(b bool) *bool {
|
||||||
|
return &b
|
||||||
|
}
|
||||||
+132
-20
@@ -5,40 +5,152 @@ package ctrld
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
"github.com/quic-go/quic-go"
|
"github.com/quic-go/quic-go"
|
||||||
"github.com/quic-go/quic-go/http3"
|
"github.com/quic-go/quic-go/http3"
|
||||||
|
|
||||||
|
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (uc *UpstreamConfig) setupDOH3Transport() {
|
func (uc *UpstreamConfig) setupDOH3Transport() {
|
||||||
uc.setupDOH3TransportWithoutPingUpstream()
|
uc.setupDOH3TransportWithoutPingUpstream()
|
||||||
uc.pingUpstream()
|
go uc.pingUpstream()
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
ProxyLog.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
|
||||||
|
}
|
||||||
|
ProxyLog.Debug().Msgf("sending doh3 request to: %s", conn.RemoteAddr())
|
||||||
|
return conn, err
|
||||||
|
}
|
||||||
|
return rt
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uc *UpstreamConfig) setupDOH3TransportWithoutPingUpstream() {
|
func (uc *UpstreamConfig) setupDOH3TransportWithoutPingUpstream() {
|
||||||
rt := &http3.RoundTripper{}
|
uc.mu.Lock()
|
||||||
rt.Dial = func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
defer uc.mu.Unlock()
|
||||||
host := addr
|
switch uc.IPStack {
|
||||||
ProxyLog.Debug().Msgf("debug dial context D0H3 %s - %s", addr, bootstrapDNS)
|
case IpStackBoth, "":
|
||||||
// if we have a bootstrap ip set, use it to avoid DNS lookup
|
uc.http3RoundTripper = uc.newDOH3Transport(uc.bootstrapIPs)
|
||||||
if uc.BootstrapIP != "" {
|
case IpStackV4:
|
||||||
if _, port, _ := net.SplitHostPort(addr); port != "" {
|
uc.http3RoundTripper = uc.newDOH3Transport(uc.bootstrapIPs4)
|
||||||
addr = net.JoinHostPort(uc.BootstrapIP, port)
|
case IpStackV6:
|
||||||
}
|
uc.http3RoundTripper = uc.newDOH3Transport(uc.bootstrapIPs6)
|
||||||
ProxyLog.Debug().Msgf("sending doh3 request to: %s", addr)
|
case IpStackSplit:
|
||||||
}
|
uc.http3RoundTripper4 = uc.newDOH3Transport(uc.bootstrapIPs4)
|
||||||
remoteAddr, err := net.ResolveUDPAddr("udp", addr)
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
if err != nil {
|
defer cancel()
|
||||||
return nil, err
|
if ctrldnet.IPv6Available(ctx) {
|
||||||
|
uc.http3RoundTripper6 = uc.newDOH3Transport(uc.bootstrapIPs6)
|
||||||
|
} else {
|
||||||
|
uc.http3RoundTripper6 = uc.http3RoundTripper4
|
||||||
}
|
}
|
||||||
|
uc.http3RoundTripper = uc.newDOH3Transport(uc.bootstrapIPs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
udpConn, err := net.ListenUDP("udp", nil)
|
func (uc *UpstreamConfig) doh3Transport(dnsType uint16) http.RoundTripper {
|
||||||
if err != nil {
|
uc.mu.Lock()
|
||||||
return nil, err
|
defer uc.mu.Unlock()
|
||||||
|
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 quic.DialEarlyContext(ctx, udpConn, remoteAddr, host, tlsCfg, cfg)
|
}
|
||||||
|
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{}
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
uc.http3RoundTripper = rt
|
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...)
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
package ctrld
|
package ctrld
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
func (uc *UpstreamConfig) setupDOH3Transport() {}
|
func (uc *UpstreamConfig) setupDOH3Transport() {}
|
||||||
|
|
||||||
func (uc *UpstreamConfig) setupDOH3TransportWithoutPingUpstream() {}
|
func (uc *UpstreamConfig) setupDOH3TransportWithoutPingUpstream() {}
|
||||||
|
func (uc *UpstreamConfig) doh3Transport(dnsType uint16) http.RoundTripper { return nil }
|
||||||
|
|||||||
+13
-115
@@ -24,10 +24,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 +44,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) {
|
||||||
@@ -61,6 +65,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},
|
||||||
@@ -130,9 +135,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,116 +176,3 @@ func configWithInvalidRcodes(t *testing.T) *ctrld.Config {
|
|||||||
}
|
}
|
||||||
return cfg
|
return cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUpstreamConfig_Init(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
uc *ctrld.UpstreamConfig
|
|
||||||
expected *ctrld.UpstreamConfig
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
"doh+doh3",
|
|
||||||
&ctrld.UpstreamConfig{
|
|
||||||
Name: "doh",
|
|
||||||
Type: "doh",
|
|
||||||
Endpoint: "https://example.com",
|
|
||||||
BootstrapIP: "",
|
|
||||||
Domain: "",
|
|
||||||
Timeout: 0,
|
|
||||||
},
|
|
||||||
&ctrld.UpstreamConfig{
|
|
||||||
Name: "doh",
|
|
||||||
Type: "doh",
|
|
||||||
Endpoint: "https://example.com",
|
|
||||||
BootstrapIP: "",
|
|
||||||
Domain: "example.com",
|
|
||||||
Timeout: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dot+doq",
|
|
||||||
&ctrld.UpstreamConfig{
|
|
||||||
Name: "dot",
|
|
||||||
Type: "dot",
|
|
||||||
Endpoint: "freedns.controld.com:8853",
|
|
||||||
BootstrapIP: "",
|
|
||||||
Domain: "",
|
|
||||||
Timeout: 0,
|
|
||||||
},
|
|
||||||
&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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+22
-6
@@ -227,9 +227,27 @@ Value `0` means no timeout.
|
|||||||
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.
|
||||||
|
|
||||||
@@ -271,16 +289,14 @@ 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
|
||||||
- Required: yes
|
|
||||||
|
|
||||||
### 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
|
|
||||||
|
|
||||||
### 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`.
|
||||||
|
|||||||
@@ -7,25 +7,36 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
"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 http.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,26 +44,34 @@ 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 {
|
||||||
if r.http3RoundTripper == nil {
|
transport := r.uc.doh3Transport(dnsTyp)
|
||||||
|
if transport == nil {
|
||||||
return nil, errors.New("DoH3 is not supported")
|
return nil, errors.New("DoH3 is not supported")
|
||||||
}
|
}
|
||||||
c.Transport = r.http3RoundTripper
|
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 {
|
||||||
if closer, ok := r.http3RoundTripper.(io.Closer); ok {
|
if closer, ok := c.Transport.(io.Closer); ok {
|
||||||
closer.Close()
|
closer.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,5 +89,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, ProxyLog.Debug().Interface("header", req.Header), "sending request header")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,18 +14,25 @@ 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
|
||||||
_, port, _ := net.SplitHostPort(endpoint)
|
_, port, _ := net.SplitHostPort(endpoint)
|
||||||
endpoint = net.JoinHostPort(r.uc.BootstrapIP, port)
|
endpoint = net.JoinHostPort(r.uc.BootstrapIP, port)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
package ctrld
|
|
||||||
|
|
||||||
// TODO(cuonglm): use stdlib once we bump minimum version to 1.20
|
|
||||||
|
|
||||||
func joinErrors(errs ...error) error {
|
|
||||||
n := 0
|
|
||||||
for _, err := range errs {
|
|
||||||
if err != nil {
|
|
||||||
n++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if n == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
e := &joinError{
|
|
||||||
errs: make([]error, 0, n),
|
|
||||||
}
|
|
||||||
for _, err := range errs {
|
|
||||||
if err != nil {
|
|
||||||
e.errs = append(e.errs, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
|
|
||||||
type joinError struct {
|
|
||||||
errs []error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *joinError) Error() string {
|
|
||||||
var b []byte
|
|
||||||
for i, err := range e.errs {
|
|
||||||
if i > 0 {
|
|
||||||
b = append(b, '\n')
|
|
||||||
}
|
|
||||||
b = append(b, err.Error()...)
|
|
||||||
}
|
|
||||||
return string(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *joinError) Unwrap() []error {
|
|
||||||
return e.errs
|
|
||||||
}
|
|
||||||
@@ -1,16 +1,18 @@
|
|||||||
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/cuonglm/osinfo v0.0.0-20230329055532-c513f836da19
|
||||||
github.com/frankban/quicktest v1.14.3
|
github.com/frankban/quicktest v1.14.3
|
||||||
github.com/fsnotify/fsnotify v1.6.0
|
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.0.6
|
||||||
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-20221215072855-de60144f33f8
|
||||||
|
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/miekg/dns v1.1.50
|
github.com/miekg/dns v1.1.50
|
||||||
github.com/pelletier/go-toml/v2 v2.0.6
|
github.com/pelletier/go-toml/v2 v2.0.6
|
||||||
@@ -19,10 +21,12 @@ require (
|
|||||||
github.com/spf13/cobra v1.4.0
|
github.com/spf13/cobra v1.4.0
|
||||||
github.com/spf13/viper v1.14.0
|
github.com/spf13/viper v1.14.0
|
||||||
github.com/stretchr/testify v1.8.1
|
github.com/stretchr/testify v1.8.1
|
||||||
|
github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54
|
||||||
|
golang.org/x/net v0.7.0
|
||||||
golang.org/x/sync v0.1.0
|
golang.org/x/sync v0.1.0
|
||||||
golang.org/x/sys v0.5.0
|
golang.org/x/sys v0.5.0
|
||||||
golang.zx2c4.com/wireguard/windows v0.5.3
|
golang.zx2c4.com/wireguard/windows v0.5.3
|
||||||
tailscale.com v1.34.1
|
tailscale.com v1.38.3
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -36,7 +40,6 @@ require (
|
|||||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // 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.0.0 // indirect
|
||||||
github.com/josharian/native v1.0.0 // indirect
|
|
||||||
github.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b // indirect
|
github.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b // indirect
|
||||||
github.com/kr/pretty v0.3.0 // 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
|
||||||
@@ -45,9 +48,9 @@ require (
|
|||||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.14 // 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.1 // 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.0 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/onsi/ginkgo/v2 v2.2.0 // indirect
|
github.com/onsi/ginkgo/v2 v2.2.0 // indirect
|
||||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||||
@@ -62,15 +65,19 @@ require (
|
|||||||
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/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/subosito/gotenv v1.4.1 // indirect
|
github.com/subosito/gotenv v1.4.1 // indirect
|
||||||
github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4 // indirect
|
github.com/u-root/uio v0.0.0-20221213070652-c3537552635f // indirect
|
||||||
|
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect
|
||||||
go4.org/mem v0.0.0-20210711025021-927187094b94 // indirect
|
go4.org/mem v0.0.0-20210711025021-927187094b94 // indirect
|
||||||
golang.org/x/crypto v0.4.0 // indirect
|
golang.org/x/crypto v0.6.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
|
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
|
||||||
golang.org/x/mod v0.6.0 // indirect
|
golang.org/x/mod v0.7.0 // indirect
|
||||||
golang.org/x/net v0.7.0 // indirect
|
|
||||||
golang.org/x/text v0.7.0 // indirect
|
golang.org/x/text v0.7.0 // indirect
|
||||||
golang.org/x/tools v0.2.0 // indirect
|
golang.org/x/tools v0.4.1-0.20221208213631-3f74d914ae6d // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // 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,6 +38,8 @@ 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=
|
||||||
@@ -50,10 +52,12 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
|
|||||||
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.1/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=
|
||||||
@@ -163,10 +167,12 @@ github.com/illarion/gonotify v1.0.1 h1:F1d+0Fgbq/sDWjj/r66ekjDG+IDeecQKUFH4wNwso
|
|||||||
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.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20211209223715-7d93572ebe8e h1:IQpunlq7T+NiJJMO7ODYV2YWBiv/KnObR3gofX0mWOo=
|
github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8 h1:Z72DOke2yOK0Ms4Z2LK1E1OrRJXOxSj5DllTz2FYTRg=
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20211209223715-7d93572ebe8e/go.mod h1:h+MxyHxRg9NH3terB1nfRIUaQEcI0XOVkdR9LNBlp8E=
|
github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8/go.mod h1:m5WMe03WCvWcXjRnhvaAbAAXdCnu20J5P+mmH44ZzpE=
|
||||||
github.com/josharian/native v1.0.0 h1:Ts/E8zCSEsG17dUqv7joXJFybuMLjQfWE04tsBODTxk=
|
|
||||||
github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||||
|
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||||
|
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 h1:elKwZS1OcdQ0WwEDBeqxKwb7WB62QX8bvZ/FJnVXIfk=
|
||||||
|
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86/go.mod h1:aFAMtuldEgx/4q7iSGazk22+IcgvtiC+HIimFO9XlS8=
|
||||||
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
|
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
|
||||||
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
|
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
|
||||||
github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
|
github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
|
||||||
@@ -202,14 +208,15 @@ github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE
|
|||||||
github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
|
github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
|
||||||
github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY=
|
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.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/netlink v1.6.0/go.mod h1:0o3PlBmGst1xve7wQ7j/hwpNaFaH4qCRyWCdcZk8/vA=
|
||||||
|
github.com/mdlayher/netlink v1.7.1 h1:FdUaT/e33HjEXagwELR8R3/KL1Fq5x3G5jgHLp/BTmg=
|
||||||
|
github.com/mdlayher/netlink v1.7.1/go.mod h1:nKO5CSjE/DJjVhk/TNp6vCE1ktVxEA8VEh8drhZzxsQ=
|
||||||
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.1.1/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5Awbj+qDs=
|
||||||
github.com/mdlayher/socket v0.2.3 h1:XZA2X2TjdOwNoNPVPclRCURoX/hokBY8nkTmRZFEheM=
|
github.com/mdlayher/socket v0.4.0 h1:280wsy40IC9M9q1uPGcLBwXpcTQDtoGwVt+BNoITxIw=
|
||||||
github.com/mdlayher/socket v0.2.3/go.mod h1:bz12/FozYNH/VbvC3q7TRIK/Y6dH1kCKsXaUeXi/FmY=
|
github.com/mdlayher/socket v0.4.0/go.mod h1:xxFqz5GRCUN3UEOm9CZqEJsAbe1C8OwSK46NlmWuVoc=
|
||||||
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
|
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
|
||||||
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
|
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=
|
||||||
@@ -242,9 +249,7 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE
|
|||||||
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.8.1-0.20211023094830-115ce09fd6b4 h1:Ha8xCaq6ln1a+R91Km45Oq6lPXj2Mla6CRJYcuV2h1w=
|
||||||
github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
|
github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
|
||||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY=
|
|
||||||
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/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
@@ -275,9 +280,13 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs
|
|||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
|
github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
|
||||||
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
github.com/subosito/gotenv v1.4.1/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-20221213070652-c3537552635f h1:dpx1PHxYqAnXzbryJrWP1NQLzEjwcVgFLhkknuFQ7ww=
|
||||||
github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4 h1:hl6sK6aFgTLISijk6xIzeqnPzQcsLqqvL6vEfTPinME=
|
github.com/u-root/uio v0.0.0-20221213070652-c3537552635f/go.mod h1:IogEAUBXDEwX7oR/BMmCctShYs80ql4hF0ySdzGxf7E=
|
||||||
github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA=
|
github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54 h1:8mhqcHPqTMhSPoslhGYihEgSfc77+7La1P6kiB6+9So=
|
||||||
|
github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54/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.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg=
|
||||||
|
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
||||||
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=
|
||||||
@@ -299,8 +308,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
|||||||
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-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.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
|
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
|
||||||
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
|
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||||
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=
|
||||||
@@ -337,8 +346,8 @@ 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 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I=
|
golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
|
||||||
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
|
golang.org/x/mod v0.7.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-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@@ -425,6 +434,7 @@ golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
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=
|
||||||
@@ -433,6 +443,7 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
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-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=
|
||||||
@@ -448,7 +459,6 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
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=
|
||||||
@@ -456,7 +466,9 @@ golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
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-20211216021012-1d35b9e2eb4e/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-20220128215802-99c3d69c2c27/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-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.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/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=
|
||||||
@@ -524,8 +536,8 @@ golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4f
|
|||||||
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.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE=
|
golang.org/x/tools v0.4.1-0.20221208213631-3f74d914ae6d h1:9ZNWAi4CYhNv60mXGgAncgq7SGc5qa7C8VZV8Tg7Ggs=
|
||||||
golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
|
golang.org/x/tools v0.4.1-0.20221208213631-3f74d914ae6d/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
|
||||||
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=
|
||||||
@@ -645,5 +657,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.38.3 h1:2aX3+u0Re8QcN6nq7zf9Aa4ZCR2Nf6Imv3isqdQrb58=
|
||||||
tailscale.com v1.34.1/go.mod h1:ZsBP7rjzzB2rp+UCOumr9DAe0EQ6OPivwSXcz/BrekQ=
|
tailscale.com v1.38.3/go.mod h1:UWLQxcd8dz+lds2I+HpfXSruHrvXM1j4zd4zdx86t7w=
|
||||||
|
|||||||
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,23 +3,31 @@ package controld
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"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"
|
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
||||||
|
"github.com/Control-D-Inc/ctrld/internal/router"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
apiDomain = "api.controld.com"
|
||||||
resolverDataURL = "https://api.controld.com/utility"
|
resolverDataURL = "https://api.controld.com/utility"
|
||||||
InvalidConfigCode = 40401
|
InvalidConfigCode = 40401
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +54,7 @@ type utilityRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FetchResolverConfig fetch Control D config for given uid.
|
// FetchResolverConfig fetch Control D config for given uid.
|
||||||
func FetchResolverConfig(uid string) (*ResolverConfig, error) {
|
func FetchResolverConfig(uid, version string) (*ResolverConfig, error) {
|
||||||
body, _ := json.Marshal(utilityRequest{UID: uid})
|
body, _ := json.Marshal(utilityRequest{UID: uid})
|
||||||
req, err := http.NewRequest("POST", resolverDataURL, bytes.NewReader(body))
|
req, err := http.NewRequest("POST", resolverDataURL, bytes.NewReader(body))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -54,17 +62,28 @@ func FetchResolverConfig(uid string) (*ResolverConfig, error) {
|
|||||||
}
|
}
|
||||||
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) {
|
||||||
// We experiment hanging in TLS handshake when connecting to ControlD API
|
ips := ctrld.LookupIP(apiDomain)
|
||||||
// with ipv6. So prefer ipv4 if available.
|
if len(ips) == 0 {
|
||||||
proto := "tcp6"
|
ctrld.ProxyLog.Warn().Msgf("No IPs found for %s, connecting to %s", apiDomain, addr)
|
||||||
if ctrldnet.SupportsIPv4() {
|
return ctrldnet.Dialer.DialContext(ctx, network, addr)
|
||||||
proto = "tcp4"
|
|
||||||
}
|
}
|
||||||
return ctrldnet.Dialer.DialContext(ctx, proto, addr)
|
ctrld.ProxyLog.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() == router.DDWrt {
|
||||||
|
transport.TLSClientConfig = &tls.Config{RootCAs: certs.CACertPool()}
|
||||||
}
|
}
|
||||||
client := http.Client{
|
client := http.Client{
|
||||||
Timeout: 10 * time.Second,
|
Timeout: 10 * time.Second,
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
const utilityURL = "https://api.controld.com/utility"
|
|
||||||
|
|
||||||
func TestFetchResolverConfig(t *testing.T) {
|
func TestFetchResolverConfig(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -24,7 +22,7 @@ func TestFetchResolverConfig(t *testing.T) {
|
|||||||
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)
|
got, err := FetchResolverConfig(tc.uid, "dev-test")
|
||||||
require.False(t, (err != nil) != tc.wantErr, err)
|
require.False(t, (err != nil) != tc.wantErr, err)
|
||||||
if !tc.wantErr {
|
if !tc.wantErr {
|
||||||
assert.NotEmpty(t, got.DOH)
|
assert.NotEmpty(t, got.DOH)
|
||||||
|
|||||||
+2
-2
@@ -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 (
|
||||||
@@ -131,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[:])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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?!
|
||||||
|
|||||||
+47
-22
@@ -2,6 +2,7 @@ package net
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -12,7 +13,6 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
controldIPv6Test = "ipv6.controld.io"
|
controldIPv6Test = "ipv6.controld.io"
|
||||||
controldIPv4Test = "ipv4.controld.io"
|
|
||||||
bootstrapDNS = "76.76.2.0:53"
|
bootstrapDNS = "76.76.2.0:53"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -37,8 +37,6 @@ var probeStackDialer = &net.Dialer{
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
stackOnce atomic.Pointer[sync.Once]
|
stackOnce atomic.Pointer[sync.Once]
|
||||||
ipv4Enabled bool
|
|
||||||
ipv6Enabled bool
|
|
||||||
canListenIPv6Local bool
|
canListenIPv6Local bool
|
||||||
hasNetworkUp bool
|
hasNetworkUp bool
|
||||||
)
|
)
|
||||||
@@ -47,13 +45,8 @@ func init() {
|
|||||||
stackOnce.Store(new(sync.Once))
|
stackOnce.Store(new(sync.Once))
|
||||||
}
|
}
|
||||||
|
|
||||||
func supportIPv4() bool {
|
|
||||||
_, err := probeStackDialer.Dial("tcp4", net.JoinHostPort(controldIPv4Test, "80"))
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func supportIPv6(ctx context.Context) bool {
|
func supportIPv6(ctx context.Context) bool {
|
||||||
_, err := probeStackDialer.DialContext(ctx, "tcp6", net.JoinHostPort(controldIPv6Test, "80"))
|
_, err := probeStackDialer.DialContext(ctx, "tcp6", net.JoinHostPort(controldIPv6Test, "443"))
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +59,7 @@ func supportListenIPv6Local() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func probeStack() {
|
func probeStack() {
|
||||||
b := backoff.NewBackoff("probeStack", func(format string, args ...any) {}, time.Minute)
|
b := backoff.NewBackoff("probeStack", func(format string, args ...any) {}, 5*time.Second)
|
||||||
for {
|
for {
|
||||||
if _, err := probeStackDialer.Dial("udp", bootstrapDNS); err == nil {
|
if _, err := probeStackDialer.Dial("udp", bootstrapDNS); err == nil {
|
||||||
hasNetworkUp = true
|
hasNetworkUp = true
|
||||||
@@ -75,8 +68,6 @@ func probeStack() {
|
|||||||
b.BackOff(context.Background(), err)
|
b.BackOff(context.Background(), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ipv4Enabled = supportIPv4()
|
|
||||||
ipv6Enabled = supportIPv6(context.Background())
|
|
||||||
canListenIPv6Local = supportListenIPv6Local()
|
canListenIPv6Local = supportListenIPv6Local()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,16 +76,6 @@ func Up() bool {
|
|||||||
return hasNetworkUp
|
return hasNetworkUp
|
||||||
}
|
}
|
||||||
|
|
||||||
func SupportsIPv4() bool {
|
|
||||||
stackOnce.Load().Do(probeStack)
|
|
||||||
return ipv4Enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
func SupportsIPv6() bool {
|
|
||||||
stackOnce.Load().Do(probeStack)
|
|
||||||
return ipv6Enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
func SupportsIPv6ListenLocal() bool {
|
func SupportsIPv6ListenLocal() bool {
|
||||||
stackOnce.Load().Do(probeStack)
|
stackOnce.Load().Do(probeStack)
|
||||||
return canListenIPv6Local
|
return canListenIPv6Local
|
||||||
@@ -112,3 +93,47 @@ func IsIPv6(ip string) bool {
|
|||||||
parsedIP := net.ParseIP(ip)
|
parsedIP := net.ParseIP(ip)
|
||||||
return parsedIP != nil && parsedIP.To4() == nil && parsedIP.To16() != nil
|
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()
|
||||||
|
|
||||||
|
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)
|
||||||
|
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,121 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
"tailscale.com/util/lineread"
|
||||||
|
|
||||||
|
"github.com/Control-D-Inc/ctrld"
|
||||||
|
)
|
||||||
|
|
||||||
|
var clientInfoFiles = []string{
|
||||||
|
"/tmp/dnsmasq.leases", // ddwrt
|
||||||
|
"/tmp/dhcp.leases", // openwrt
|
||||||
|
"/var/lib/misc/dnsmasq.leases", // merlin
|
||||||
|
"/mnt/data/udapi-config/dnsmasq.lease", // UDM Pro
|
||||||
|
"/data/udapi-config/dnsmasq.lease", // UDR
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *router) watchClientInfoTable() {
|
||||||
|
if r.watcher == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
timer := time.NewTicker(time.Minute * 5)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-timer.C:
|
||||||
|
for _, name := range r.watcher.WatchList() {
|
||||||
|
_ = readClientInfoFile(name)
|
||||||
|
}
|
||||||
|
case event, ok := <-r.watcher.Events:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if event.Has(fsnotify.Write) {
|
||||||
|
if err := readClientInfoFile(event.Name); err != nil && !os.IsNotExist(err) {
|
||||||
|
log.Println("could not read client info file:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case err, ok := <-r.watcher.Errors:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Println("error:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Stop() error {
|
||||||
|
if Name() == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
r := routerPlatform.Load()
|
||||||
|
if r.watcher != nil {
|
||||||
|
if err := r.watcher.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetClientInfoByMac(mac string) *ctrld.ClientInfo {
|
||||||
|
if mac == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_ = Name()
|
||||||
|
r := routerPlatform.Load()
|
||||||
|
val, ok := r.mac.Load(mac)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return val.(*ctrld.ClientInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readClientInfoFile(name string) error {
|
||||||
|
f, err := os.Open(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
return readClientInfoReader(f)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func readClientInfoReader(reader io.Reader) error {
|
||||||
|
r := routerPlatform.Load()
|
||||||
|
return lineread.Reader(reader, func(line []byte) error {
|
||||||
|
fields := bytes.Fields(line)
|
||||||
|
if len(fields) != 5 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
mac := string(fields[1])
|
||||||
|
if _, err := net.ParseMAC(mac); err != nil {
|
||||||
|
// The second field is not a mac, skip.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ip := normalizeIP(string(fields[2]))
|
||||||
|
if net.ParseIP(ip) == nil {
|
||||||
|
log.Printf("invalid ip address entry: %q", ip)
|
||||||
|
ip = ""
|
||||||
|
}
|
||||||
|
hostname := string(fields[3])
|
||||||
|
r.mac.Store(mac, &ctrld.ClientInfo{Mac: mac, IP: ip, Hostname: hostname})
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/Control-D-Inc/ctrld"
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_readClientInfoReader(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
in string
|
||||||
|
mac string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"good",
|
||||||
|
`1683329857 e6:20:59:b8:c1:6d 192.168.1.186 * 01:e6:20:59:b8:c1:6d
|
||||||
|
`,
|
||||||
|
"e6:20:59:b8:c1:6d",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bad seen on UDMdream machine",
|
||||||
|
`1683329857 e6:20:59:b8:c1:6e 192.168.1.111 * 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
|
||||||
|
`,
|
||||||
|
"e6:20:59:b8:c1:6e",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
r := routerPlatform.Load()
|
||||||
|
r.mac.Delete(tc.mac)
|
||||||
|
if err := readClientInfoReader(strings.NewReader(tc.in)); err != nil {
|
||||||
|
t.Errorf("readClientInfoReader() error = %v", err)
|
||||||
|
}
|
||||||
|
info, existed := r.mac.Load(tc.mac)
|
||||||
|
if !existed {
|
||||||
|
t.Error("client info missing")
|
||||||
|
}
|
||||||
|
if ci, ok := info.(*ctrld.ClientInfo); ok && existed && ci.Mac != tc.mac {
|
||||||
|
t.Errorf("mac mismatched, got: %q, want: %q", ci.Mac, tc.mac)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
nvramCtrldKeyPrefix = "ctrld_"
|
||||||
|
nvramCtrldSetupKey = "ctrld_setup"
|
||||||
|
nvramRCStartupKey = "rc_startup"
|
||||||
|
)
|
||||||
|
|
||||||
|
//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
|
||||||
|
`)
|
||||||
|
|
||||||
|
func setupDDWrt() error {
|
||||||
|
// Already setup.
|
||||||
|
if val, _ := nvram("get", nvramCtrldSetupKey); val == "1" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := dnsMasqConf()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
nvramKvMap := nvramKV()
|
||||||
|
nvramKvMap["dnsmasq_options"] = data
|
||||||
|
if err := nvramSetup(nvramKvMap); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart dnsmasq service.
|
||||||
|
if err := ddwrtRestartDNSMasq(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanupDDWrt() error {
|
||||||
|
// Restore old configs.
|
||||||
|
if err := nvramRestore(nvramKV()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Restart dnsmasq service.
|
||||||
|
if err := ddwrtRestartDNSMasq(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func postInstallDDWrt() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ddwrtRestartDNSMasq() 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("get", "enable_jffs2")
|
||||||
|
return out == "1"
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
const dnsMasqConfigContentTmpl = `# GENERATED BY ctrld - DO NOT MODIFY
|
||||||
|
no-resolv
|
||||||
|
server=127.0.0.1#5354
|
||||||
|
{{- if .SendClientInfo}}
|
||||||
|
add-mac
|
||||||
|
{{- end}}
|
||||||
|
`
|
||||||
|
|
||||||
|
const merlinDNSMasqPostConfPath = "/jffs/scripts/dnsmasq.postconf"
|
||||||
|
const merlinDNSMasqPostConfMarker = `# GENERATED BY ctrld - EOF`
|
||||||
|
|
||||||
|
const merlinDNSMasqPostConfTmpl = `# GENERATED BY ctrld - DO NOT MODIFY
|
||||||
|
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
config_file="$1"
|
||||||
|
. /usr/sbin/helper.sh
|
||||||
|
|
||||||
|
pid=$(cat /tmp/ctrld.pid 2>/dev/null)
|
||||||
|
if [ -n "$pid" ] && [ -f "/proc/${pid}/cmdline" ]; then
|
||||||
|
pc_delete "servers-file" "$config_file" # no WAN DNS settings
|
||||||
|
pc_append "no-resolv" "$config_file" # do not read /etc/resolv.conf
|
||||||
|
pc_append "server=127.0.0.1#5354" "$config_file" # use ctrld as upstream
|
||||||
|
{{- if .SendClientInfo}}
|
||||||
|
pc_append "add-mac" "$config_file" # add client mac
|
||||||
|
{{- end}}
|
||||||
|
pc_delete "dnssec" "$config_file" # disable DNSSEC
|
||||||
|
pc_delete "trust-anchor=" "$config_file" # disable DNSSEC
|
||||||
|
|
||||||
|
# For John fork
|
||||||
|
pc_delete "resolv-file" "$config_file" # no WAN DNS settings
|
||||||
|
|
||||||
|
# Change /etc/resolv.conf, which may be changed by WAN DNS setup
|
||||||
|
pc_delete "nameserver" /etc/resolv.conf
|
||||||
|
pc_append "nameserver 127.0.0.1" /etc/resolv.conf
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
`
|
||||||
|
|
||||||
|
func dnsMasqConf() (string, error) {
|
||||||
|
var sb strings.Builder
|
||||||
|
var tmplText string
|
||||||
|
switch Name() {
|
||||||
|
case DDWrt, OpenWrt, Ubios:
|
||||||
|
tmplText = dnsMasqConfigContentTmpl
|
||||||
|
case Merlin:
|
||||||
|
tmplText = merlinDNSMasqPostConfTmpl
|
||||||
|
}
|
||||||
|
tmpl := template.Must(template.New("").Parse(tmplText))
|
||||||
|
var to = &struct {
|
||||||
|
SendClientInfo bool
|
||||||
|
}{
|
||||||
|
routerPlatform.Load().sendClientInfo,
|
||||||
|
}
|
||||||
|
if err := tmpl.Execute(&sb, to); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return sb.String(), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupMerlin() error {
|
||||||
|
buf, err := os.ReadFile(merlinDNSMasqPostConfPath)
|
||||||
|
// Already setup.
|
||||||
|
if bytes.Contains(buf, []byte(merlinDNSMasqPostConfMarker)) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
merlinDNSMasqPostConf, err := dnsMasqConf()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data := strings.Join([]string{
|
||||||
|
merlinDNSMasqPostConf,
|
||||||
|
"\n",
|
||||||
|
merlinDNSMasqPostConfMarker,
|
||||||
|
"\n",
|
||||||
|
string(buf),
|
||||||
|
}, "\n")
|
||||||
|
// Write dnsmasq post conf file.
|
||||||
|
if err := os.WriteFile(merlinDNSMasqPostConfPath, []byte(data), 0750); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Restart dnsmasq service.
|
||||||
|
if err := merlinRestartDNSMasq(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := nvramSetup(nvramKV()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanupMerlin() error {
|
||||||
|
// Restore old configs.
|
||||||
|
if err := nvramRestore(nvramKV()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
buf, err := os.ReadFile(merlinDNSMasqPostConfPath)
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Restore dnsmasq post conf file.
|
||||||
|
if err := os.WriteFile(merlinDNSMasqPostConfPath, merlinParsePostConf(buf), 0750); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Restart dnsmasq service.
|
||||||
|
if err := merlinRestartDNSMasq(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func postInstallMerlin() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func merlinRestartDNSMasq() error {
|
||||||
|
if out, err := exec.Command("service", "restart_dnsmasq").CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("restart_dnsmasq: %s, %w", string(out), err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func merlinParsePostConf(buf []byte) []byte {
|
||||||
|
if len(buf) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
parts := bytes.Split(buf, []byte(merlinDNSMasqPostConfMarker))
|
||||||
|
if len(parts) != 1 {
|
||||||
|
return bytes.TrimLeftFunc(parts[1], unicode.IsSpace)
|
||||||
|
}
|
||||||
|
return buf
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_merlinParsePostConf(t *testing.T) {
|
||||||
|
origContent := "# foo"
|
||||||
|
data := strings.Join([]string{
|
||||||
|
merlinDNSMasqPostConfTmpl,
|
||||||
|
"\n",
|
||||||
|
merlinDNSMasqPostConfMarker,
|
||||||
|
"\n",
|
||||||
|
}, "\n")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
data string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"empty", "", ""},
|
||||||
|
{"no ctrld", origContent, origContent},
|
||||||
|
{"ctrld with data", data + origContent, origContent},
|
||||||
|
{"ctrld without data", data, ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
//t.Parallel()
|
||||||
|
if got := merlinParsePostConf([]byte(tc.data)); !bytes.Equal(got, []byte(tc.expected)) {
|
||||||
|
t.Errorf("unexpected result, want: %q, got: %q", tc.expected, string(got))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func nvram(args ...string) (string, error) {
|
||||||
|
cmd := exec.Command("nvram", args...)
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return "", fmt.Errorf("%s:%w", stderr.String(), err)
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(stdout.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
NOTE:
|
||||||
|
- For Openwrt, DNSSEC is not included in default dnsmasq (require dnsmasq-full).
|
||||||
|
- For Merlin, DNSSEC is configured during postconf script (see merlinDNSMasqPostConfTmpl).
|
||||||
|
- For Ubios UDM Pro/Dream Machine, DNSSEC is not included in their dnsmasq package:
|
||||||
|
+https://community.ui.com/questions/Implement-DNSSEC-into-UniFi/951c72b0-4d88-4c86-9174-45417bd2f9ca
|
||||||
|
+https://community.ui.com/questions/Enable-DNSSEC-for-Unifi-Dream-Machine-FW-updates/e68e367c-d09b-4459-9444-18908f7c1ea1
|
||||||
|
*/
|
||||||
|
func nvramKV() map[string]string {
|
||||||
|
switch Name() {
|
||||||
|
case DDWrt:
|
||||||
|
return 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.
|
||||||
|
}
|
||||||
|
case Merlin:
|
||||||
|
return map[string]string{
|
||||||
|
"dnspriv_enable": "0", // Ensure Merlin native DoT disabled.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func nvramSetup(m map[string]string) error {
|
||||||
|
// Backup current value, store ctrld's configs.
|
||||||
|
for key, value := range m {
|
||||||
|
old, err := nvram("get", key)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", old, err)
|
||||||
|
}
|
||||||
|
if out, err := nvram("set", nvramCtrldKeyPrefix+key+"="+old); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", out, err)
|
||||||
|
}
|
||||||
|
if out, err := nvram("set", key+"="+value); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", out, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if out, err := nvram("set", nvramCtrldSetupKey+"=1"); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", out, err)
|
||||||
|
}
|
||||||
|
// Commit.
|
||||||
|
if out, err := nvram("commit"); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", out, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func nvramRestore(m map[string]string) error {
|
||||||
|
// Restore old configs.
|
||||||
|
for key := range m {
|
||||||
|
ctrldKey := nvramCtrldKeyPrefix + key
|
||||||
|
old, err := nvram("get", ctrldKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", old, err)
|
||||||
|
}
|
||||||
|
_, _ = nvram("unset", ctrldKey)
|
||||||
|
if out, err := nvram("set", key+"="+old); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", out, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if out, err := nvram("unset", "ctrld_setup"); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", out, err)
|
||||||
|
}
|
||||||
|
// Commit.
|
||||||
|
if out, err := nvram("commit"); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", out, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errUCIEntryNotFound = errors.New("uci: Entry not found")
|
||||||
|
|
||||||
|
const openwrtDNSMasqConfigPath = "/tmp/dnsmasq.d/ctrld.conf"
|
||||||
|
|
||||||
|
// IsGLiNet reports whether the router is an GL.iNet router.
|
||||||
|
func IsGLiNet() bool {
|
||||||
|
if Name() != OpenWrt {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
buf, _ := os.ReadFile("/proc/version")
|
||||||
|
// The output of /proc/version contains "(glinet@glinet)".
|
||||||
|
return bytes.Contains(buf, []byte(" (glinet"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupOpenWrt() error {
|
||||||
|
// Delete dnsmasq port if set.
|
||||||
|
if _, err := uci("delete", "dhcp.@dnsmasq[0].port"); err != nil && !errors.Is(err, errUCIEntryNotFound) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Disable dnsmasq as DNS server.
|
||||||
|
dnsMasqConfigContent, err := dnsMasqConf()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(openwrtDNSMasqConfigPath, []byte(dnsMasqConfigContent), 0600); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Commit.
|
||||||
|
if _, err := uci("commit"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Restart dnsmasq service.
|
||||||
|
if err := openwrtRestartDNSMasq(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanupOpenWrt() error {
|
||||||
|
// Remove the custom dnsmasq config
|
||||||
|
if err := os.Remove(openwrtDNSMasqConfigPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Restart dnsmasq service.
|
||||||
|
if err := openwrtRestartDNSMasq(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func postInstallOpenWrt() error {
|
||||||
|
return exec.Command("/etc/init.d/ctrld", "enable").Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func uci(args ...string) (string, error) {
|
||||||
|
cmd := exec.Command("uci", args...)
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
if strings.HasPrefix(stderr.String(), errUCIEntryNotFound.Error()) {
|
||||||
|
return "", errUCIEntryNotFound
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("%s:%w", stderr.String(), err)
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(stdout.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func openwrtRestartDNSMasq() error {
|
||||||
|
if out, err := exec.Command("/etc/init.d/dnsmasq", "restart").CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", string(out), err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
const openWrtScript = `#!/bin/sh /etc/rc.common
|
||||||
|
USE_PROCD=1
|
||||||
|
# After network starts
|
||||||
|
START=21
|
||||||
|
# Before network stops
|
||||||
|
STOP=89
|
||||||
|
cmd="{{.Path}}{{range .Arguments}} {{.|cmd}}{{end}}"
|
||||||
|
name="{{.Name}}"
|
||||||
|
pid_file="/var/run/${name}.pid"
|
||||||
|
|
||||||
|
start_service() {
|
||||||
|
echo "Starting ${name}"
|
||||||
|
procd_open_instance
|
||||||
|
procd_set_param command ${cmd}
|
||||||
|
procd_set_param respawn # respawn automatically if something died
|
||||||
|
procd_set_param stdout 1 # forward stdout of the command to logd
|
||||||
|
procd_set_param stderr 1 # same for stderr
|
||||||
|
procd_set_param pidfile ${pid_file} # write a pid file on instance start and remove it on stop
|
||||||
|
procd_close_instance
|
||||||
|
echo "${name} has been started"
|
||||||
|
}
|
||||||
|
`
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
"github.com/kardianos/service"
|
||||||
|
"tailscale.com/logtail/backoff"
|
||||||
|
|
||||||
|
"github.com/Control-D-Inc/ctrld"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
OpenWrt = "openwrt"
|
||||||
|
DDWrt = "ddwrt"
|
||||||
|
Merlin = "merlin"
|
||||||
|
Ubios = "ubios"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrNotSupported reports the current router is not supported error.
|
||||||
|
var ErrNotSupported = errors.New("unsupported platform")
|
||||||
|
|
||||||
|
var routerPlatform atomic.Pointer[router]
|
||||||
|
|
||||||
|
type router struct {
|
||||||
|
name string
|
||||||
|
sendClientInfo bool
|
||||||
|
mac sync.Map
|
||||||
|
watcher *fsnotify.Watcher
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportedPlatforms return all platforms that can be configured to run with ctrld.
|
||||||
|
func SupportedPlatforms() []string {
|
||||||
|
return []string{DDWrt, Merlin, OpenWrt, Ubios}
|
||||||
|
}
|
||||||
|
|
||||||
|
var configureFunc = map[string]func() error{
|
||||||
|
DDWrt: setupDDWrt,
|
||||||
|
Merlin: setupMerlin,
|
||||||
|
OpenWrt: setupOpenWrt,
|
||||||
|
Ubios: setupUbiOS,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure configures things for running ctrld on the router.
|
||||||
|
func Configure(c *ctrld.Config) error {
|
||||||
|
name := Name()
|
||||||
|
switch name {
|
||||||
|
case DDWrt, Merlin, OpenWrt, Ubios:
|
||||||
|
if c.HasUpstreamSendClientInfo() {
|
||||||
|
r := routerPlatform.Load()
|
||||||
|
r.sendClientInfo = true
|
||||||
|
watcher, err := fsnotify.NewWatcher()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.watcher = watcher
|
||||||
|
go r.watchClientInfoTable()
|
||||||
|
for _, file := range clientInfoFiles {
|
||||||
|
_ = readClientInfoFile(file)
|
||||||
|
_ = r.watcher.Add(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
configure := configureFunc[name]
|
||||||
|
if err := configure(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return ErrNotSupported
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigureService performs necessary setup for running ctrld as a service on router.
|
||||||
|
func ConfigureService(sc *service.Config) error {
|
||||||
|
name := Name()
|
||||||
|
switch name {
|
||||||
|
case DDWrt:
|
||||||
|
if !ddwrtJff2Enabled() {
|
||||||
|
return errDdwrtJffs2NotEnabled
|
||||||
|
}
|
||||||
|
case OpenWrt:
|
||||||
|
sc.Option["SysvScript"] = openWrtScript
|
||||||
|
case Merlin, Ubios:
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PreStart blocks until the router is ready for running ctrld.
|
||||||
|
func PreStart() (err error) {
|
||||||
|
if Name() != DDWrt {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pidFile := "/tmp/ctrld.pid"
|
||||||
|
// On Merlin, NTP may out of sync, so waiting for it to be ready.
|
||||||
|
//
|
||||||
|
// Remove pid file and trigger dnsmasq restart, so NTP can resolve
|
||||||
|
// server name and perform time synchronization.
|
||||||
|
pid, err := os.ReadFile(pidFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("PreStart: os.Readfile: %w", err)
|
||||||
|
}
|
||||||
|
if err := os.Remove(pidFile); err != nil {
|
||||||
|
return fmt.Errorf("PreStart: os.Remove: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if werr := os.WriteFile(pidFile, pid, 0600); werr != nil {
|
||||||
|
err = errors.Join(err, werr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if rerr := merlinRestartDNSMasq(); rerr != nil {
|
||||||
|
err = errors.Join(err, rerr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err := merlinRestartDNSMasq(); err != nil {
|
||||||
|
return fmt.Errorf("PreStart: merlinRestartDNSMasq: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait until `ntp_read=1` set.
|
||||||
|
b := backoff.NewBackoff("PreStart", func(format string, args ...any) {}, 10*time.Second)
|
||||||
|
for {
|
||||||
|
out, err := nvram("get", "ntp_ready")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("PreStart: nvram: %w", err)
|
||||||
|
}
|
||||||
|
if out == "1" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
b.BackOff(context.Background(), errors.New("ntp not ready"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostInstall performs task after installing ctrld on router.
|
||||||
|
func PostInstall() error {
|
||||||
|
name := Name()
|
||||||
|
switch name {
|
||||||
|
case DDWrt:
|
||||||
|
return postInstallDDWrt()
|
||||||
|
case Merlin:
|
||||||
|
return postInstallMerlin()
|
||||||
|
case OpenWrt:
|
||||||
|
return postInstallOpenWrt()
|
||||||
|
case Ubios:
|
||||||
|
return postInstallUbiOS()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup cleans ctrld setup on the router.
|
||||||
|
func Cleanup() error {
|
||||||
|
name := Name()
|
||||||
|
switch name {
|
||||||
|
case DDWrt:
|
||||||
|
return cleanupDDWrt()
|
||||||
|
case Merlin:
|
||||||
|
return cleanupMerlin()
|
||||||
|
case OpenWrt:
|
||||||
|
return cleanupOpenWrt()
|
||||||
|
case Ubios:
|
||||||
|
return cleanupUbiOS()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenAddress returns the listener address of ctrld on router.
|
||||||
|
func ListenAddress() string {
|
||||||
|
name := Name()
|
||||||
|
switch name {
|
||||||
|
case DDWrt, Merlin, OpenWrt, Ubios:
|
||||||
|
return "127.0.0.1:5354"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns name of the router platform.
|
||||||
|
func Name() string {
|
||||||
|
if r := routerPlatform.Load(); r != nil {
|
||||||
|
return r.name
|
||||||
|
}
|
||||||
|
r := &router{}
|
||||||
|
r.name = distroName()
|
||||||
|
routerPlatform.Store(r)
|
||||||
|
return r.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func distroName() string {
|
||||||
|
switch {
|
||||||
|
case bytes.HasPrefix(uname(), []byte("DD-WRT")):
|
||||||
|
return DDWrt
|
||||||
|
case bytes.HasPrefix(uname(), []byte("ASUSWRT-Merlin")):
|
||||||
|
return Merlin
|
||||||
|
case haveFile("/etc/openwrt_version"):
|
||||||
|
return OpenWrt
|
||||||
|
case haveDir("/data/unifi"):
|
||||||
|
return Ubios
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func haveFile(file string) bool {
|
||||||
|
_, err := os.Stat(file)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func haveDir(dir string) bool {
|
||||||
|
fi, _ := os.Stat(dir)
|
||||||
|
return fi != nil && fi.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func uname() []byte {
|
||||||
|
out, _ := exec.Command("uname", "-o").Output()
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/kardianos/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
systems := []service.System{
|
||||||
|
&linuxSystemService{
|
||||||
|
name: "ddwrt",
|
||||||
|
detect: func() bool { return Name() == DDWrt },
|
||||||
|
interactive: func() bool {
|
||||||
|
is, _ := isInteractive()
|
||||||
|
return is
|
||||||
|
},
|
||||||
|
new: newddwrtService,
|
||||||
|
},
|
||||||
|
&linuxSystemService{
|
||||||
|
name: "merlin",
|
||||||
|
detect: func() bool { return Name() == Merlin },
|
||||||
|
interactive: func() bool {
|
||||||
|
is, _ := isInteractive()
|
||||||
|
return is
|
||||||
|
},
|
||||||
|
new: newMerlinService,
|
||||||
|
},
|
||||||
|
&linuxSystemService{
|
||||||
|
name: "ubios",
|
||||||
|
detect: func() bool {
|
||||||
|
if Name() != Ubios {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
out, err := exec.Command("ubnt-device-info", "firmware").CombinedOutput()
|
||||||
|
if err == nil {
|
||||||
|
// For v2/v3, UbiOS use a Debian base with systemd, so it is not
|
||||||
|
// necessary to use custom implementation for supporting init system.
|
||||||
|
return bytes.HasPrefix(out, []byte("1."))
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
interactive: func() bool {
|
||||||
|
is, _ := isInteractive()
|
||||||
|
return is
|
||||||
|
},
|
||||||
|
new: newUbiosService,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
systems = append(systems, service.AvailableSystems()...)
|
||||||
|
service.ChooseSystem(systems...)
|
||||||
|
}
|
||||||
|
|
||||||
|
type linuxSystemService struct {
|
||||||
|
name string
|
||||||
|
detect func() bool
|
||||||
|
interactive func() bool
|
||||||
|
new func(i service.Interface, platform string, c *service.Config) (service.Service, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sc linuxSystemService) String() string {
|
||||||
|
return sc.name
|
||||||
|
}
|
||||||
|
func (sc linuxSystemService) Detect() bool {
|
||||||
|
return sc.detect()
|
||||||
|
}
|
||||||
|
func (sc linuxSystemService) Interactive() bool {
|
||||||
|
return sc.interactive()
|
||||||
|
}
|
||||||
|
func (sc linuxSystemService) New(i service.Interface, c *service.Config) (service.Service, error) {
|
||||||
|
return sc.new(i, sc.String(), c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isInteractive() (bool, error) {
|
||||||
|
ppid := os.Getppid()
|
||||||
|
if ppid == 1 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,292 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/kardianos/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ddwrtSvc struct {
|
||||||
|
i service.Interface
|
||||||
|
platform string
|
||||||
|
*service.Config
|
||||||
|
rcStartup string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newddwrtService(i service.Interface, platform string, c *service.Config) (service.Service, error) {
|
||||||
|
s := &ddwrtSvc{
|
||||||
|
i: i,
|
||||||
|
platform: platform,
|
||||||
|
Config: c,
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll("/jffs/etc/config", 0644); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ddwrtSvc) String() string {
|
||||||
|
if len(s.DisplayName) > 0 {
|
||||||
|
return s.DisplayName
|
||||||
|
}
|
||||||
|
return s.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ddwrtSvc) Platform() string {
|
||||||
|
return s.platform
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ddwrtSvc) configPath() string {
|
||||||
|
return fmt.Sprintf("/jffs/etc/config/%s.startup", s.Config.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ddwrtSvc) template() *template.Template {
|
||||||
|
return template.Must(template.New("").Parse(ddwrtSvcScript))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ddwrtSvc) Install() error {
|
||||||
|
confPath := s.configPath()
|
||||||
|
if _, err := os.Stat(confPath); err == nil {
|
||||||
|
return fmt.Errorf("already installed: %s", confPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
path, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(path, "/jffs/") {
|
||||||
|
return errors.New("could not install service outside /jffs")
|
||||||
|
}
|
||||||
|
|
||||||
|
var to = &struct {
|
||||||
|
*service.Config
|
||||||
|
Path string
|
||||||
|
}{
|
||||||
|
s.Config,
|
||||||
|
path,
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Create(confPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
if err := s.template().Execute(f, to); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = os.Chmod(confPath, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
if err := template.Must(template.New("").Parse(ddwrtStartupCmd)).Execute(&sb, to); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.rcStartup = sb.String()
|
||||||
|
curVal, err := nvram("get", nvramRCStartupKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := nvram("set", nvramCtrldKeyPrefix+nvramRCStartupKey+"="+curVal); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
val := strings.Join([]string{curVal, s.rcStartup + " &", fmt.Sprintf(`echo $! > "/tmp/%s.pid"`, s.Config.Name)}, "\n")
|
||||||
|
|
||||||
|
if _, err := nvram("set", nvramRCStartupKey+"="+val); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if out, err := nvram("commit"); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", out, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ddwrtSvc) Uninstall() error {
|
||||||
|
if err := os.Remove(s.configPath()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctrldStartupKey := nvramCtrldKeyPrefix + nvramRCStartupKey
|
||||||
|
rcStartup, err := nvram("get", ctrldStartupKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, _ = nvram("unset", ctrldStartupKey)
|
||||||
|
if _, err := nvram("set", nvramRCStartupKey+"="+rcStartup); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if out, err := nvram("commit"); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", out, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ddwrtSvc) Logger(errs chan<- error) (service.Logger, error) {
|
||||||
|
if service.Interactive() {
|
||||||
|
return service.ConsoleLogger, nil
|
||||||
|
}
|
||||||
|
return s.SystemLogger(errs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ddwrtSvc) SystemLogger(errs chan<- error) (service.Logger, error) {
|
||||||
|
// TODO(cuonglm): detect syslog enable and return proper logger?
|
||||||
|
// this at least works with default configuration.
|
||||||
|
if service.Interactive() {
|
||||||
|
return service.ConsoleLogger, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
return &noopLogger{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ddwrtSvc) Run() (err error) {
|
||||||
|
err = s.i.Start(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if interactice, _ := isInteractive(); !interactice {
|
||||||
|
signal.Ignore(syscall.SIGHUP)
|
||||||
|
}
|
||||||
|
var sigChan = make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigChan, syscall.SIGTERM, os.Interrupt)
|
||||||
|
<-sigChan
|
||||||
|
|
||||||
|
return s.i.Stop(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ddwrtSvc) Status() (service.Status, error) {
|
||||||
|
if _, err := os.Stat(s.configPath()); os.IsNotExist(err) {
|
||||||
|
return service.StatusUnknown, service.ErrNotInstalled
|
||||||
|
}
|
||||||
|
out, err := exec.Command(s.configPath(), "status").CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return service.StatusUnknown, err
|
||||||
|
}
|
||||||
|
switch string(bytes.TrimSpace(out)) {
|
||||||
|
case "running":
|
||||||
|
return service.StatusRunning, nil
|
||||||
|
default:
|
||||||
|
return service.StatusStopped, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ddwrtSvc) Start() error {
|
||||||
|
return exec.Command(s.configPath(), "start").Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ddwrtSvc) Stop() error {
|
||||||
|
return exec.Command(s.configPath(), "stop").Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ddwrtSvc) Restart() error {
|
||||||
|
err := s.Stop()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
type noopLogger struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c noopLogger) Error(v ...interface{}) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (c noopLogger) Warning(v ...interface{}) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (c noopLogger) Info(v ...interface{}) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (c noopLogger) Errorf(format string, a ...interface{}) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (c noopLogger) Warningf(format string, a ...interface{}) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (c noopLogger) Infof(format string, a ...interface{}) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const ddwrtStartupCmd = `{{.Path}}{{range .Arguments}} {{.}}{{end}}`
|
||||||
|
const ddwrtSvcScript = `#!/bin/sh
|
||||||
|
|
||||||
|
name="{{.Name}}"
|
||||||
|
cmd="{{.Path}}{{range .Arguments}} {{.}}{{end}}"
|
||||||
|
pid_file="/tmp/$name.pid"
|
||||||
|
|
||||||
|
get_pid() {
|
||||||
|
cat "$pid_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
is_running() {
|
||||||
|
[ -f "$pid_file" ] && ps | grep -q "^ *$(get_pid) "
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
start)
|
||||||
|
if is_running; then
|
||||||
|
echo "Already started"
|
||||||
|
else
|
||||||
|
echo "Starting $name"
|
||||||
|
$cmd &
|
||||||
|
echo $! > "$pid_file"
|
||||||
|
chmod 600 "$pid_file"
|
||||||
|
if ! is_running; then
|
||||||
|
echo "Failed to start $name"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
if is_running; then
|
||||||
|
echo -n "Stopping $name..."
|
||||||
|
kill "$(get_pid)"
|
||||||
|
for _ in 1 2 3 4 5; do
|
||||||
|
if ! is_running; then
|
||||||
|
echo "stopped"
|
||||||
|
if [ -f "$pid_file" ]; then
|
||||||
|
rm "$pid_file"
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
printf "."
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "failed to stop $name"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
$0 stop
|
||||||
|
$0 start
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
if is_running; then
|
||||||
|
echo "running"
|
||||||
|
else
|
||||||
|
echo "stopped"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: $0 {start|stop|restart|status}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
exit 0
|
||||||
|
`
|
||||||
@@ -0,0 +1,354 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/kardianos/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
merlinJFFSScriptPath = "/jffs/scripts/services-start"
|
||||||
|
merlinJFFSServiceEventScriptPath = "/jffs/scripts/service-event"
|
||||||
|
)
|
||||||
|
|
||||||
|
type merlinSvc struct {
|
||||||
|
i service.Interface
|
||||||
|
platform string
|
||||||
|
*service.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMerlinService(i service.Interface, platform string, c *service.Config) (service.Service, error) {
|
||||||
|
s := &merlinSvc{
|
||||||
|
i: i,
|
||||||
|
platform: platform,
|
||||||
|
Config: c,
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *merlinSvc) String() string {
|
||||||
|
if len(s.DisplayName) > 0 {
|
||||||
|
return s.DisplayName
|
||||||
|
}
|
||||||
|
return s.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *merlinSvc) Platform() string {
|
||||||
|
return s.platform
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *merlinSvc) configPath() string {
|
||||||
|
path, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return path + ".startup"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *merlinSvc) template() *template.Template {
|
||||||
|
return template.Must(template.New("").Parse(merlinSvcScript))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *merlinSvc) Install() error {
|
||||||
|
exePath, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(exePath, "/jffs/") {
|
||||||
|
return errors.New("could not install service outside /jffs")
|
||||||
|
}
|
||||||
|
if _, err := nvram("set", "jffs2_scripts=1"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := nvram("commit"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
confPath := s.configPath()
|
||||||
|
if _, err := os.Stat(confPath); err == nil {
|
||||||
|
return fmt.Errorf("already installed: %s", confPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
var to = &struct {
|
||||||
|
*service.Config
|
||||||
|
Path string
|
||||||
|
}{
|
||||||
|
s.Config,
|
||||||
|
exePath,
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Create(confPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("os.Create: %w", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
if err := s.template().Execute(f, to); err != nil {
|
||||||
|
return fmt.Errorf("s.template.Execute: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = os.Chmod(confPath, 0755); err != nil {
|
||||||
|
return fmt.Errorf("os.Chmod: startup script: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(merlinJFFSScriptPath), 0755); err != nil {
|
||||||
|
return fmt.Errorf("os.MkdirAll: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpScript, err := os.CreateTemp("", "ctrld_install")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("os.CreateTemp: %w", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(tmpScript.Name())
|
||||||
|
defer tmpScript.Close()
|
||||||
|
|
||||||
|
if _, err := tmpScript.WriteString(merlinAddLineToScript); err != nil {
|
||||||
|
return fmt.Errorf("tmpScript.WriteString: %w", err)
|
||||||
|
}
|
||||||
|
if err := tmpScript.Close(); err != nil {
|
||||||
|
return fmt.Errorf("tmpScript.Close: %w", err)
|
||||||
|
}
|
||||||
|
addLineToScript := func(line, script string) error {
|
||||||
|
if _, err := os.Stat(script); os.IsNotExist(err) {
|
||||||
|
if err := os.WriteFile(script, []byte("#!/bin/sh\n"), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := os.Chmod(script, 0755); err != nil {
|
||||||
|
return fmt.Errorf("os.Chmod: jffs script: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := exec.Command("sh", tmpScript.Name(), line, script).Run(); err != nil {
|
||||||
|
return fmt.Errorf("exec.Command: add startup script: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for script, line := range map[string]string{
|
||||||
|
merlinJFFSScriptPath: s.configPath() + " start",
|
||||||
|
merlinJFFSServiceEventScriptPath: s.configPath() + ` service_event "$1" "$2"`,
|
||||||
|
} {
|
||||||
|
if err := addLineToScript(line, script); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *merlinSvc) Uninstall() error {
|
||||||
|
if err := os.Remove(s.configPath()); err != nil {
|
||||||
|
return fmt.Errorf("os.Remove: %w", err)
|
||||||
|
}
|
||||||
|
tmpScript, err := os.CreateTemp("", "ctrld_uninstall")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("os.CreateTemp: %w", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(tmpScript.Name())
|
||||||
|
defer tmpScript.Close()
|
||||||
|
|
||||||
|
if _, err := tmpScript.WriteString(merlinRemoveLineFromScript); err != nil {
|
||||||
|
return fmt.Errorf("tmpScript.WriteString: %w", err)
|
||||||
|
}
|
||||||
|
if err := tmpScript.Close(); err != nil {
|
||||||
|
return fmt.Errorf("tmpScript.Close: %w", err)
|
||||||
|
}
|
||||||
|
removeLineFromScript := func(line, script string) error {
|
||||||
|
if _, err := os.Stat(script); os.IsNotExist(err) {
|
||||||
|
if err := os.WriteFile(script, []byte("#!/bin/sh\n"), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := os.Chmod(script, 0755); err != nil {
|
||||||
|
return fmt.Errorf("os.Chmod: jffs script: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := exec.Command("sh", tmpScript.Name(), line, script).Run(); err != nil {
|
||||||
|
return fmt.Errorf("exec.Command: add startup script: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for script, line := range map[string]string{
|
||||||
|
merlinJFFSScriptPath: s.configPath() + " start",
|
||||||
|
merlinJFFSServiceEventScriptPath: s.configPath() + ` service_event "$1" "$2"`,
|
||||||
|
} {
|
||||||
|
if err := removeLineFromScript(line, script); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *merlinSvc) Logger(errs chan<- error) (service.Logger, error) {
|
||||||
|
if service.Interactive() {
|
||||||
|
return service.ConsoleLogger, nil
|
||||||
|
}
|
||||||
|
return s.SystemLogger(errs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *merlinSvc) SystemLogger(errs chan<- error) (service.Logger, error) {
|
||||||
|
return newSysLogger(s.Name, errs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *merlinSvc) Run() (err error) {
|
||||||
|
err = s.i.Start(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if interactice, _ := isInteractive(); !interactice {
|
||||||
|
signal.Ignore(syscall.SIGHUP)
|
||||||
|
}
|
||||||
|
|
||||||
|
var sigChan = make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigChan, syscall.SIGTERM, os.Interrupt)
|
||||||
|
<-sigChan
|
||||||
|
|
||||||
|
return s.i.Stop(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *merlinSvc) Status() (service.Status, error) {
|
||||||
|
if _, err := os.Stat(s.configPath()); os.IsNotExist(err) {
|
||||||
|
return service.StatusUnknown, service.ErrNotInstalled
|
||||||
|
}
|
||||||
|
out, err := exec.Command(s.configPath(), "status").CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return service.StatusUnknown, err
|
||||||
|
}
|
||||||
|
switch string(bytes.TrimSpace(out)) {
|
||||||
|
case "running":
|
||||||
|
return service.StatusRunning, nil
|
||||||
|
default:
|
||||||
|
return service.StatusStopped, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *merlinSvc) Start() error {
|
||||||
|
return exec.Command(s.configPath(), "start").Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *merlinSvc) Stop() error {
|
||||||
|
return exec.Command(s.configPath(), "stop").Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *merlinSvc) Restart() error {
|
||||||
|
err := s.Stop()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
const merlinSvcScript = `#!/bin/sh
|
||||||
|
|
||||||
|
name="{{.Name}}"
|
||||||
|
cmd="{{.Path}}{{range .Arguments}} {{.}}{{end}}"
|
||||||
|
pid_file="/tmp/$name.pid"
|
||||||
|
|
||||||
|
get_pid() {
|
||||||
|
cat "$pid_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
is_running() {
|
||||||
|
[ -f "$pid_file" ] && ps | grep -q "^ *$(get_pid) "
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
start)
|
||||||
|
if is_running; then
|
||||||
|
logger -c "Already started"
|
||||||
|
else
|
||||||
|
logger -c "Starting $name"
|
||||||
|
if [ -f /rom/ca-bundle.crt ]; then
|
||||||
|
# For John’s fork
|
||||||
|
export SSL_CERT_FILE=/rom/ca-bundle.crt
|
||||||
|
fi
|
||||||
|
$cmd &
|
||||||
|
echo $! > "$pid_file"
|
||||||
|
chmod 600 "$pid_file"
|
||||||
|
if ! is_running; then
|
||||||
|
logger -c "Failed to start $name"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
if is_running; then
|
||||||
|
logger -c "Stopping $name..."
|
||||||
|
kill "$(get_pid)"
|
||||||
|
for _ in 1 2 3 4 5; do
|
||||||
|
if ! is_running; then
|
||||||
|
logger -c "stopped"
|
||||||
|
if [ -f "$pid_file" ]; then
|
||||||
|
rm "$pid_file"
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
printf "."
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
logger -c "failed to stop $name"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
$0 stop
|
||||||
|
$0 start
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
if is_running; then
|
||||||
|
echo "running"
|
||||||
|
else
|
||||||
|
echo "stopped"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
service_event)
|
||||||
|
event=$2
|
||||||
|
svc=$3
|
||||||
|
dnsmasq_pid_file=$(sed -n '/pid-file=/s///p' /etc/dnsmasq.conf)
|
||||||
|
|
||||||
|
if [ "$event" = "restart" ] && [ "$svc" = "diskmon" ]; then
|
||||||
|
kill "$(cat "$dnsmasq_pid_file")" >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: $0 {start|stop|restart|status}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
exit 0
|
||||||
|
`
|
||||||
|
|
||||||
|
const merlinAddLineToScript = `#!/bin/sh
|
||||||
|
|
||||||
|
line=$1
|
||||||
|
file=$2
|
||||||
|
|
||||||
|
. /usr/sbin/helper.sh
|
||||||
|
|
||||||
|
pc_append "$line" "$file"
|
||||||
|
`
|
||||||
|
|
||||||
|
const merlinRemoveLineFromScript = `#!/bin/sh
|
||||||
|
|
||||||
|
line=$1
|
||||||
|
file=$2
|
||||||
|
|
||||||
|
. /usr/sbin/helper.sh
|
||||||
|
|
||||||
|
pc_delete "$line" "$file"
|
||||||
|
`
|
||||||
@@ -0,0 +1,336 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"text/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kardianos/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This is a copy of https://github.com/kardianos/service/blob/v1.2.1/service_sysv_linux.go,
|
||||||
|
// with modification for supporting ubios v1 init system.
|
||||||
|
|
||||||
|
type ubiosSvc struct {
|
||||||
|
i service.Interface
|
||||||
|
platform string
|
||||||
|
*service.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUbiosService(i service.Interface, platform string, c *service.Config) (service.Service, error) {
|
||||||
|
s := &ubiosSvc{
|
||||||
|
i: i,
|
||||||
|
platform: platform,
|
||||||
|
Config: c,
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ubiosSvc) String() string {
|
||||||
|
if len(s.DisplayName) > 0 {
|
||||||
|
return s.DisplayName
|
||||||
|
}
|
||||||
|
return s.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ubiosSvc) Platform() string {
|
||||||
|
return s.platform
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ubiosSvc) configPath() string {
|
||||||
|
return "/etc/init.d/" + s.Config.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ubiosSvc) execPath() (string, error) {
|
||||||
|
if len(s.Executable) != 0 {
|
||||||
|
return filepath.Abs(s.Executable)
|
||||||
|
}
|
||||||
|
return os.Executable()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ubiosSvc) template() *template.Template {
|
||||||
|
return template.Must(template.New("").Funcs(tf).Parse(ubiosSvcScript))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ubiosSvc) Install() error {
|
||||||
|
confPath := s.configPath()
|
||||||
|
if _, err := os.Stat(confPath); err == nil {
|
||||||
|
return fmt.Errorf("init already exists: %s", confPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Create(confPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create config path: %w", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
path, err := s.execPath()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get exec path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var to = &struct {
|
||||||
|
*service.Config
|
||||||
|
Path string
|
||||||
|
DnsMasqConfPath string
|
||||||
|
}{
|
||||||
|
s.Config,
|
||||||
|
path,
|
||||||
|
ubiosDNSMasqConfigPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.template().Execute(f, to); err != nil {
|
||||||
|
return fmt.Errorf("failed to create init script: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := f.Close(); err != nil {
|
||||||
|
return fmt.Errorf("failed to save init script: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = os.Chmod(confPath, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to set init script executable: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable on boot
|
||||||
|
script, err := os.CreateTemp("", "ctrld_boot.service")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create boot service tmp file: %w", err)
|
||||||
|
}
|
||||||
|
defer script.Close()
|
||||||
|
|
||||||
|
svcConfig := *to.Config
|
||||||
|
svcConfig.Arguments = os.Args[1:]
|
||||||
|
to.Config = &svcConfig
|
||||||
|
if err := template.Must(template.New("").Funcs(tf).Parse(ubiosBootSystemdService)).Execute(script, &to); err != nil {
|
||||||
|
return fmt.Errorf("failed to create boot service file: %w", err)
|
||||||
|
}
|
||||||
|
if err := script.Close(); err != nil {
|
||||||
|
return fmt.Errorf("failed to save boot service file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the boot script to container and start.
|
||||||
|
cmd := exec.Command("podman", "cp", "--pause=false", script.Name(), "unifi-os:/lib/systemd/system/ctrld-boot.service")
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("failed to copy boot script, out: %s, err: %v", string(out), err)
|
||||||
|
}
|
||||||
|
cmd = exec.Command("podman", "exec", "unifi-os", "systemctl", "enable", "--now", "ctrld-boot.service")
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("failed to start ctrld boot script, out: %s, err: %v", string(out), err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ubiosSvc) Uninstall() error {
|
||||||
|
if err := os.Remove(s.configPath()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Remove ctrld-boot service inside unifi-os container.
|
||||||
|
cmd := exec.Command("podman", "exec", "unifi-os", "systemctl", "disable", "ctrld-boot.service")
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("failed to disable ctrld-boot service, out: %s, err: %v", string(out), err)
|
||||||
|
}
|
||||||
|
cmd = exec.Command("podman", "exec", "unifi-os", "rm", "/lib/systemd/system/ctrld-boot.service")
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove ctrld-boot service file, out: %s, err: %v", string(out), err)
|
||||||
|
}
|
||||||
|
cmd = exec.Command("podman", "exec", "unifi-os", "systemctl", "daemon-reload")
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("failed to reload systemd service, out: %s, err: %v", string(out), err)
|
||||||
|
}
|
||||||
|
cmd = exec.Command("podman", "exec", "unifi-os", "systemctl", "reset-failed")
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("failed to reset-failed systemd service, out: %s, err: %v", string(out), err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ubiosSvc) Logger(errs chan<- error) (service.Logger, error) {
|
||||||
|
if service.Interactive() {
|
||||||
|
return service.ConsoleLogger, nil
|
||||||
|
}
|
||||||
|
return s.SystemLogger(errs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ubiosSvc) SystemLogger(errs chan<- error) (service.Logger, error) {
|
||||||
|
return newSysLogger(s.Name, errs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ubiosSvc) Run() (err error) {
|
||||||
|
err = s.i.Start(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if interactice, _ := isInteractive(); !interactice {
|
||||||
|
signal.Ignore(syscall.SIGHUP)
|
||||||
|
}
|
||||||
|
|
||||||
|
var sigChan = make(chan os.Signal, 3)
|
||||||
|
signal.Notify(sigChan, syscall.SIGTERM, os.Interrupt)
|
||||||
|
<-sigChan
|
||||||
|
|
||||||
|
return s.i.Stop(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ubiosSvc) Status() (service.Status, error) {
|
||||||
|
if _, err := os.Stat(s.configPath()); os.IsNotExist(err) {
|
||||||
|
return service.StatusUnknown, service.ErrNotInstalled
|
||||||
|
}
|
||||||
|
out, err := exec.Command(s.configPath(), "status").CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return service.StatusUnknown, err
|
||||||
|
}
|
||||||
|
switch string(bytes.TrimSpace(out)) {
|
||||||
|
case "Running":
|
||||||
|
return service.StatusRunning, nil
|
||||||
|
default:
|
||||||
|
return service.StatusStopped, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ubiosSvc) Start() error {
|
||||||
|
return exec.Command(s.configPath(), "start").Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ubiosSvc) Stop() error {
|
||||||
|
return exec.Command(s.configPath(), "stop").Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ubiosSvc) Restart() error {
|
||||||
|
err := s.Stop()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
return s.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
const ubiosBootSystemdService = `[Unit]
|
||||||
|
Description=Run ctrld On Startup UDM
|
||||||
|
Wants=network-online.target
|
||||||
|
After=network-online.target
|
||||||
|
StartLimitIntervalSec=500
|
||||||
|
StartLimitBurst=5
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5s
|
||||||
|
ExecStart=/sbin/ssh-proxy '[ -f "{{.DnsMasqConfPath}}" ] || {{.Path}}{{range .Arguments}} {{.|cmd}}{{end}}'
|
||||||
|
RemainAfterExit=true
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
`
|
||||||
|
|
||||||
|
const ubiosSvcScript = `#!/bin/sh
|
||||||
|
# For RedHat and cousins:
|
||||||
|
# chkconfig: - 99 01
|
||||||
|
# description: {{.Description}}
|
||||||
|
# processname: {{.Path}}
|
||||||
|
|
||||||
|
### BEGIN INIT INFO
|
||||||
|
# Provides: {{.Path}}
|
||||||
|
# Required-Start:
|
||||||
|
# Required-Stop:
|
||||||
|
# Default-Start: 2 3 4 5
|
||||||
|
# Default-Stop: 0 1 6
|
||||||
|
# Short-Description: {{.DisplayName}}
|
||||||
|
# Description: {{.Description}}
|
||||||
|
### END INIT INFO
|
||||||
|
|
||||||
|
cmd="{{.Path}}{{range .Arguments}} {{.|cmd}}{{end}}"
|
||||||
|
|
||||||
|
name=$(basename $(readlink -f $0))
|
||||||
|
pid_file="/var/run/$name.pid"
|
||||||
|
stdout_log="/var/log/$name.log"
|
||||||
|
stderr_log="/var/log/$name.err"
|
||||||
|
|
||||||
|
[ -e /etc/sysconfig/$name ] && . /etc/sysconfig/$name
|
||||||
|
|
||||||
|
get_pid() {
|
||||||
|
cat "$pid_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
is_running() {
|
||||||
|
[ -f "$pid_file" ] && cat /proc/$(get_pid)/stat > /dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
start)
|
||||||
|
if is_running; then
|
||||||
|
echo "Already started"
|
||||||
|
else
|
||||||
|
echo "Starting $name"
|
||||||
|
{{if .WorkingDirectory}}cd '{{.WorkingDirectory}}'{{end}}
|
||||||
|
$cmd >> "$stdout_log" 2>> "$stderr_log" &
|
||||||
|
echo $! > "$pid_file"
|
||||||
|
if ! is_running; then
|
||||||
|
echo "Unable to start, see $stdout_log and $stderr_log"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
if is_running; then
|
||||||
|
echo -n "Stopping $name.."
|
||||||
|
kill $(get_pid)
|
||||||
|
for i in $(seq 1 10)
|
||||||
|
do
|
||||||
|
if ! is_running; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
echo -n "."
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
if is_running; then
|
||||||
|
echo "Not stopped; may still be shutting down or shutdown may have failed"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "Stopped"
|
||||||
|
if [ -f "$pid_file" ]; then
|
||||||
|
rm "$pid_file"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Not running"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
$0 stop
|
||||||
|
if is_running; then
|
||||||
|
echo "Unable to stop, will not attempt to start"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
$0 start
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
if is_running; then
|
||||||
|
echo "Running"
|
||||||
|
else
|
||||||
|
echo "Stopped"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: $0 {start|stop|restart|status}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
exit 0
|
||||||
|
`
|
||||||
|
|
||||||
|
var tf = map[string]interface{}{
|
||||||
|
"cmd": func(s string) string {
|
||||||
|
return `"` + strings.Replace(s, `"`, `\"`, -1) + `"`
|
||||||
|
},
|
||||||
|
"cmdEscape": func(s string) string {
|
||||||
|
return strings.Replace(s, " ", `\x20`, -1)
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
//go:build linux || darwin || freebsd
|
||||||
|
|
||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/syslog"
|
||||||
|
|
||||||
|
"github.com/kardianos/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newSysLogger(name string, errs chan<- error) (service.Logger, error) {
|
||||||
|
w, err := syslog.New(syslog.LOG_INFO, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return sysLogger{w, errs}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type sysLogger struct {
|
||||||
|
*syslog.Writer
|
||||||
|
errs chan<- error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sysLogger) send(err error) error {
|
||||||
|
if err != nil && s.errs != nil {
|
||||||
|
s.errs <- err
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sysLogger) Error(v ...interface{}) error {
|
||||||
|
return s.send(s.Writer.Err(fmt.Sprint(v...)))
|
||||||
|
}
|
||||||
|
func (s sysLogger) Warning(v ...interface{}) error {
|
||||||
|
return s.send(s.Writer.Warning(fmt.Sprint(v...)))
|
||||||
|
}
|
||||||
|
func (s sysLogger) Info(v ...interface{}) error {
|
||||||
|
return s.send(s.Writer.Info(fmt.Sprint(v...)))
|
||||||
|
}
|
||||||
|
func (s sysLogger) Errorf(format string, a ...interface{}) error {
|
||||||
|
return s.send(s.Writer.Err(fmt.Sprintf(format, a...)))
|
||||||
|
}
|
||||||
|
func (s sysLogger) Warningf(format string, a ...interface{}) error {
|
||||||
|
return s.send(s.Writer.Warning(fmt.Sprintf(format, a...)))
|
||||||
|
}
|
||||||
|
func (s sysLogger) Infof(format string, a ...interface{}) error {
|
||||||
|
return s.send(s.Writer.Info(fmt.Sprintf(format, a...)))
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import "github.com/kardianos/service"
|
||||||
|
|
||||||
|
func newSysLogger(name string, errs chan<- error) (service.Logger, error) {
|
||||||
|
return service.ConsoleLogger, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ubiosDNSMasqConfigPath = "/run/dnsmasq.conf.d/zzzctrld.conf"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupUbiOS() error {
|
||||||
|
// Disable dnsmasq as DNS server.
|
||||||
|
dnsMasqConfigContent, err := dnsMasqConf()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(ubiosDNSMasqConfigPath, []byte(dnsMasqConfigContent), 0600); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Restart dnsmasq service.
|
||||||
|
if err := ubiosRestartDNSMasq(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanupUbiOS() error {
|
||||||
|
// Remove the custom dnsmasq config
|
||||||
|
if err := os.Remove(ubiosDNSMasqConfigPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Restart dnsmasq service.
|
||||||
|
if err := ubiosRestartDNSMasq(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func postInstallUbiOS() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ubiosRestartDNSMasq() error {
|
||||||
|
buf, err := os.ReadFile("/run/dnsmasq.pid")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pid, err := strconv.ParseUint(string(bytes.TrimSpace(buf)), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
proc, err := os.FindProcess(int(pid))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return proc.Kill()
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package ctrld
|
||||||
|
|
||||||
|
import "net"
|
||||||
|
|
||||||
|
type dnsFn func() []string
|
||||||
|
|
||||||
|
func nameservers() []string {
|
||||||
|
var dns []string
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
ch := make(chan []string)
|
||||||
|
fns := dnsFns()
|
||||||
|
|
||||||
|
for _, fn := range fns {
|
||||||
|
go func(fn dnsFn) {
|
||||||
|
ch <- fn()
|
||||||
|
}(fn)
|
||||||
|
}
|
||||||
|
for range fns {
|
||||||
|
for _, ns := range <-ch {
|
||||||
|
if seen[ns] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[ns] = true
|
||||||
|
dns = append(dns, net.JoinHostPort(ns, "53"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dns
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
//go:build darwin || dragonfly || freebsd || netbsd || openbsd
|
||||||
|
|
||||||
|
package ctrld
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"golang.org/x/net/route"
|
||||||
|
)
|
||||||
|
|
||||||
|
func dnsFns() []dnsFn {
|
||||||
|
return []dnsFn{dnsFromRIB, dnsFromIPConfig}
|
||||||
|
}
|
||||||
|
|
||||||
|
func dnsFromRIB() []string {
|
||||||
|
var dns []string
|
||||||
|
rib, err := route.FetchRIB(syscall.AF_UNSPEC, route.RIBTypeRoute, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
messages, err := route.ParseRIB(route.RIBTypeRoute, rib)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, message := range messages {
|
||||||
|
message, ok := message.(*route.RouteMessage)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
addresses := message.Addrs
|
||||||
|
if len(addresses) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dst, gw := toNetIP(addresses[0]), toNetIP(addresses[1])
|
||||||
|
if dst == nil || gw == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if gw.IsLoopback() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if dst.Equal(net.IPv4zero) || dst.Equal(net.IPv6zero) {
|
||||||
|
dns = append(dns, gw.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dns
|
||||||
|
}
|
||||||
|
|
||||||
|
func dnsFromIPConfig() []string {
|
||||||
|
if runtime.GOOS != "darwin" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cmd := exec.Command("ipconfig", "getoption", "", "domain_name_server")
|
||||||
|
out, _ := cmd.Output()
|
||||||
|
if ip := net.ParseIP(strings.TrimSpace(string(out))); ip != nil {
|
||||||
|
return []string{ip.String()}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toNetIP(addr route.Addr) net.IP {
|
||||||
|
switch t := addr.(type) {
|
||||||
|
case *route.Inet4Addr:
|
||||||
|
return net.IPv4(t.IP[0], t.IP[1], t.IP[2], t.IP[3])
|
||||||
|
case *route.Inet6Addr:
|
||||||
|
ip := make(net.IP, net.IPv6len)
|
||||||
|
copy(ip, t.IP[:])
|
||||||
|
return ip
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package ctrld
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"encoding/hex"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/Control-D-Inc/ctrld/internal/dns/resolvconffile"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
v4RouteFile = "/proc/net/route"
|
||||||
|
v6RouteFile = "/proc/net/ipv6_route"
|
||||||
|
)
|
||||||
|
|
||||||
|
func dnsFns() []dnsFn {
|
||||||
|
return []dnsFn{dns4, dns6, dnsFromSystemdResolver}
|
||||||
|
}
|
||||||
|
|
||||||
|
func dns4() []string {
|
||||||
|
f, err := os.Open(v4RouteFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
var dns []string
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
s := bufio.NewScanner(f)
|
||||||
|
first := true
|
||||||
|
for s.Scan() {
|
||||||
|
if first {
|
||||||
|
first = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fields := bytes.Fields(s.Bytes())
|
||||||
|
if len(fields) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
gw := make([]byte, net.IPv4len)
|
||||||
|
// Third fields is gateway.
|
||||||
|
if _, err := hex.Decode(gw, fields[2]); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ip := net.IPv4(gw[3], gw[2], gw[1], gw[0])
|
||||||
|
if ip.Equal(net.IPv4zero) || seen[ip.String()] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[ip.String()] = true
|
||||||
|
dns = append(dns, ip.String())
|
||||||
|
}
|
||||||
|
return dns
|
||||||
|
}
|
||||||
|
|
||||||
|
func dns6() []string {
|
||||||
|
f, err := os.Open(v6RouteFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
var dns []string
|
||||||
|
s := bufio.NewScanner(f)
|
||||||
|
for s.Scan() {
|
||||||
|
fields := bytes.Fields(s.Bytes())
|
||||||
|
if len(fields) < 4 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
gw := make([]byte, net.IPv6len)
|
||||||
|
// Fifth fields is gateway.
|
||||||
|
if _, err := hex.Decode(gw, fields[4]); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ip := net.IP(gw)
|
||||||
|
if ip.Equal(net.IPv6zero) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dns = append(dns, ip.String())
|
||||||
|
}
|
||||||
|
return dns
|
||||||
|
}
|
||||||
|
|
||||||
|
func dnsFromSystemdResolver() []string {
|
||||||
|
c, err := resolvconffile.ParseFile("/run/systemd/resolve/resolv.conf")
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ns := make([]string, 0, len(c.Nameservers))
|
||||||
|
for _, nameserver := range c.Nameservers {
|
||||||
|
ns = append(ns, nameserver.String())
|
||||||
|
}
|
||||||
|
return ns
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package ctrld
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestNameservers(t *testing.T) {
|
||||||
|
ns := nameservers()
|
||||||
|
if len(ns) == 0 {
|
||||||
|
t.Fatal("failed to get nameservers")
|
||||||
|
}
|
||||||
|
t.Log(ns)
|
||||||
|
}
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
//go:build !js && !windows
|
|
||||||
|
|
||||||
package ctrld
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/resolvconffile"
|
|
||||||
)
|
|
||||||
|
|
||||||
func nameservers() []string {
|
|
||||||
return resolvconffile.NameServersWithPort()
|
|
||||||
}
|
|
||||||
+43
-54
@@ -2,70 +2,59 @@ package ctrld
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
"os"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"unsafe"
|
|
||||||
|
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||||
|
|
||||||
"golang.org/x/sys/windows"
|
"golang.org/x/sys/windows"
|
||||||
)
|
)
|
||||||
|
|
||||||
func nameservers() []string {
|
func dnsFns() []dnsFn {
|
||||||
aas, err := adapterAddresses()
|
return []dnsFn{dnsFromAdapter}
|
||||||
|
}
|
||||||
|
|
||||||
|
func dnsFromAdapter() []string {
|
||||||
|
aas, err := winipcfg.GetAdaptersAddresses(syscall.AF_UNSPEC, winipcfg.GAAFlagIncludeGateways|winipcfg.GAAFlagIncludePrefix)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
ns := make([]string, 0, len(aas))
|
ns := make([]string, 0, len(aas)*2)
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
do := func(addr windows.SocketAddress) {
|
||||||
|
sa, err := addr.Sockaddr.Sockaddr()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var ip net.IP
|
||||||
|
switch sa := sa.(type) {
|
||||||
|
case *syscall.SockaddrInet4:
|
||||||
|
ip = net.IPv4(sa.Addr[0], sa.Addr[1], sa.Addr[2], sa.Addr[3])
|
||||||
|
case *syscall.SockaddrInet6:
|
||||||
|
ip = make(net.IP, net.IPv6len)
|
||||||
|
copy(ip, sa.Addr[:])
|
||||||
|
if ip[0] == 0xfe && ip[1] == 0xc0 {
|
||||||
|
// Ignore these fec0/10 ones. Windows seems to
|
||||||
|
// populate them as defaults on its misc rando
|
||||||
|
// interfaces.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
||||||
|
if ip.IsLoopback() || seen[ip.String()] {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seen[ip.String()] = true
|
||||||
|
ns = append(ns, ip.String())
|
||||||
|
}
|
||||||
for _, aa := range aas {
|
for _, aa := range aas {
|
||||||
for dns := aa.FirstDnsServerAddress; dns != nil; dns = dns.Next {
|
for dns := aa.FirstDNSServerAddress; dns != nil; dns = dns.Next {
|
||||||
sa, err := dns.Address.Sockaddr.Sockaddr()
|
do(dns.Address)
|
||||||
if err != nil {
|
}
|
||||||
continue
|
for gw := aa.FirstGatewayAddress; gw != nil; gw = gw.Next {
|
||||||
}
|
do(gw.Address)
|
||||||
var ip net.IP
|
|
||||||
switch sa := sa.(type) {
|
|
||||||
case *syscall.SockaddrInet4:
|
|
||||||
ip = net.IPv4(sa.Addr[0], sa.Addr[1], sa.Addr[2], sa.Addr[3])
|
|
||||||
case *syscall.SockaddrInet6:
|
|
||||||
ip = make(net.IP, net.IPv6len)
|
|
||||||
copy(ip, sa.Addr[:])
|
|
||||||
if ip[0] == 0xfe && ip[1] == 0xc0 {
|
|
||||||
// Ignore these fec0/10 ones. Windows seems to
|
|
||||||
// populate them as defaults on its misc rando
|
|
||||||
// interfaces.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
// Unexpected type.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ns = append(ns, net.JoinHostPort(ip.String(), "53"))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ns
|
return ns
|
||||||
}
|
}
|
||||||
|
|
||||||
func adapterAddresses() ([]*windows.IpAdapterAddresses, error) {
|
|
||||||
var b []byte
|
|
||||||
l := uint32(15000) // recommended initial size
|
|
||||||
for {
|
|
||||||
b = make([]byte, l)
|
|
||||||
err := windows.GetAdaptersAddresses(syscall.AF_UNSPEC, windows.GAA_FLAG_INCLUDE_PREFIX, 0, (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])), &l)
|
|
||||||
if err == nil {
|
|
||||||
if l == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if err.(syscall.Errno) != syscall.ERROR_BUFFER_OVERFLOW {
|
|
||||||
return nil, os.NewSyscallError("getadaptersaddresses", err)
|
|
||||||
}
|
|
||||||
if l <= uint32(len(b)) {
|
|
||||||
return nil, os.NewSyscallError("getadaptersaddresses", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var aas []*windows.IpAdapterAddresses
|
|
||||||
for aa := (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])); aa != nil; aa = aa.Next {
|
|
||||||
aas = append(aas, aa)
|
|
||||||
}
|
|
||||||
return aas, nil
|
|
||||||
}
|
|
||||||
|
|||||||
+87
-7
@@ -5,6 +5,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
@@ -32,7 +34,7 @@ var errUnknownResolver = errors.New("unknown resolver")
|
|||||||
|
|
||||||
// NewResolver creates a Resolver based on the given upstream config.
|
// NewResolver creates a Resolver based on the given upstream config.
|
||||||
func NewResolver(uc *UpstreamConfig) (Resolver, error) {
|
func NewResolver(uc *UpstreamConfig) (Resolver, error) {
|
||||||
typ, endpoint := uc.Type, uc.Endpoint
|
typ := uc.Type
|
||||||
switch typ {
|
switch typ {
|
||||||
case ResolverTypeDOH, ResolverTypeDOH3:
|
case ResolverTypeDOH, ResolverTypeDOH3:
|
||||||
return newDohResolver(uc), nil
|
return newDohResolver(uc), nil
|
||||||
@@ -43,7 +45,7 @@ func NewResolver(uc *UpstreamConfig) (Resolver, error) {
|
|||||||
case ResolverTypeOS:
|
case ResolverTypeOS:
|
||||||
return or, nil
|
return or, nil
|
||||||
case ResolverTypeLegacy:
|
case ResolverTypeLegacy:
|
||||||
return &legacyResolver{endpoint: endpoint}, nil
|
return &legacyResolver{uc: uc}, nil
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("%w: %s", errUnknownResolver, typ)
|
return nil, fmt.Errorf("%w: %s", errUnknownResolver, typ)
|
||||||
}
|
}
|
||||||
@@ -69,9 +71,16 @@ func (o *osResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error
|
|||||||
|
|
||||||
dnsClient := &dns.Client{Net: "udp"}
|
dnsClient := &dns.Client{Net: "udp"}
|
||||||
ch := make(chan *osResolverResult, numServers)
|
ch := make(chan *osResolverResult, numServers)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(len(o.nameservers))
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(ch)
|
||||||
|
}()
|
||||||
for _, server := range o.nameservers {
|
for _, server := range o.nameservers {
|
||||||
go func(server string) {
|
go func(server string) {
|
||||||
answer, _, err := dnsClient.ExchangeContext(ctx, msg, server)
|
defer wg.Done()
|
||||||
|
answer, _, err := dnsClient.ExchangeContext(ctx, msg.Copy(), server)
|
||||||
ch <- &osResolverResult{answer: answer, err: err}
|
ch <- &osResolverResult{answer: answer, err: err}
|
||||||
}(server)
|
}(server)
|
||||||
}
|
}
|
||||||
@@ -85,7 +94,7 @@ func (o *osResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error
|
|||||||
errs = append(errs, res.err)
|
errs = append(errs, res.err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, joinErrors(errs...)
|
return nil, errors.Join(errs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDialer(dnsAddress string) *net.Dialer {
|
func newDialer(dnsAddress string) *net.Dialer {
|
||||||
@@ -101,16 +110,87 @@ func newDialer(dnsAddress string) *net.Dialer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type legacyResolver struct {
|
type legacyResolver struct {
|
||||||
endpoint string
|
uc *UpstreamConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *legacyResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) {
|
func (r *legacyResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) {
|
||||||
// See comment in (*dotResolver).resolve method.
|
// See comment in (*dotResolver).resolve method.
|
||||||
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
|
||||||
|
}
|
||||||
|
_, udpNet := r.uc.netForDNSType(dnsTyp)
|
||||||
dnsClient := &dns.Client{
|
dnsClient := &dns.Client{
|
||||||
Net: "udp",
|
Net: udpNet,
|
||||||
Dialer: dialer,
|
Dialer: dialer,
|
||||||
}
|
}
|
||||||
answer, _, err := dnsClient.ExchangeContext(ctx, msg, r.endpoint)
|
answer, _, err := dnsClient.ExchangeContext(ctx, msg, r.uc.Endpoint)
|
||||||
return answer, err
|
return answer, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LookupIP looks up host using OS resolver.
|
||||||
|
// It returns a slice of that host's IPv4 and IPv6 addresses.
|
||||||
|
func LookupIP(domain string) []string {
|
||||||
|
return lookupIP(domain, -1, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupIP(domain string, timeout int, withBootstrapDNS bool) (ips []string) {
|
||||||
|
resolver := &osResolver{nameservers: nameservers()}
|
||||||
|
if withBootstrapDNS {
|
||||||
|
resolver.nameservers = append([]string{net.JoinHostPort(bootstrapDNS, "53")}, resolver.nameservers...)
|
||||||
|
}
|
||||||
|
ProxyLog.Debug().Msgf("Resolving %q using bootstrap DNS %q", domain, resolver.nameservers)
|
||||||
|
timeoutMs := 2000
|
||||||
|
if timeout > 0 && timeout < timeoutMs {
|
||||||
|
timeoutMs = timeout
|
||||||
|
}
|
||||||
|
questionDomain := dns.Fqdn(domain)
|
||||||
|
ipFromRecord := func(record dns.RR) string {
|
||||||
|
switch ar := record.(type) {
|
||||||
|
case *dns.A:
|
||||||
|
if ar.Hdr.Name != questionDomain {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return ar.A.String()
|
||||||
|
case *dns.AAAA:
|
||||||
|
if ar.Hdr.Name != questionDomain {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return ar.AAAA.String()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
lookup := func(dnsType uint16) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutMs)*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetQuestion(questionDomain, dnsType)
|
||||||
|
m.RecursionDesired = true
|
||||||
|
|
||||||
|
r, err := resolver.Resolve(ctx, m)
|
||||||
|
if err != nil {
|
||||||
|
ProxyLog.Error().Err(err).Msgf("could not lookup %q record for domain %q", dns.TypeToString[dnsType], domain)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Rcode != dns.RcodeSuccess {
|
||||||
|
ProxyLog.Error().Msgf("could not resolve domain %q, return code: %s", domain, dns.RcodeToString[r.Rcode])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(r.Answer) == 0 {
|
||||||
|
ProxyLog.Error().Msg("no answer from OS resolver")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, a := range r.Answer {
|
||||||
|
if ip := ipFromRecord(a); ip != "" {
|
||||||
|
ips = append(ips, ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Find all A, AAAA records of the domain.
|
||||||
|
for _, dnsType := range []uint16{dns.TypeAAAA, dns.TypeA} {
|
||||||
|
lookup(dnsType)
|
||||||
|
}
|
||||||
|
return ips
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package ctrld
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_osResolver_Resolve(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer cancel()
|
||||||
|
resolver := &osResolver{nameservers: []string{"127.0.0.127:5353"}}
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetQuestion("controld.com.", dns.TypeA)
|
||||||
|
m.RecursionDesired = true
|
||||||
|
_, _ = resolver.Resolve(context.Background(), m)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-time.After(10 * time.Second):
|
||||||
|
t.Error("os resolver hangs")
|
||||||
|
case <-ctx.Done():
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_upstreamTypeFromEndpoint(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
endpoint string
|
||||||
|
resolverType string
|
||||||
|
}{
|
||||||
|
{"doh", "https://freedns.controld.com/p2", ResolverTypeDOH},
|
||||||
|
{"doq", "quic://p2.freedns.controld.com", ResolverTypeDOQ},
|
||||||
|
{"dot", "p2.freedns.controld.com", ResolverTypeDOT},
|
||||||
|
{"legacy", "8.8.8.8:53", ResolverTypeLegacy},
|
||||||
|
{"legacy ipv6", "[2404:6800:4005:809::200e]:53", ResolverTypeLegacy},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if rt := ResolverTypeFromEndpoint(tc.endpoint); rt != tc.resolverType {
|
||||||
|
t.Errorf("mismatch, want: %s, got: %s", tc.resolverType, rt)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,6 +50,13 @@ type = "legacy"
|
|||||||
endpoint = "8.8.8.8"
|
endpoint = "8.8.8.8"
|
||||||
timeout = 5
|
timeout = 5
|
||||||
|
|
||||||
|
[upstream.3]
|
||||||
|
name = "DOH with client info"
|
||||||
|
type = "doh"
|
||||||
|
endpoint = "https://dns.controld.com/client_info_upstream/main-device"
|
||||||
|
timeout = 5
|
||||||
|
send_client_info = false
|
||||||
|
|
||||||
[listener.0]
|
[listener.0]
|
||||||
ip = "127.0.0.1"
|
ip = "127.0.0.1"
|
||||||
port = 53
|
port = 53
|
||||||
|
|||||||
Reference in New Issue
Block a user