mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-02-03 22:18:39 +00:00
Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c22701940 | ||
|
|
a77a924320 | ||
|
|
95dbf71939 | ||
|
|
8869e33a20 | ||
|
|
c94e1b02d2 | ||
|
|
42d29b626b | ||
|
|
b65a5ac283 | ||
|
|
ba48ff5965 | ||
|
|
b3a342bc44 | ||
|
|
9927803497 | ||
|
|
f0c604a9f1 | ||
|
|
8a56389396 | ||
|
|
9f7bfc76db | ||
|
|
a7a5501ea5 | ||
|
|
c401c4ef87 | ||
|
|
8ffb42962a | ||
|
|
aad04200cb | ||
|
|
4bfcacaf3c | ||
|
|
5b362412be | ||
|
|
ccf07a7d1c | ||
|
|
e4eb3b2ded | ||
|
|
77b62f8734 | ||
|
|
096e7ea429 | ||
|
|
3e6f6cc721 | ||
|
|
7dab688252 | ||
|
|
7cd1f7adda | ||
|
|
9a249c3029 | ||
|
|
0dfa377e08 | ||
|
|
14bc29751f | ||
|
|
e6800fbc82 | ||
|
|
4f6c2032a1 | ||
|
|
d1589bd9d6 | ||
|
|
85c95a6a3a | ||
|
|
fa50cd4df4 | ||
|
|
018f6651c1 | ||
|
|
1a40767cb7 | ||
|
|
12512a60da | ||
|
|
b0114dfaeb | ||
|
|
fb20d443c1 | ||
|
|
262dcb1dff | ||
|
|
8b08cc8a6e | ||
|
|
930a5ad439 | ||
|
|
8852f60ccb | ||
|
|
2e1b3f9d07 | ||
|
|
6d3c82d38d | ||
|
|
cad71997aa | ||
|
|
82900eeca6 | ||
|
|
84fca06c62 | ||
|
|
64f2dcb25b | ||
|
|
4c2d21a8f8 | ||
|
|
4172fc09d0 | ||
|
|
d9b699501d | ||
|
|
71b1b324db | ||
|
|
35c890048b | ||
|
|
bac6810956 | ||
|
|
997ec342e0 | ||
|
|
e385547461 | ||
|
|
83b551fb2d | ||
|
|
45f827a2c5 | ||
|
|
3218b5fac1 | ||
|
|
df514d15a5 | ||
|
|
50b0e5a4b0 | ||
|
|
6428ac23a0 | ||
|
|
790cb773e2 | ||
|
|
9dab097268 | ||
|
|
f13f61592c | ||
|
|
2f42fc055d | ||
|
|
1cdce73070 | ||
|
|
5f9ac5889b |
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -9,18 +9,18 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: ["windows-latest", "ubuntu-latest", "macOS-latest"]
|
||||
go: ["1.19.x"]
|
||||
go: ["1.20.x"]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- uses: WillAbides/setup-go-faster@v1.7.0
|
||||
- uses: WillAbides/setup-go-faster@v1.8.0
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
- run: "go test -race ./..."
|
||||
- uses: dominikh/staticcheck-action@v1.2.0
|
||||
with:
|
||||
version: "2022.1.1"
|
||||
version: "2023.1.2"
|
||||
install-go: false
|
||||
cache-key: ${{ matrix.go }}
|
||||
|
||||
@@ -9,6 +9,8 @@ builds:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -s -w
|
||||
- -X main.version={{.Version}}
|
||||
- -X main.commit={{.Commit}}
|
||||
goos:
|
||||
- darwin
|
||||
goarch:
|
||||
|
||||
42
.goreleaser-qf.yaml
Normal file
42
.goreleaser-qf.yaml
Normal file
@@ -0,0 +1,42 @@
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
builds:
|
||||
- id: ctrld
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -s -w
|
||||
- -X main.version={{.Version}}
|
||||
- -X main.commit={{.Commit}}
|
||||
goos:
|
||||
- darwin
|
||||
- linux
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
tags:
|
||||
- qf
|
||||
main: ./cmd/ctrld
|
||||
hooks:
|
||||
post: /bin/sh ./scripts/upx.sh {{ .Path }}
|
||||
archives:
|
||||
- format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
strip_parent_binary_folder: true
|
||||
wrap_in_directory: true
|
||||
files:
|
||||
- README.md
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
snapshot:
|
||||
name_template: "{{ incpatch .Version }}-next"
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
@@ -9,8 +9,11 @@ builds:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -s -w
|
||||
- -X main.version={{.Version}}
|
||||
- -X main.commit={{.Commit}}
|
||||
goos:
|
||||
- linux
|
||||
- freebsd
|
||||
- windows
|
||||
goarch:
|
||||
- 386
|
||||
@@ -25,6 +28,12 @@ builds:
|
||||
gomips:
|
||||
- softfloat
|
||||
main: ./cmd/ctrld
|
||||
hooks:
|
||||
post: /bin/sh ./scripts/upx.sh {{ .Path }}
|
||||
ignore:
|
||||
- goos: freebsd
|
||||
goarch: arm
|
||||
goarm: 5
|
||||
archives:
|
||||
- format_overrides:
|
||||
- goos: windows
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
# ctrld
|
||||
|
||||

|
||||
[](https://pkg.go.dev/github.com/Control-D-Inc/ctrld)
|
||||
[](https://goreportcard.com/report/github.com/Control-D-Inc/ctrld)
|
||||
|
||||
A highly configurable DNS forwarding proxy with support for:
|
||||
- Multiple listeners for incoming queries
|
||||
- Multiple upstreams with fallbacks
|
||||
|
||||
230
cmd/ctrld/cli.go
230
cmd/ctrld/cli.go
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -14,16 +15,31 @@ import (
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
|
||||
"github.com/cuonglm/osinfo"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/kardianos/service"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/interfaces"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld"
|
||||
"github.com/Control-D-Inc/ctrld/internal/controld"
|
||||
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
||||
)
|
||||
|
||||
const selfCheckFQDN = "verify.controld.com"
|
||||
|
||||
var (
|
||||
version = "dev"
|
||||
commit = "none"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -52,16 +68,28 @@ _/ ___\ __\_ __ \ | / __ |
|
||||
\/ dns forwarding proxy \/
|
||||
`
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "ctrld",
|
||||
Short: strings.TrimLeft(rootShortDesc, "\n"),
|
||||
Version: curVersion(),
|
||||
}
|
||||
|
||||
func curVersion() string {
|
||||
if version != "dev" {
|
||||
version = "v" + version
|
||||
}
|
||||
if len(commit) > 7 {
|
||||
commit = commit[:7]
|
||||
}
|
||||
return fmt.Sprintf("%s-%s", version, commit)
|
||||
}
|
||||
|
||||
func initCLI() {
|
||||
// Enable opening via explorer.exe on Windows.
|
||||
// See: https://github.com/spf13/cobra/issues/844.
|
||||
cobra.MousetrapHelpText = ""
|
||||
cobra.EnableCommandSorting = false
|
||||
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "ctrld",
|
||||
Short: strings.TrimLeft(rootShortDesc, "\n"),
|
||||
Version: "1.1.0",
|
||||
}
|
||||
rootCmd.PersistentFlags().CountVarP(
|
||||
&verbose,
|
||||
"verbose",
|
||||
@@ -80,6 +108,34 @@ func initCLI() {
|
||||
log.Fatal("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")
|
||||
}
|
||||
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)
|
||||
writeDefaultConfig := !noConfigStart && configBase64 == ""
|
||||
configs := []struct {
|
||||
@@ -103,8 +159,13 @@ func initCLI() {
|
||||
if err := v.Unmarshal(&cfg); err != nil {
|
||||
log.Fatalf("failed to unmarshal config: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("starting ctrld %s\n", curVersion())
|
||||
oi := osinfo.New()
|
||||
log.Printf("os: %s\n", oi.String())
|
||||
|
||||
// Wait for network up.
|
||||
if !netUp() {
|
||||
if !ctrldnet.Up() {
|
||||
log.Fatal("network is not up yet")
|
||||
}
|
||||
processLogAndCacheFlags()
|
||||
@@ -139,22 +200,8 @@ func initCLI() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
s, err := service.New(&prog{}, svcConfig)
|
||||
if err != nil {
|
||||
mainLog.Fatal().Err(err).Msg("failed create new service")
|
||||
}
|
||||
serviceLogger, err := s.Logger(nil)
|
||||
if err != nil {
|
||||
mainLog.Error().Err(err).Msg("failed to get service logger")
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.Run(); err != nil {
|
||||
if sErr := serviceLogger.Error(err); sErr != nil {
|
||||
mainLog.Error().Err(sErr).Msg("failed to write service log")
|
||||
}
|
||||
mainLog.Error().Err(err).Msg("failed to start service")
|
||||
}
|
||||
close(waitCh)
|
||||
<-stopCh
|
||||
},
|
||||
}
|
||||
runCmd.Flags().BoolVarP(&daemon, "daemon", "d", false, "Run as daemon")
|
||||
@@ -177,7 +224,7 @@ func initCLI() {
|
||||
startCmd := &cobra.Command{
|
||||
PreRun: checkHasElevatedPrivilege,
|
||||
Use: "start",
|
||||
Short: "Start the ctrld service",
|
||||
Short: "Install and start the ctrld service",
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
sc := &service.Config{}
|
||||
@@ -188,20 +235,32 @@ func initCLI() {
|
||||
}
|
||||
setDependencies(sc)
|
||||
sc.Arguments = append([]string{"run"}, osArgs...)
|
||||
|
||||
// No config path, generating config in HOME directory.
|
||||
noConfigStart := isNoConfigStart(cmd)
|
||||
writeDefaultConfig := !noConfigStart && configBase64 == ""
|
||||
if configPath != "" {
|
||||
v.SetConfigFile(configPath)
|
||||
}
|
||||
if dir, err := os.UserHomeDir(); err == nil {
|
||||
// WorkingDirectory is not supported on Windows.
|
||||
sc.WorkingDirectory = dir
|
||||
// No config path, generating config in HOME directory.
|
||||
noConfigStart := isNoConfigStart(cmd)
|
||||
writeDefaultConfig := !noConfigStart && configBase64 == ""
|
||||
setWorkingDirectory(sc, dir)
|
||||
if configPath == "" && writeDefaultConfig {
|
||||
defaultConfigFile = filepath.Join(dir, defaultConfigFile)
|
||||
readConfigFile(writeDefaultConfig && cdUID == "")
|
||||
v.SetConfigFile(defaultConfigFile)
|
||||
}
|
||||
sc.Arguments = append(sc.Arguments, "--homedir="+dir)
|
||||
}
|
||||
|
||||
readConfigFile(writeDefaultConfig && cdUID == "")
|
||||
if err := v.Unmarshal(&cfg); err != nil {
|
||||
log.Fatalf("failed to unmarshal config: %v", err)
|
||||
}
|
||||
|
||||
logPath := cfg.Service.LogPath
|
||||
cfg.Service.LogPath = ""
|
||||
initLogging()
|
||||
cfg.Service.LogPath = logPath
|
||||
|
||||
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.
|
||||
@@ -224,8 +283,24 @@ func initCLI() {
|
||||
{s.Start, true},
|
||||
}
|
||||
if doTasks(tasks) {
|
||||
status, err := s.Status()
|
||||
if err != nil {
|
||||
mainLog.Warn().Err(err).Msg("could not get service status")
|
||||
return
|
||||
}
|
||||
|
||||
status = selfCheckStatus(status)
|
||||
switch status {
|
||||
case service.StatusRunning:
|
||||
mainLog.Info().Msg("Service started")
|
||||
default:
|
||||
mainLog.Error().Msg("Service did not start, please check system/service log for details error")
|
||||
if runtime.GOOS == "linux" {
|
||||
prog.resetDNS()
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
prog.setDNS()
|
||||
mainLog.Info().Msg("Service started")
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -293,24 +368,34 @@ func initCLI() {
|
||||
status, err := s.Status()
|
||||
if err != nil {
|
||||
stderrMsg(err.Error())
|
||||
return
|
||||
os.Exit(1)
|
||||
}
|
||||
switch status {
|
||||
case service.StatusUnknown:
|
||||
stdoutMsg("Unknown status")
|
||||
os.Exit(2)
|
||||
case service.StatusRunning:
|
||||
stdoutMsg("Service is running")
|
||||
os.Exit(0)
|
||||
case service.StatusStopped:
|
||||
stdoutMsg("Service is stopped")
|
||||
os.Exit(1)
|
||||
}
|
||||
},
|
||||
}
|
||||
if runtime.GOOS == "darwin" {
|
||||
// On darwin, running status command without privileges may return wrong information.
|
||||
statusCmd.PreRun = checkHasElevatedPrivilege
|
||||
}
|
||||
|
||||
uninstallCmd := &cobra.Command{
|
||||
PreRun: checkHasElevatedPrivilege,
|
||||
Use: "uninstall",
|
||||
Short: "Uninstall the ctrld service",
|
||||
Args: cobra.NoArgs,
|
||||
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) {
|
||||
prog := &prog{}
|
||||
s, err := service.New(prog, svcConfig)
|
||||
@@ -324,13 +409,16 @@ func initCLI() {
|
||||
}
|
||||
initLogging()
|
||||
if doTasks(tasks) {
|
||||
if iface == "" {
|
||||
iface = "auto"
|
||||
}
|
||||
prog.resetDNS()
|
||||
mainLog.Info().Msg("Service uninstalled")
|
||||
return
|
||||
}
|
||||
},
|
||||
}
|
||||
uninstallCmd.Flags().StringVarP(&iface, "iface", "", "auto", `Reset DNS setting for iface, "auto" means the default interface gateway`)
|
||||
uninstallCmd.Flags().StringVarP(&iface, "iface", "", "", `Reset DNS setting for iface, use "auto" for the default gateway interface`)
|
||||
|
||||
listIfacesCmd := &cobra.Command{
|
||||
Use: "list",
|
||||
@@ -432,6 +520,8 @@ func initCLI() {
|
||||
func writeConfigFile() error {
|
||||
if cfu := v.ConfigFileUsed(); cfu != "" {
|
||||
defaultConfigFile = cfu
|
||||
} else if configPath != "" {
|
||||
defaultConfigFile = configPath
|
||||
}
|
||||
f, err := os.OpenFile(defaultConfigFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, os.FileMode(0o644))
|
||||
if err != nil {
|
||||
@@ -444,7 +534,7 @@ func writeConfigFile() error {
|
||||
}
|
||||
}
|
||||
enc := toml.NewEncoder(f).SetIndentTables(true)
|
||||
if err := enc.Encode(v.AllSettings()); err != nil {
|
||||
if err := enc.Encode(&cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
@@ -457,7 +547,7 @@ func readConfigFile(writeDefaultConfig bool) bool {
|
||||
// If err == nil, there's a config supplied via `--config`, no default config written.
|
||||
err := v.ReadInConfig()
|
||||
if err == nil {
|
||||
fmt.Println("loading config file from:", v.ConfigFileUsed())
|
||||
log.Println("loading config file from:", v.ConfigFileUsed())
|
||||
defaultConfigFile = v.ConfigFileUsed()
|
||||
return true
|
||||
}
|
||||
@@ -471,7 +561,7 @@ func readConfigFile(writeDefaultConfig bool) bool {
|
||||
if err := writeConfigFile(); err != nil {
|
||||
log.Fatalf("failed to write default config file: %v", err)
|
||||
} else {
|
||||
fmt.Println("writing default config file to: " + defaultConfigFile)
|
||||
log.Println("writing default config file to: " + defaultConfigFile)
|
||||
}
|
||||
defaultConfigWritten = true
|
||||
return false
|
||||
@@ -503,18 +593,24 @@ func processNoConfigFlags(noConfigStart bool) {
|
||||
}
|
||||
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{
|
||||
"0": {
|
||||
Name: primaryUpstream,
|
||||
Endpoint: primaryUpstream,
|
||||
Type: ctrld.ResolverTypeDOH,
|
||||
Name: pEndpoint,
|
||||
Endpoint: pEndpoint,
|
||||
Type: pType,
|
||||
},
|
||||
}
|
||||
if secondaryUpstream != "" {
|
||||
sEndpoint, sType := endpointAndTyp(secondaryUpstream)
|
||||
upstream["1"] = &ctrld.UpstreamConfig{
|
||||
Name: secondaryUpstream,
|
||||
Endpoint: secondaryUpstream,
|
||||
Type: ctrld.ResolverTypeLegacy,
|
||||
Name: sEndpoint,
|
||||
Endpoint: sEndpoint,
|
||||
Type: sType,
|
||||
}
|
||||
rules := make([]ctrld.Rule, 0, len(domains))
|
||||
for _, domain := range domains {
|
||||
@@ -534,7 +630,7 @@ func processCDFlags() {
|
||||
iface = "auto"
|
||||
}
|
||||
logger := mainLog.With().Str("mode", "cd").Logger()
|
||||
logger.Info().Msg("fetching Controld-D configuration")
|
||||
logger.Info().Msgf("fetching Controld D configuration from API: %s", cdUID)
|
||||
resolverConfig, err := controld.FetchResolverConfig(cdUID)
|
||||
if uer, ok := err.(*controld.UtilityErrorResponse); ok && uer.ErrorField.Code == controld.InvalidConfigCode {
|
||||
s, err := service.New(&prog{}, svcConfig)
|
||||
@@ -594,10 +690,6 @@ func processCDFlags() {
|
||||
},
|
||||
}
|
||||
|
||||
v = viper.NewWithOptions(viper.KeyDelimiter("::"))
|
||||
v.Set("network", cfg.Network)
|
||||
v.Set("upstream", cfg.Upstream)
|
||||
v.Set("listener", cfg.Listener)
|
||||
processLogAndCacheFlags()
|
||||
if err := writeConfigFile(); err != nil {
|
||||
logger.Fatal().Err(err).Msg("failed to write config file")
|
||||
@@ -666,3 +758,45 @@ func defaultIfaceName() string {
|
||||
}
|
||||
return dri
|
||||
}
|
||||
|
||||
func selfCheckStatus(status service.Status) service.Status {
|
||||
c := new(dns.Client)
|
||||
bo := backoff.NewBackoff("self-check", logf, 10*time.Second)
|
||||
bo.LogLongerThan = 500 * time.Millisecond
|
||||
ctx := context.Background()
|
||||
err := errors.New("query failed")
|
||||
maxAttempts := 20
|
||||
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 {
|
||||
log.Printf("failed to unmarshal listener config: %v", err)
|
||||
return
|
||||
}
|
||||
})
|
||||
v.WatchConfig()
|
||||
for i := 0; i < maxAttempts; i++ {
|
||||
lc := cfg.Listener["0"]
|
||||
mu.Lock()
|
||||
if lcChanged != nil {
|
||||
lc = lcChanged["0"]
|
||||
}
|
||||
mu.Unlock()
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion(selfCheckFQDN+".", dns.TypeA)
|
||||
m.RecursionDesired = true
|
||||
r, _, _ := c.ExchangeContext(ctx, m, net.JoinHostPort(lc.IP, strconv.Itoa(lc.Port)))
|
||||
if r != nil && r.Rcode == dns.RcodeSuccess && len(r.Answer) > 0 {
|
||||
mainLog.Debug().Msgf("self-check against %q succeeded", selfCheckFQDN)
|
||||
return status
|
||||
}
|
||||
bo.BackOff(ctx, err)
|
||||
}
|
||||
mainLog.Debug().Msgf("self-check against %q failed", selfCheckFQDN)
|
||||
return service.StatusUnknown
|
||||
}
|
||||
|
||||
23
cmd/ctrld/cli_test.go
Normal file
23
cmd/ctrld/cli_test.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_writeConfigFile(t *testing.T) {
|
||||
tmpdir := t.TempDir()
|
||||
// simulate --config CLI flag by setting configPath manually.
|
||||
configPath = filepath.Join(tmpdir, "ctrld.toml")
|
||||
_, err := os.Stat(configPath)
|
||||
assert.True(t, os.IsNotExist(err))
|
||||
|
||||
assert.NoError(t, writeConfigFile())
|
||||
|
||||
_, err = os.Stat(configPath)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -12,14 +12,16 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld"
|
||||
"github.com/Control-D-Inc/ctrld/internal/dnscache"
|
||||
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
||||
)
|
||||
|
||||
const staleTTL = 60 * time.Second
|
||||
|
||||
func (p *prog) serveUDP(listenerNum string) error {
|
||||
func (p *prog) serveDNS(listenerNum string) error {
|
||||
listenerConfig := p.cfg.Listener[listenerNum]
|
||||
// make sure ip is allocated
|
||||
if allocErr := p.allocateIP(listenerConfig.IP); allocErr != nil {
|
||||
@@ -31,12 +33,13 @@ func (p *prog) serveUDP(listenerNum string) error {
|
||||
failoverRcodes = listenerConfig.Policy.FailoverRcodeNumbers
|
||||
}
|
||||
handler := dns.HandlerFunc(func(w dns.ResponseWriter, m *dns.Msg) {
|
||||
domain := canonicalName(m.Question[0].Name)
|
||||
q := m.Question[0]
|
||||
domain := canonicalName(q.Name)
|
||||
reqId := requestID()
|
||||
fmtSrcToDest := fmtRemoteToLocal(listenerNum, w.RemoteAddr().String(), w.LocalAddr().String())
|
||||
t := time.Now()
|
||||
ctx := context.WithValue(context.Background(), ctrld.ReqIdCtxKey{}, reqId)
|
||||
ctrld.Log(ctx, proxyLog.Debug(), "%s received query: %s", fmtSrcToDest, domain)
|
||||
ctrld.Log(ctx, mainLog.Debug(), "%s received query: %s %s", fmtSrcToDest, dns.TypeToString[q.Qtype], domain)
|
||||
upstreams, matched := p.upstreamFor(ctx, listenerNum, listenerConfig, w.RemoteAddr(), domain)
|
||||
var answer *dns.Msg
|
||||
if !matched && listenerConfig.Restricted {
|
||||
@@ -46,32 +49,53 @@ func (p *prog) serveUDP(listenerNum string) error {
|
||||
} else {
|
||||
answer = p.proxy(ctx, upstreams, failoverRcodes, m)
|
||||
rtt := time.Since(t)
|
||||
ctrld.Log(ctx, proxyLog.Debug(), "received response of %d bytes in %s", answer.Len(), rtt)
|
||||
ctrld.Log(ctx, mainLog.Debug(), "received response of %d bytes in %s", answer.Len(), rtt)
|
||||
}
|
||||
if err := w.WriteMsg(answer); err != nil {
|
||||
ctrld.Log(ctx, mainLog.Error().Err(err), "serveUDP: failed to send DNS response to client")
|
||||
}
|
||||
})
|
||||
|
||||
// On Windows, there's no easy way for disabling/removing IPv6 DNS resolver, so we check whether we can
|
||||
// listen on ::1, then spawn a listener for receiving DNS requests.
|
||||
if runtime.GOOS == "windows" && supportsIPv6ListenLocal() {
|
||||
go func() {
|
||||
g, ctx := errgroup.WithContext(context.Background())
|
||||
for _, proto := range []string{"udp", "tcp"} {
|
||||
proto := proto
|
||||
// On Windows, there's no easy way for disabling/removing IPv6 DNS resolver, so we check whether we can
|
||||
// listen on ::1, then spawn a listener for receiving DNS requests.
|
||||
if runtime.GOOS == "windows" && ctrldnet.SupportsIPv6ListenLocal() {
|
||||
g.Go(func() error {
|
||||
s := &dns.Server{
|
||||
Addr: net.JoinHostPort("::1", strconv.Itoa(listenerConfig.Port)),
|
||||
Net: proto,
|
||||
Handler: handler,
|
||||
}
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
_ = s.Shutdown()
|
||||
}()
|
||||
if err := s.ListenAndServe(); err != nil {
|
||||
mainLog.Error().Err(err).Msg("could not serving on ::1")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
g.Go(func() error {
|
||||
s := &dns.Server{
|
||||
Addr: net.JoinHostPort("::1", strconv.Itoa(listenerConfig.Port)),
|
||||
Net: "udp",
|
||||
Addr: net.JoinHostPort(listenerConfig.IP, strconv.Itoa(listenerConfig.Port)),
|
||||
Net: proto,
|
||||
Handler: handler,
|
||||
}
|
||||
_ = s.ListenAndServe()
|
||||
}()
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
_ = s.Shutdown()
|
||||
}()
|
||||
if err := s.ListenAndServe(); err != nil {
|
||||
mainLog.Error().Err(err).Msgf("could not listen and serve on: %s", s.Addr)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
s := &dns.Server{
|
||||
Addr: net.JoinHostPort(listenerConfig.IP, strconv.Itoa(listenerConfig.Port)),
|
||||
Net: "udp",
|
||||
Handler: handler,
|
||||
}
|
||||
return s.ListenAndServe()
|
||||
return g.Wait()
|
||||
}
|
||||
|
||||
func (p *prog) upstreamFor(ctx context.Context, defaultUpstreamNum string, lc *ctrld.ListenerConfig, addr net.Addr, domain string) ([]string, bool) {
|
||||
@@ -83,10 +107,10 @@ func (p *prog) upstreamFor(ctx context.Context, defaultUpstreamNum string, lc *c
|
||||
|
||||
defer func() {
|
||||
if !matched && lc.Restricted {
|
||||
ctrld.Log(ctx, proxyLog.Info(), "query refused, %s does not match any network policy", addr.String())
|
||||
ctrld.Log(ctx, mainLog.Info(), "query refused, %s does not match any network policy", addr.String())
|
||||
return
|
||||
}
|
||||
ctrld.Log(ctx, proxyLog.Info(), "%s, %s, %s -> %v", matchedPolicy, matchedNetwork, matchedRule, upstreams)
|
||||
ctrld.Log(ctx, mainLog.Info(), "%s, %s, %s -> %v", matchedPolicy, matchedNetwork, matchedRule, upstreams)
|
||||
}()
|
||||
|
||||
if lc.Policy == nil {
|
||||
@@ -159,19 +183,19 @@ func (p *prog) proxy(ctx context.Context, upstreams []string, failoverRcodes []i
|
||||
answer.SetRcode(msg, answer.Rcode)
|
||||
now := time.Now()
|
||||
if cachedValue.Expire.After(now) {
|
||||
ctrld.Log(ctx, proxyLog.Debug(), "hit cached response")
|
||||
ctrld.Log(ctx, mainLog.Debug(), "hit cached response")
|
||||
setCachedAnswerTTL(answer, now, cachedValue.Expire)
|
||||
return answer
|
||||
}
|
||||
staleAnswer = answer
|
||||
}
|
||||
}
|
||||
resolve := func(n int, upstreamConfig *ctrld.UpstreamConfig, msg *dns.Msg) *dns.Msg {
|
||||
ctrld.Log(ctx, proxyLog.Debug(), "sending query to %s: %s", upstreams[n], upstreamConfig.Name)
|
||||
resolve1 := func(n int, upstreamConfig *ctrld.UpstreamConfig, msg *dns.Msg) (*dns.Msg, error) {
|
||||
ctrld.Log(ctx, mainLog.Debug(), "sending query to %s: %s", upstreams[n], upstreamConfig.Name)
|
||||
dnsResolver, err := ctrld.NewResolver(upstreamConfig)
|
||||
if err != nil {
|
||||
ctrld.Log(ctx, proxyLog.Error().Err(err), "failed to create resolver")
|
||||
return nil
|
||||
ctrld.Log(ctx, mainLog.Error().Err(err), "failed to create resolver")
|
||||
return nil, err
|
||||
}
|
||||
resolveCtx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
@@ -180,9 +204,19 @@ func (p *prog) proxy(ctx context.Context, upstreams []string, failoverRcodes []i
|
||||
defer cancel()
|
||||
resolveCtx = timeoutCtx
|
||||
}
|
||||
answer, err := dnsResolver.Resolve(resolveCtx, msg)
|
||||
return dnsResolver.Resolve(resolveCtx, msg)
|
||||
}
|
||||
resolve := func(n int, upstreamConfig *ctrld.UpstreamConfig, msg *dns.Msg) *dns.Msg {
|
||||
answer, err := resolve1(n, upstreamConfig, msg)
|
||||
if err != nil {
|
||||
ctrld.Log(ctx, proxyLog.Error().Err(err), "failed to resolve query")
|
||||
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.
|
||||
upstreamConfig.ReBootstrap()
|
||||
answer, err = resolve1(n, upstreamConfig, msg)
|
||||
if err == nil {
|
||||
return answer
|
||||
}
|
||||
ctrld.Log(ctx, mainLog.Error().Err(err), "failed to resolve query")
|
||||
return nil
|
||||
}
|
||||
return answer
|
||||
@@ -191,7 +225,7 @@ func (p *prog) proxy(ctx context.Context, upstreams []string, failoverRcodes []i
|
||||
answer := resolve(n, upstreamConfig, msg)
|
||||
if answer == nil {
|
||||
if serveStaleCache && staleAnswer != nil {
|
||||
ctrld.Log(ctx, proxyLog.Debug(), "serving stale cached response")
|
||||
ctrld.Log(ctx, mainLog.Debug(), "serving stale cached response")
|
||||
now := time.Now()
|
||||
setCachedAnswerTTL(staleAnswer, now, now.Add(staleTTL))
|
||||
return staleAnswer
|
||||
@@ -199,7 +233,7 @@ func (p *prog) proxy(ctx context.Context, upstreams []string, failoverRcodes []i
|
||||
continue
|
||||
}
|
||||
if answer.Rcode != dns.RcodeSuccess && len(upstreamConfigs) > 1 && containRcode(failoverRcodes, answer.Rcode) {
|
||||
ctrld.Log(ctx, proxyLog.Debug(), "failover rcode matched, process to next upstream")
|
||||
ctrld.Log(ctx, mainLog.Debug(), "failover rcode matched, process to next upstream")
|
||||
continue
|
||||
}
|
||||
if p.cache != nil {
|
||||
@@ -211,11 +245,11 @@ func (p *prog) proxy(ctx context.Context, upstreams []string, failoverRcodes []i
|
||||
}
|
||||
setCachedAnswerTTL(answer, now, expired)
|
||||
p.cache.Add(dnscache.NewKey(msg, upstreams[n]), dnscache.NewValue(answer, expired))
|
||||
ctrld.Log(ctx, proxyLog.Debug(), "add cached response")
|
||||
ctrld.Log(ctx, mainLog.Debug(), "add cached response")
|
||||
}
|
||||
return answer
|
||||
}
|
||||
ctrld.Log(ctx, proxyLog.Error(), "all upstreams failed")
|
||||
ctrld.Log(ctx, mainLog.Error(), "all upstreams failed")
|
||||
answer := new(dns.Msg)
|
||||
answer.SetRcode(msg, dns.RcodeServerFailure)
|
||||
return answer
|
||||
@@ -316,6 +350,7 @@ func ttlFromMsg(msg *dns.Msg) uint32 {
|
||||
}
|
||||
|
||||
var osUpstreamConfig = &ctrld.UpstreamConfig{
|
||||
Name: "OS resolver",
|
||||
Type: ctrld.ResolverTypeOS,
|
||||
Name: "OS resolver",
|
||||
Type: ctrld.ResolverTypeOS,
|
||||
Timeout: 2000,
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
@@ -27,11 +27,8 @@ var (
|
||||
cfg ctrld.Config
|
||||
verbose int
|
||||
|
||||
bootstrapDNS = "76.76.2.0"
|
||||
|
||||
rootLogger = zerolog.New(io.Discard)
|
||||
mainLog = rootLogger
|
||||
proxyLog = rootLogger
|
||||
|
||||
cdUID string
|
||||
iface string
|
||||
@@ -59,33 +56,31 @@ func normalizeLogFilePath(logFilePath string) string {
|
||||
|
||||
func initLogging() {
|
||||
writers := []io.Writer{io.Discard}
|
||||
isLog := cfg.Service.LogLevel != ""
|
||||
if logFilePath := normalizeLogFilePath(cfg.Service.LogPath); logFilePath != "" {
|
||||
// Create parent directory if necessary.
|
||||
if err := os.MkdirAll(filepath.Dir(logFilePath), 0750); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to create log path: %v", err)
|
||||
log.Printf("failed to create log path: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
logFile, err := os.OpenFile(logFilePath, os.O_APPEND|os.O_CREATE|os.O_RDWR, os.FileMode(0o600))
|
||||
// Backup old log file with .1 suffix.
|
||||
if err := os.Rename(logFilePath, logFilePath+".1"); err != nil && !os.IsNotExist(err) {
|
||||
log.Printf("could not backup old log file: %v", err)
|
||||
}
|
||||
logFile, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_RDWR, os.FileMode(0o600))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to create log file: %v", err)
|
||||
log.Printf("failed to create log file: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
isLog = true
|
||||
writers = append(writers, logFile)
|
||||
}
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
|
||||
consoleWriter := zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) {
|
||||
w.TimeFormat = time.StampMilli
|
||||
})
|
||||
writers = append(writers, consoleWriter)
|
||||
multi := zerolog.MultiLevelWriter(writers...)
|
||||
mainLog = mainLog.Output(multi).With().Timestamp().Str("prefix", "main").Logger()
|
||||
if verbose > 0 || isLog {
|
||||
proxyLog = proxyLog.Output(multi).With().Timestamp().Logger()
|
||||
// TODO: find a better way.
|
||||
ctrld.ProxyLog = proxyLog
|
||||
}
|
||||
mainLog = mainLog.Output(multi).With().Timestamp().Logger()
|
||||
// TODO: find a better way.
|
||||
ctrld.ProxyLog = mainLog
|
||||
|
||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
logLevel := cfg.Service.LogLevel
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/logtail/backoff"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld/internal/controld"
|
||||
)
|
||||
|
||||
const (
|
||||
controldIPv6Test = "ipv6.controld.io"
|
||||
)
|
||||
|
||||
var (
|
||||
stackOnce sync.Once
|
||||
ipv6Enabled bool
|
||||
canListenIPv6Local bool
|
||||
hasNetworkUp bool
|
||||
)
|
||||
|
||||
func probeStack() {
|
||||
b := backoff.NewBackoff("probeStack", func(format string, args ...any) {}, time.Minute)
|
||||
for {
|
||||
if _, err := controld.Dialer.Dial("udp", net.JoinHostPort(bootstrapDNS, "53")); err == nil {
|
||||
hasNetworkUp = true
|
||||
break
|
||||
} else {
|
||||
b.BackOff(context.Background(), err)
|
||||
}
|
||||
}
|
||||
if _, err := controld.Dialer.Dial("tcp6", net.JoinHostPort(controldIPv6Test, "80")); err == nil {
|
||||
ipv6Enabled = true
|
||||
}
|
||||
if ln, err := net.Listen("tcp6", "[::1]:53"); err == nil {
|
||||
ln.Close()
|
||||
canListenIPv6Local = true
|
||||
}
|
||||
}
|
||||
|
||||
func netUp() bool {
|
||||
stackOnce.Do(probeStack)
|
||||
return hasNetworkUp
|
||||
}
|
||||
|
||||
func supportsIPv6() bool {
|
||||
stackOnce.Do(probeStack)
|
||||
return ipv6Enabled
|
||||
}
|
||||
|
||||
func supportsIPv6ListenLocal() bool {
|
||||
stackOnce.Do(probeStack)
|
||||
return canListenIPv6Local
|
||||
}
|
||||
|
||||
// isIPv6 checks if the provided IP is v6.
|
||||
//
|
||||
//lint:ignore U1000 use in os_windows.go
|
||||
func isIPv6(ip string) bool {
|
||||
parsedIP := net.ParseIP(ip)
|
||||
return parsedIP != nil && parsedIP.To4() == nil && parsedIP.To16() != nil
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"net"
|
||||
"os/exec"
|
||||
"strings"
|
||||
@@ -14,21 +15,30 @@ func patchNetIfaceName(iface *net.Interface) error {
|
||||
return err
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(bytes.NewReader(b))
|
||||
if name := networkServiceName(iface.Name, bytes.NewReader(b)); name != "" {
|
||||
iface.Name = name
|
||||
mainLog.Debug().Str("network_service", name).Msg("found network service name for interface")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func networkServiceName(ifaceName string, r io.Reader) string {
|
||||
scanner := bufio.NewScanner(r)
|
||||
prevLine := ""
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.Contains(line, "*") {
|
||||
// Network services is disabled.
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(line, "Device: "+iface.Name) {
|
||||
if !strings.Contains(line, "Device: "+ifaceName) {
|
||||
prevLine = line
|
||||
continue
|
||||
}
|
||||
parts := strings.Split(line, ",")
|
||||
if _, networkServiceName, ok := strings.Cut(parts[0], "(Hardware Port: "); ok {
|
||||
mainLog.Debug().Str("network_service", networkServiceName).Msg("found network service name for interface")
|
||||
iface.Name = networkServiceName
|
||||
parts := strings.SplitN(prevLine, " ", 2)
|
||||
if len(parts) == 2 {
|
||||
return strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return ""
|
||||
}
|
||||
|
||||
59
cmd/ctrld/net_darwin_test.go
Normal file
59
cmd/ctrld/net_darwin_test.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const listnetworkserviceorderOutput = `
|
||||
(1) USB 10/100/1000 LAN 2
|
||||
(Hardware Port: USB 10/100/1000 LAN, Device: en7)
|
||||
|
||||
(2) Ethernet
|
||||
(Hardware Port: Ethernet, Device: en0)
|
||||
|
||||
(3) Wi-Fi
|
||||
(Hardware Port: Wi-Fi, Device: en1)
|
||||
|
||||
(4) Bluetooth PAN
|
||||
(Hardware Port: Bluetooth PAN, Device: en4)
|
||||
|
||||
(5) Thunderbolt Bridge
|
||||
(Hardware Port: Thunderbolt Bridge, Device: bridge0)
|
||||
|
||||
(6) kernal
|
||||
(Hardware Port: com.wireguard.macos, Device: )
|
||||
|
||||
(7) WS BT
|
||||
(Hardware Port: com.wireguard.macos, Device: )
|
||||
|
||||
(8) ca-001-stg
|
||||
(Hardware Port: com.wireguard.macos, Device: )
|
||||
|
||||
(9) ca-001-stg-2
|
||||
(Hardware Port: com.wireguard.macos, Device: )
|
||||
|
||||
`
|
||||
|
||||
func Test_networkServiceName(t *testing.T) {
|
||||
tests := []struct {
|
||||
ifaceName string
|
||||
networkServiceName string
|
||||
}{
|
||||
{"en7", "USB 10/100/1000 LAN 2"},
|
||||
{"en0", "Ethernet"},
|
||||
{"en1", "Wi-Fi"},
|
||||
{"en4", "Bluetooth PAN"},
|
||||
{"bridge0", "Thunderbolt Bridge"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.ifaceName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
name := networkServiceName(tc.ifaceName, strings.NewReader(listnetworkserviceorderOutput))
|
||||
assert.Equal(t, tc.networkServiceName, name)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-systemd/v22/dbus"
|
||||
@@ -24,10 +23,6 @@ systemd-resolved=false
|
||||
var networkManagerCtrldConfFile = filepath.Join(nmConfDir, nmCtrldConfFilename)
|
||||
|
||||
func setupNetworkManager() error {
|
||||
if runtime.GOOS != "linux" {
|
||||
mainLog.Debug().Msg("skipping NetworkManager setup, not on Linux")
|
||||
return nil
|
||||
}
|
||||
if content, _ := os.ReadFile(nmCtrldConfContent); string(content) == nmCtrldConfContent {
|
||||
mainLog.Debug().Msg("NetworkManager already setup, nothing to do")
|
||||
return nil
|
||||
@@ -48,10 +43,6 @@ func setupNetworkManager() error {
|
||||
}
|
||||
|
||||
func restoreNetworkManager() error {
|
||||
if runtime.GOOS != "linux" {
|
||||
mainLog.Debug().Msg("skipping NetworkManager restoring, not on Linux")
|
||||
return nil
|
||||
}
|
||||
err := os.Remove(networkManagerCtrldConfFile)
|
||||
if os.IsNotExist(err) {
|
||||
mainLog.Debug().Msg("NetworkManager is not available")
|
||||
15
cmd/ctrld/network_manager_others.go
Normal file
15
cmd/ctrld/network_manager_others.go
Normal file
@@ -0,0 +1,15 @@
|
||||
//go:build !linux
|
||||
|
||||
package main
|
||||
|
||||
func setupNetworkManager() error {
|
||||
reloadNetworkManager()
|
||||
return nil
|
||||
}
|
||||
|
||||
func restoreNetworkManager() error {
|
||||
reloadNetworkManager()
|
||||
return nil
|
||||
}
|
||||
|
||||
func reloadNetworkManager() {}
|
||||
@@ -1,6 +1,3 @@
|
||||
//go:build darwin
|
||||
// +build darwin
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
68
cmd/ctrld/os_freebsd.go
Normal file
68
cmd/ctrld/os_freebsd.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"os/exec"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld/internal/dns"
|
||||
"github.com/Control-D-Inc/ctrld/internal/resolvconffile"
|
||||
)
|
||||
|
||||
// allocate loopback ip
|
||||
// sudo ifconfig lo0 127.0.0.53 alias
|
||||
func allocateIP(ip string) error {
|
||||
cmd := exec.Command("ifconfig", "lo0", ip, "alias")
|
||||
if err := cmd.Run(); err != nil {
|
||||
mainLog.Error().Err(err).Msg("allocateIP failed")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func deAllocateIP(ip string) error {
|
||||
cmd := exec.Command("ifconfig", "lo0", ip, "-alias")
|
||||
if err := cmd.Run(); err != nil {
|
||||
mainLog.Error().Err(err).Msg("deAllocateIP failed")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// set the dns server for the provided network interface
|
||||
func setDNS(iface *net.Interface, nameservers []string) error {
|
||||
r, err := dns.NewOSConfigurator(logf, iface.Name)
|
||||
if err != nil {
|
||||
mainLog.Error().Err(err).Msg("failed to create DNS OS configurator")
|
||||
return err
|
||||
}
|
||||
|
||||
ns := make([]netip.Addr, 0, len(nameservers))
|
||||
for _, nameserver := range nameservers {
|
||||
ns = append(ns, netip.MustParseAddr(nameserver))
|
||||
}
|
||||
|
||||
if err := r.SetDNS(dns.OSConfig{Nameservers: ns}); err != nil {
|
||||
mainLog.Error().Err(err).Msg("failed to set DNS")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resetDNS(iface *net.Interface) error {
|
||||
r, err := dns.NewOSConfigurator(logf, iface.Name)
|
||||
if err != nil {
|
||||
mainLog.Error().Err(err).Msg("failed to create DNS OS configurator")
|
||||
return err
|
||||
}
|
||||
|
||||
if err := r.Close(); err != nil {
|
||||
mainLog.Error().Err(err).Msg("failed to rollback DNS setting")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func currentDNS(_ *net.Interface) []string {
|
||||
return resolvconffile.NameServers("")
|
||||
}
|
||||
@@ -19,19 +19,16 @@ import (
|
||||
"tailscale.com/util/dnsname"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld/internal/dns"
|
||||
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
||||
"github.com/Control-D-Inc/ctrld/internal/resolvconffile"
|
||||
)
|
||||
|
||||
var logf = func(format string, args ...any) {
|
||||
mainLog.Debug().Msgf(format, args...)
|
||||
}
|
||||
|
||||
// allocate loopback ip
|
||||
// sudo ip a add 127.0.0.2/24 dev lo
|
||||
func allocateIP(ip string) error {
|
||||
cmd := exec.Command("ip", "a", "add", ip+"/24", "dev", "lo")
|
||||
if err := cmd.Run(); err != nil {
|
||||
mainLog.Error().Err(err).Msg("allocateIP failed")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
mainLog.Error().Err(err).Msgf("allocateIP failed: %s", string(out))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -79,16 +76,20 @@ func setDNS(iface *net.Interface, nameservers []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func resetDNS(iface *net.Interface) error {
|
||||
if r, err := dns.NewOSConfigurator(logf, iface.Name); err == nil {
|
||||
if err := r.Close(); err != nil {
|
||||
mainLog.Error().Err(err).Msg("failed to rollback DNS setting")
|
||||
return err
|
||||
func resetDNS(iface *net.Interface) (err error) {
|
||||
defer func() {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if r.Mode() == "direct" {
|
||||
return nil
|
||||
if r, oerr := dns.NewOSConfigurator(logf, iface.Name); oerr == nil {
|
||||
_ = r.SetDNS(dns.OSConfig{})
|
||||
if err := r.Close(); err != nil {
|
||||
mainLog.Error().Err(err).Msg("failed to rollback DNS setting")
|
||||
return
|
||||
}
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
var ns []string
|
||||
c, err := nclient4.New(iface.Name)
|
||||
@@ -111,7 +112,7 @@ func resetDNS(iface *net.Interface) error {
|
||||
}
|
||||
|
||||
// TODO(cuonglm): handle DHCPv6 properly.
|
||||
if supportsIPv6() {
|
||||
if ctrldnet.SupportsIPv6() {
|
||||
c := client6.NewClient()
|
||||
conversation, err := c.Exchange(iface.Name)
|
||||
if err != nil {
|
||||
|
||||
13
cmd/ctrld/os_others.go
Normal file
13
cmd/ctrld/os_others.go
Normal file
@@ -0,0 +1,13 @@
|
||||
//go:build !linux && !darwin && !freebsd
|
||||
|
||||
package main
|
||||
|
||||
// TODO(cuonglm): implement.
|
||||
func allocateIP(ip string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO(cuonglm): implement.
|
||||
func deAllocateIP(ip string) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,6 +1,3 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -10,18 +7,10 @@ import (
|
||||
"strconv"
|
||||
|
||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||
|
||||
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
||||
)
|
||||
|
||||
// TODO(cuonglm): implement.
|
||||
func allocateIP(ip string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO(cuonglm): implement.
|
||||
func deAllocateIP(ip string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func setDNS(iface *net.Interface, nameservers []string) error {
|
||||
if len(nameservers) == 0 {
|
||||
return errors.New("empty DNS nameservers")
|
||||
@@ -39,7 +28,7 @@ func setDNS(iface *net.Interface, nameservers []string) error {
|
||||
|
||||
// TODO(cuonglm): should we use system API?
|
||||
func resetDNS(iface *net.Interface) error {
|
||||
if supportsIPv6ListenLocal() {
|
||||
if ctrldnet.SupportsIPv6ListenLocal() {
|
||||
if output, err := netsh("interface", "ipv6", "set", "dnsserver", strconv.Itoa(iface.Index), "dhcp"); err != nil {
|
||||
mainLog.Warn().Err(err).Msgf("failed to reset ipv6 DNS: %s", string(output))
|
||||
}
|
||||
@@ -54,7 +43,7 @@ func resetDNS(iface *net.Interface) error {
|
||||
|
||||
func setPrimaryDNS(iface *net.Interface, dns string) error {
|
||||
ipVer := "ipv4"
|
||||
if isIPv6(dns) {
|
||||
if ctrldnet.IsIPv6(dns) {
|
||||
ipVer = "ipv6"
|
||||
}
|
||||
idx := strconv.Itoa(iface.Index)
|
||||
@@ -63,7 +52,7 @@ func setPrimaryDNS(iface *net.Interface, dns string) error {
|
||||
mainLog.Error().Err(err).Msgf("failed to set primary DNS: %s", string(output))
|
||||
return err
|
||||
}
|
||||
if ipVer == "ipv4" {
|
||||
if ipVer == "ipv4" && ctrldnet.SupportsIPv6ListenLocal() {
|
||||
// Disable IPv6 DNS, so the query will be fallback to IPv4.
|
||||
_, _ = netsh("interface", "ipv6", "set", "dnsserver", idx, "static", "::1", "primary")
|
||||
}
|
||||
@@ -73,7 +62,7 @@ func setPrimaryDNS(iface *net.Interface, dns string) error {
|
||||
|
||||
func addSecondaryDNS(iface *net.Interface, dns string) error {
|
||||
ipVer := "ipv4"
|
||||
if isIPv6(dns) {
|
||||
if ctrldnet.IsIPv6(dns) {
|
||||
ipVer = "ipv6"
|
||||
}
|
||||
output, err := netsh("interface", ipVer, "add", "dns", strconv.Itoa(iface.Index), dns, "index=2")
|
||||
|
||||
@@ -2,6 +2,8 @@ package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
@@ -9,12 +11,15 @@ import (
|
||||
"syscall"
|
||||
|
||||
"github.com/kardianos/service"
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld"
|
||||
"github.com/Control-D-Inc/ctrld/internal/dnscache"
|
||||
)
|
||||
|
||||
var logf = func(format string, args ...any) {
|
||||
mainLog.Debug().Msgf(format, args...)
|
||||
}
|
||||
|
||||
var errWindowsAddrInUse = syscall.Errno(0x2740)
|
||||
|
||||
var svcConfig = &service.Config{
|
||||
@@ -23,6 +28,10 @@ var svcConfig = &service.Config{
|
||||
}
|
||||
|
||||
type prog struct {
|
||||
mu sync.Mutex
|
||||
waitCh chan struct{}
|
||||
stopCh chan struct{}
|
||||
|
||||
cfg *ctrld.Config
|
||||
cache dnscache.Cacher
|
||||
}
|
||||
@@ -30,11 +39,12 @@ type prog struct {
|
||||
func (p *prog) Start(s service.Service) error {
|
||||
p.cfg = &cfg
|
||||
go p.run()
|
||||
mainLog.Info().Msg("Service started")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *prog) run() {
|
||||
// Wait the caller to signal that we can do our logic.
|
||||
<-p.waitCh
|
||||
p.preRun()
|
||||
if p.cfg.Service.CacheEnable {
|
||||
cacher, err := dnscache.NewLRUCache(p.cfg.Service.CacheSize)
|
||||
@@ -51,7 +61,7 @@ func (p *prog) run() {
|
||||
for _, cidr := range nc.Cidrs {
|
||||
_, ipNet, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
proxyLog.Error().Err(err).Str("network", nc.Name).Str("cidr", cidr).Msg("invalid cidr")
|
||||
mainLog.Error().Err(err).Str("network", nc.Name).Str("cidr", cidr).Msg("invalid cidr")
|
||||
continue
|
||||
}
|
||||
nc.IPNets = append(nc.IPNets, ipNet)
|
||||
@@ -61,43 +71,10 @@ func (p *prog) run() {
|
||||
uc := p.cfg.Upstream[n]
|
||||
uc.Init()
|
||||
if uc.BootstrapIP == "" {
|
||||
// resolve it manually and set the bootstrap ip
|
||||
c := new(dns.Client)
|
||||
for _, dnsType := range []uint16{dns.TypeAAAA, dns.TypeA} {
|
||||
if !supportsIPv6() && dnsType == dns.TypeAAAA {
|
||||
continue
|
||||
}
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion(uc.Domain+".", dnsType)
|
||||
m.RecursionDesired = true
|
||||
r, _, err := c.Exchange(m, net.JoinHostPort(bootstrapDNS, "53"))
|
||||
if err != nil {
|
||||
proxyLog.Error().Err(err).Msgf("could not resolve domain %s for upstream.%s", uc.Domain, n)
|
||||
continue
|
||||
}
|
||||
if r.Rcode != dns.RcodeSuccess {
|
||||
proxyLog.Error().Msgf("could not resolve domain return code: %d, upstream.%s", r.Rcode, n)
|
||||
continue
|
||||
}
|
||||
if len(r.Answer) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, a := range r.Answer {
|
||||
switch ar := a.(type) {
|
||||
case *dns.A:
|
||||
uc.BootstrapIP = ar.A.String()
|
||||
case *dns.AAAA:
|
||||
uc.BootstrapIP = ar.AAAA.String()
|
||||
default:
|
||||
continue
|
||||
}
|
||||
mainLog.Info().Str("bootstrap_ip", uc.BootstrapIP).Msgf("Setting bootstrap IP for upstream.%s", n)
|
||||
// Stop if we reached here, because we got the bootstrap IP from r.Answer.
|
||||
break
|
||||
}
|
||||
// If we reached here, uc.BootstrapIP was set, nothing to do anymore.
|
||||
break
|
||||
}
|
||||
uc.SetupBootstrapIP()
|
||||
mainLog.Info().Str("bootstrap_ip", uc.BootstrapIP).Msgf("Setting bootstrap IP for upstream.%s", n)
|
||||
} else {
|
||||
mainLog.Info().Str("bootstrap_ip", uc.BootstrapIP).Msgf("Using bootstrap IP for upstream.%s", n)
|
||||
}
|
||||
uc.SetupTransport()
|
||||
}
|
||||
@@ -109,60 +86,44 @@ func (p *prog) run() {
|
||||
listenerConfig := p.cfg.Listener[listenerNum]
|
||||
upstreamConfig := p.cfg.Upstream[listenerNum]
|
||||
if upstreamConfig == nil {
|
||||
proxyLog.Error().Msgf("missing upstream config for: [listener.%s]", listenerNum)
|
||||
mainLog.Error().Msgf("missing upstream config for: [listener.%s]", listenerNum)
|
||||
return
|
||||
}
|
||||
addr := net.JoinHostPort(listenerConfig.IP, strconv.Itoa(listenerConfig.Port))
|
||||
mainLog.Info().Msgf("Starting DNS server on listener.%s: %s", listenerNum, addr)
|
||||
err := p.serveUDP(listenerNum)
|
||||
if err != nil && !defaultConfigWritten {
|
||||
proxyLog.Fatal().Err(err).Msgf("Unable to start dns proxy on listener.%s", listenerNum)
|
||||
err := p.serveDNS(listenerNum)
|
||||
if err != nil && !defaultConfigWritten && cdUID == "" {
|
||||
mainLog.Fatal().Err(err).Msgf("Unable to start dns proxy on listener.%s", listenerNum)
|
||||
return
|
||||
}
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if opErr, ok := err.(*net.OpError); ok {
|
||||
if opErr, ok := err.(*net.OpError); ok && listenerNum == "0" {
|
||||
if sErr, ok := opErr.Err.(*os.SyscallError); ok && errors.Is(opErr.Err, syscall.EADDRINUSE) || errors.Is(sErr.Err, errWindowsAddrInUse) {
|
||||
proxyLog.Warn().Msgf("Address %s already in used, pick a random one", addr)
|
||||
pc, err := net.ListenPacket("udp", net.JoinHostPort(listenerConfig.IP, "0"))
|
||||
if err != nil {
|
||||
proxyLog.Fatal().Err(err).Msg("failed to listen packet")
|
||||
return
|
||||
}
|
||||
_, portStr, _ := net.SplitHostPort(pc.LocalAddr().String())
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
proxyLog.Fatal().Err(err).Msg("malformed port")
|
||||
return
|
||||
}
|
||||
listenerConfig.Port = port
|
||||
v.Set("listener", map[string]*ctrld.ListenerConfig{
|
||||
"0": {
|
||||
IP: "127.0.0.1",
|
||||
Port: port,
|
||||
},
|
||||
})
|
||||
mainLog.Warn().Msgf("Address %s already in used, pick a random one", addr)
|
||||
ip := randomLocalIP()
|
||||
listenerConfig.IP = ip
|
||||
port := listenerConfig.Port
|
||||
cfg.Upstream = map[string]*ctrld.UpstreamConfig{"0": cfg.Upstream["0"]}
|
||||
if err := writeConfigFile(); err != nil {
|
||||
proxyLog.Fatal().Err(err).Msg("failed to write config file")
|
||||
mainLog.Fatal().Err(err).Msg("failed to write config file")
|
||||
} else {
|
||||
mainLog.Info().Msg("writing config file to: " + defaultConfigFile)
|
||||
}
|
||||
mainLog.Info().Msgf("Starting DNS server on listener.%s: %s", listenerNum, pc.LocalAddr())
|
||||
// There can be a race between closing the listener and start our own UDP server, but it's
|
||||
// rare, and we only do this once, so let conservative here.
|
||||
if err := pc.Close(); err != nil {
|
||||
proxyLog.Fatal().Err(err).Msg("failed to close packet conn")
|
||||
return
|
||||
}
|
||||
if err := p.serveUDP(listenerNum); err != nil {
|
||||
proxyLog.Fatal().Err(err).Msgf("Unable to start dns proxy on listener.%s", listenerNum)
|
||||
p.mu.Lock()
|
||||
p.cfg.Service.AllocateIP = true
|
||||
p.mu.Unlock()
|
||||
p.preRun()
|
||||
mainLog.Info().Msgf("Starting DNS server on listener.%s: %s", listenerNum, net.JoinHostPort(ip, strconv.Itoa(port)))
|
||||
if err := p.serveDNS(listenerNum); err != nil {
|
||||
mainLog.Fatal().Err(err).Msgf("Unable to start dns proxy on listener.%s", listenerNum)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
proxyLog.Fatal().Err(err).Msgf("Unable to start dns proxy on listener.%s", listenerNum)
|
||||
mainLog.Fatal().Err(err).Msgf("Unable to start dns proxy on listener.%s", listenerNum)
|
||||
}(listenerNum)
|
||||
}
|
||||
|
||||
@@ -175,10 +136,13 @@ func (p *prog) Stop(s service.Service) error {
|
||||
return err
|
||||
}
|
||||
mainLog.Info().Msg("Service stopped")
|
||||
close(p.stopCh)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *prog) allocateIP(ip string) error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if !p.cfg.Service.AllocateIP {
|
||||
return nil
|
||||
}
|
||||
@@ -186,6 +150,8 @@ func (p *prog) allocateIP(ip string) error {
|
||||
}
|
||||
|
||||
func (p *prog) deAllocateIP() error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if !p.cfg.Service.AllocateIP {
|
||||
return nil
|
||||
}
|
||||
@@ -249,3 +215,8 @@ func (p *prog) resetDNS() {
|
||||
}
|
||||
logger.Debug().Msg("Restoring DNS successfully")
|
||||
}
|
||||
|
||||
func randomLocalIP() string {
|
||||
n := rand.Intn(254-2) + 2
|
||||
return fmt.Sprintf("127.0.0.%d", n)
|
||||
}
|
||||
|
||||
20
cmd/ctrld/prog_freebsd.go
Normal file
20
cmd/ctrld/prog_freebsd.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/kardianos/service"
|
||||
)
|
||||
|
||||
func (p *prog) preRun() {
|
||||
if !service.Interactive() {
|
||||
p.setDNS()
|
||||
}
|
||||
}
|
||||
|
||||
func setDependencies(svc *service.Config) {
|
||||
// TODO(cuonglm): remove once https://github.com/kardianos/service/issues/359 fixed.
|
||||
_ = os.MkdirAll("/usr/local/etc/rc.d", 0755)
|
||||
}
|
||||
|
||||
func setWorkingDirectory(svc *service.Config, dir string) {}
|
||||
@@ -18,3 +18,7 @@ func setDependencies(svc *service.Config) {
|
||||
"After=NetworkManager-wait-online.service",
|
||||
}
|
||||
}
|
||||
|
||||
func setWorkingDirectory(svc *service.Config, dir string) {
|
||||
svc.WorkingDirectory = dir
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build !linux
|
||||
// +build !linux
|
||||
//go:build !linux && !freebsd
|
||||
|
||||
package main
|
||||
|
||||
@@ -8,3 +7,8 @@ import "github.com/kardianos/service"
|
||||
func (p *prog) preRun() {}
|
||||
|
||||
func setDependencies(svc *service.Config) {}
|
||||
|
||||
func setWorkingDirectory(svc *service.Config, dir string) {
|
||||
// WorkingDirectory is not supported on Windows.
|
||||
svc.WorkingDirectory = dir
|
||||
}
|
||||
|
||||
248
config.go
248
config.go
@@ -2,20 +2,21 @@ package ctrld
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld/internal/dnsrcode"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/lucas-clemente/quic-go"
|
||||
"github.com/lucas-clemente/quic-go/http3"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/spf13/viper"
|
||||
"golang.org/x/sync/singleflight"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld/internal/dnsrcode"
|
||||
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
||||
)
|
||||
|
||||
// SetConfigName set the config name that ctrld will look for.
|
||||
@@ -69,9 +70,9 @@ func InitConfig(v *viper.Viper, name string) {
|
||||
// Config represents ctrld supported configuration.
|
||||
type Config struct {
|
||||
Service ServiceConfig `mapstructure:"service" toml:"service,omitempty"`
|
||||
Listener map[string]*ListenerConfig `mapstructure:"listener" toml:"listener" validate:"min=1,dive"`
|
||||
Network map[string]*NetworkConfig `mapstructure:"network" toml:"network" validate:"min=1,dive"`
|
||||
Upstream map[string]*UpstreamConfig `mapstructure:"upstream" toml:"upstream" validate:"min=1,dive"`
|
||||
Listener map[string]*ListenerConfig `mapstructure:"listener" toml:"listener" validate:"min=1,dive"`
|
||||
}
|
||||
|
||||
// ServiceConfig specifies the general ctrld config.
|
||||
@@ -95,14 +96,18 @@ type NetworkConfig struct {
|
||||
|
||||
// UpstreamConfig specifies configuration for upstreams that ctrld will forward requests to.
|
||||
type UpstreamConfig struct {
|
||||
Name string `mapstructure:"name" toml:"name,omitempty"`
|
||||
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"`
|
||||
BootstrapIP string `mapstructure:"bootstrap_ip" toml:"bootstrap_ip,omitempty"`
|
||||
Domain string `mapstructure:"-" toml:"-"`
|
||||
Timeout int `mapstructure:"timeout" toml:"timeout,omitempty" validate:"gte=0"`
|
||||
transport *http.Transport `mapstructure:"-" toml:"-"`
|
||||
http3RoundTripper *http3.RoundTripper `mapstructure:"-" toml:"-"`
|
||||
Name string `mapstructure:"name" toml:"name,omitempty"`
|
||||
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"`
|
||||
BootstrapIP string `mapstructure:"bootstrap_ip" toml:"bootstrap_ip,omitempty"`
|
||||
Domain string `mapstructure:"-" toml:"-"`
|
||||
Timeout int `mapstructure:"timeout" toml:"timeout,omitempty" validate:"gte=0"`
|
||||
transport *http.Transport `mapstructure:"-" toml:"-"`
|
||||
http3RoundTripper http.RoundTripper `mapstructure:"-" toml:"-"`
|
||||
|
||||
g singleflight.Group
|
||||
bootstrapIPs []string
|
||||
nextBootstrapIP atomic.Uint32
|
||||
}
|
||||
|
||||
// ListenerConfig specifies the networks configuration that ctrld will run on.
|
||||
@@ -147,6 +152,132 @@ func (uc *UpstreamConfig) Init() {
|
||||
}
|
||||
}
|
||||
|
||||
// SetupBootstrapIP manually find all available IPs of the upstream.
|
||||
// The first usable IP will be used as bootstrap IP of the upstream.
|
||||
func (uc *UpstreamConfig) SetupBootstrapIP() {
|
||||
uc.setupBootstrapIP(true)
|
||||
}
|
||||
|
||||
// SetupBootstrapIP manually find all available IPs of the upstream.
|
||||
// The first usable IP will be used as bootstrap IP of the upstream.
|
||||
func (uc *UpstreamConfig) setupBootstrapIP(withBootstrapDNS bool) {
|
||||
bootstrapIP := func(record dns.RR) string {
|
||||
switch ar := record.(type) {
|
||||
case *dns.A:
|
||||
return ar.A.String()
|
||||
case *dns.AAAA:
|
||||
return ar.AAAA.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
resolver := &osResolver{nameservers: availableNameservers()}
|
||||
if withBootstrapDNS {
|
||||
resolver.nameservers = append([]string{net.JoinHostPort(bootstrapDNS, "53")}, resolver.nameservers...)
|
||||
}
|
||||
ProxyLog.Debug().Msgf("Resolving %q using bootstrap DNS %q", uc.Domain, resolver.nameservers)
|
||||
timeoutMs := 2000
|
||||
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 %q, return code: %s", uc.Domain, dns.RcodeToString[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)
|
||||
}
|
||||
|
||||
// ReBootstrap re-setup the bootstrap IP and the transport.
|
||||
func (uc *UpstreamConfig) ReBootstrap() {
|
||||
switch uc.Type {
|
||||
case ResolverTypeDOH, ResolverTypeDOH3:
|
||||
default:
|
||||
return
|
||||
}
|
||||
_, _, _ = uc.g.Do("ReBootstrap", func() (any, error) {
|
||||
ProxyLog.Debug().Msg("re-bootstrapping upstream ip")
|
||||
n := uint32(len(uc.bootstrapIPs))
|
||||
if n == 0 {
|
||||
uc.SetupBootstrapIP()
|
||||
uc.setupTransportWithoutPingUpstream()
|
||||
}
|
||||
|
||||
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()
|
||||
return true, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (uc *UpstreamConfig) setupTransportWithoutPingUpstream() {
|
||||
switch uc.Type {
|
||||
case ResolverTypeDOH:
|
||||
uc.setupDOHTransportWithoutPingUpstream()
|
||||
case ResolverTypeDOH3:
|
||||
uc.setupDOH3TransportWithoutPingUpstream()
|
||||
}
|
||||
}
|
||||
|
||||
// SetupTransport initializes the network transport used to connect to upstream server.
|
||||
// For now, only DoH upstream is supported.
|
||||
func (uc *UpstreamConfig) SetupTransport() {
|
||||
@@ -159,51 +290,33 @@ func (uc *UpstreamConfig) SetupTransport() {
|
||||
}
|
||||
|
||||
func (uc *UpstreamConfig) setupDOHTransport() {
|
||||
uc.transport = http.DefaultTransport.(*http.Transport).Clone()
|
||||
uc.transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
dialer := &net.Dialer{
|
||||
Timeout: 10 * time.Second,
|
||||
KeepAlive: 10 * time.Second,
|
||||
}
|
||||
Log(ctx, ProxyLog.Debug(), "debug dial context %s - %s - %s", addr, network, bootstrapDNS)
|
||||
// if we have a bootstrap ip set, use it to avoid DNS lookup
|
||||
if uc.BootstrapIP != "" {
|
||||
if _, port, _ := net.SplitHostPort(addr); port != "" {
|
||||
addr = net.JoinHostPort(uc.BootstrapIP, port)
|
||||
}
|
||||
Log(ctx, ProxyLog.Debug(), "sending doh request to: %s", addr)
|
||||
}
|
||||
return dialer.DialContext(ctx, network, addr)
|
||||
}
|
||||
|
||||
uc.setupDOHTransportWithoutPingUpstream()
|
||||
uc.pingUpstream()
|
||||
}
|
||||
|
||||
func (uc *UpstreamConfig) setupDOH3Transport() {
|
||||
uc.http3RoundTripper = &http3.RoundTripper{}
|
||||
uc.http3RoundTripper.Dial = func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
||||
host := addr
|
||||
ProxyLog.Debug().Msgf("debug dial context D0H3 %s - %s", addr, bootstrapDNS)
|
||||
func (uc *UpstreamConfig) setupDOHTransportWithoutPingUpstream() {
|
||||
uc.transport = http.DefaultTransport.(*http.Transport).Clone()
|
||||
uc.transport.IdleConnTimeout = 5 * time.Second
|
||||
|
||||
dialerTimeoutMs := 2000
|
||||
if uc.Timeout > 0 && uc.Timeout < dialerTimeoutMs {
|
||||
dialerTimeoutMs = uc.Timeout
|
||||
}
|
||||
dialerTimeout := time.Duration(dialerTimeoutMs) * time.Millisecond
|
||||
uc.transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
dialer := &net.Dialer{
|
||||
Timeout: dialerTimeout,
|
||||
KeepAlive: dialerTimeout,
|
||||
}
|
||||
// if we have a bootstrap ip set, use it to avoid DNS lookup
|
||||
if uc.BootstrapIP != "" {
|
||||
if _, port, _ := net.SplitHostPort(addr); port != "" {
|
||||
addr = net.JoinHostPort(uc.BootstrapIP, port)
|
||||
}
|
||||
ProxyLog.Debug().Msgf("sending doh3 request to: %s", addr)
|
||||
}
|
||||
remoteAddr, err := net.ResolveUDPAddr("udp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
udpConn, err := net.ListenUDP("udp", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return quic.DialEarlyContext(ctx, udpConn, remoteAddr, host, tlsCfg, cfg)
|
||||
Log(ctx, ProxyLog.Debug(), "sending doh request to: %s", addr)
|
||||
return dialer.DialContext(ctx, network, addr)
|
||||
}
|
||||
|
||||
uc.pingUpstream()
|
||||
}
|
||||
|
||||
func (uc *UpstreamConfig) pingUpstream() {
|
||||
@@ -252,3 +365,42 @@ func defaultPortFor(typ string) string {
|
||||
}
|
||||
return "53"
|
||||
}
|
||||
|
||||
func availableNameservers() []string {
|
||||
nss := nameservers()
|
||||
n := 0
|
||||
for _, ns := range nss {
|
||||
ip, _, _ := net.SplitHostPort(ns)
|
||||
// skipping invalid entry or ipv6 nameserver if ipv6 not available.
|
||||
if ip == "" || (ctrldnet.IsIPv6(ip) && !ctrldnet.SupportsIPv6()) {
|
||||
continue
|
||||
}
|
||||
nss[n] = ns
|
||||
n++
|
||||
}
|
||||
return nss[:n]
|
||||
}
|
||||
|
||||
// ResolverTypeFromEndpoint tries guessing the resolver type with a given endpoint
|
||||
// using following rules:
|
||||
//
|
||||
// - If endpoint is an IP address -> ResolverTypeLegacy
|
||||
// - If endpoint starts with "https://" -> ResolverTypeDOH
|
||||
// - If endpoint starts with "quic://" -> ResolverTypeDOQ
|
||||
// - For anything else -> ResolverTypeDOT
|
||||
func ResolverTypeFromEndpoint(endpoint string) string {
|
||||
switch {
|
||||
case strings.HasPrefix(endpoint, "https://"):
|
||||
return ResolverTypeDOH
|
||||
case strings.HasPrefix(endpoint, "quic://"):
|
||||
return ResolverTypeDOQ
|
||||
}
|
||||
host := endpoint
|
||||
if strings.Contains(endpoint, ":") {
|
||||
host, _, _ = net.SplitHostPort(host)
|
||||
}
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
return ResolverTypeLegacy
|
||||
}
|
||||
return ResolverTypeDOT
|
||||
}
|
||||
|
||||
21
config_internal_test.go
Normal file
21
config_internal_test.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package ctrld
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
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 uc.BootstrapIP == "" {
|
||||
t.Log(availableNameservers())
|
||||
t.Fatal("could not bootstrap ip without bootstrap DNS")
|
||||
}
|
||||
t.Log(uc)
|
||||
}
|
||||
44
config_quic.go
Normal file
44
config_quic.go
Normal file
@@ -0,0 +1,44 @@
|
||||
//go:build !qf
|
||||
|
||||
package ctrld
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/quic-go/quic-go/http3"
|
||||
)
|
||||
|
||||
func (uc *UpstreamConfig) setupDOH3Transport() {
|
||||
uc.setupDOH3TransportWithoutPingUpstream()
|
||||
uc.pingUpstream()
|
||||
}
|
||||
|
||||
func (uc *UpstreamConfig) setupDOH3TransportWithoutPingUpstream() {
|
||||
rt := &http3.RoundTripper{}
|
||||
rt.Dial = func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
||||
host := addr
|
||||
ProxyLog.Debug().Msgf("debug dial context D0H3 %s - %s", addr, bootstrapDNS)
|
||||
// if we have a bootstrap ip set, use it to avoid DNS lookup
|
||||
if uc.BootstrapIP != "" {
|
||||
if _, port, _ := net.SplitHostPort(addr); port != "" {
|
||||
addr = net.JoinHostPort(uc.BootstrapIP, port)
|
||||
}
|
||||
ProxyLog.Debug().Msgf("sending doh3 request to: %s", addr)
|
||||
}
|
||||
remoteAddr, err := net.ResolveUDPAddr("udp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
udpConn, err := net.ListenUDP("udp", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return quic.DialEarlyContext(ctx, udpConn, remoteAddr, host, tlsCfg, cfg)
|
||||
}
|
||||
|
||||
uc.http3RoundTripper = rt
|
||||
}
|
||||
7
config_quic_free.go
Normal file
7
config_quic_free.go
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build qf
|
||||
|
||||
package ctrld
|
||||
|
||||
func (uc *UpstreamConfig) setupDOH3Transport() {}
|
||||
|
||||
func (uc *UpstreamConfig) setupDOH3TransportWithoutPingUpstream() {}
|
||||
11
doh.go
11
doh.go
@@ -3,11 +3,11 @@ package ctrld
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/lucas-clemente/quic-go/http3"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
@@ -25,7 +25,7 @@ type dohResolver struct {
|
||||
endpoint string
|
||||
isDoH3 bool
|
||||
transport *http.Transport
|
||||
http3RoundTripper *http3.RoundTripper
|
||||
http3RoundTripper http.RoundTripper
|
||||
}
|
||||
|
||||
func (r *dohResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) {
|
||||
@@ -44,12 +44,17 @@ func (r *dohResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, erro
|
||||
|
||||
c := http.Client{Transport: r.transport}
|
||||
if r.isDoH3 {
|
||||
if r.http3RoundTripper == nil {
|
||||
return nil, errors.New("DoH3 is not supported")
|
||||
}
|
||||
c.Transport = r.http3RoundTripper
|
||||
}
|
||||
resp, err := c.Do(req)
|
||||
if err != nil {
|
||||
if r.isDoH3 {
|
||||
r.http3RoundTripper.Close()
|
||||
if closer, ok := r.http3RoundTripper.(io.Closer); ok {
|
||||
closer.Close()
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("could not perform request: %w", err)
|
||||
}
|
||||
|
||||
4
doq.go
4
doq.go
@@ -1,3 +1,5 @@
|
||||
//go:build !qf
|
||||
|
||||
package ctrld
|
||||
|
||||
import (
|
||||
@@ -7,8 +9,8 @@ import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/lucas-clemente/quic-go"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/quic-go/quic-go"
|
||||
)
|
||||
|
||||
type doqResolver struct {
|
||||
|
||||
18
doq_quic_free.go
Normal file
18
doq_quic_free.go
Normal file
@@ -0,0 +1,18 @@
|
||||
//go:build qf
|
||||
|
||||
package ctrld
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type doqResolver struct {
|
||||
uc *UpstreamConfig
|
||||
}
|
||||
|
||||
func (r *doqResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) {
|
||||
return nil, errors.New("DoQ is not supported")
|
||||
}
|
||||
43
errors.go
Normal file
43
errors.go
Normal file
@@ -0,0 +1,43 @@
|
||||
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
|
||||
}
|
||||
47
go.mod
47
go.mod
@@ -1,76 +1,77 @@
|
||||
module github.com/Control-D-Inc/ctrld
|
||||
|
||||
go 1.19
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534
|
||||
github.com/cuonglm/osinfo v0.0.0-20230329055532-c513f836da19
|
||||
github.com/frankban/quicktest v1.14.3
|
||||
github.com/fsnotify/fsnotify v1.6.0
|
||||
github.com/go-playground/validator/v10 v10.11.1
|
||||
github.com/godbus/dbus/v5 v5.0.6
|
||||
github.com/hashicorp/golang-lru/v2 v2.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/lucas-clemente/quic-go v0.29.1
|
||||
github.com/miekg/dns v1.1.50
|
||||
github.com/pelletier/go-toml/v2 v2.0.6
|
||||
github.com/quic-go/quic-go v0.32.0
|
||||
github.com/rs/zerolog v1.28.0
|
||||
github.com/spf13/cobra v1.4.0
|
||||
github.com/spf13/viper v1.14.0
|
||||
github.com/stretchr/testify v1.8.1
|
||||
golang.org/x/sys v0.4.0
|
||||
golang.org/x/net v0.7.0
|
||||
golang.org/x/sync v0.1.0
|
||||
golang.org/x/sys v0.5.0
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3
|
||||
tailscale.com v1.34.1
|
||||
tailscale.com v1.38.3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/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/kr/pretty v0.3.0 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/marten-seemann/qpack v0.2.1 // indirect
|
||||
github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect
|
||||
github.com/marten-seemann/qtls-go1-19 v0.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 // indirect
|
||||
github.com/mdlayher/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/socket v0.2.3 // indirect
|
||||
github.com/mdlayher/socket v0.4.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/nxadm/tail v1.4.8 // indirect
|
||||
github.com/onsi/ginkgo v1.16.5 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.2.0 // indirect
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/quic-go/qpack v0.4.0 // indirect
|
||||
github.com/quic-go/qtls-go1-18 v0.2.0 // indirect
|
||||
github.com/quic-go/qtls-go1-19 v0.2.0 // indirect
|
||||
github.com/quic-go/qtls-go1-20 v0.1.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 // indirect
|
||||
github.com/spf13/afero v1.9.3 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // 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
|
||||
go4.org/mem v0.0.0-20210711025021-927187094b94 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 // indirect
|
||||
golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
|
||||
golang.org/x/net v0.5.1-0.20230105164244-f8411da775a6 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/text v0.6.0 // indirect
|
||||
golang.org/x/tools v0.1.12 // indirect
|
||||
golang.org/x/crypto v0.6.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
|
||||
golang.org/x/mod v0.7.0 // indirect
|
||||
golang.org/x/text v0.7.0 // indirect
|
||||
golang.org/x/tools v0.4.1-0.20221208213631-3f74d914ae6d // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
106
go.sum
106
go.sum
@@ -54,6 +54,8 @@ github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534 h1:rtAn27
|
||||
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
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/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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -67,8 +69,6 @@ github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:Pjfxu
|
||||
github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og=
|
||||
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
|
||||
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
@@ -143,6 +143,8 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf
|
||||
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
@@ -155,7 +157,6 @@ github.com/hashicorp/golang-lru/v2 v2.0.1 h1:5pv5N1lT1fjLg2VQ5KWc7kmucp2x/kvFOnx
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.1/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8=
|
||||
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
@@ -164,10 +165,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/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
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-20211209223715-7d93572ebe8e/go.mod h1:h+MxyHxRg9NH3terB1nfRIUaQEcI0XOVkdR9LNBlp8E=
|
||||
github.com/josharian/native v1.0.0 h1:Ts/E8zCSEsG17dUqv7joXJFybuMLjQfWE04tsBODTxk=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8 h1:Z72DOke2yOK0Ms4Z2LK1E1OrRJXOxSj5DllTz2FYTRg=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8/go.mod h1:m5WMe03WCvWcXjRnhvaAbAAXdCnu20J5P+mmH44ZzpE=
|
||||
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-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
|
||||
@@ -191,16 +194,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/lucas-clemente/quic-go v0.29.1 h1:Z+WMJ++qMLhvpFkRZA+jl3BTxUjm415YBmWanXB8zP0=
|
||||
github.com/lucas-clemente/quic-go v0.29.1/go.mod h1:CTcNfLYJS2UuRNB+zcNlgvkjBhxX6Hm3WUxxAQx2mgE=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/marten-seemann/qpack v0.2.1 h1:jvTsT/HpCn2UZJdP+UUB53FfUUgeOyG5K1ns0OJOGVs=
|
||||
github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc=
|
||||
github.com/marten-seemann/qtls-go1-18 v0.1.2 h1:JH6jmzbduz0ITVQ7ShevK10Av5+jBEKAHMntXmIV7kM=
|
||||
github.com/marten-seemann/qtls-go1-18 v0.1.2/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4=
|
||||
github.com/marten-seemann/qtls-go1-19 v0.1.0 h1:rLFKD/9mp/uq1SYGYuVZhm83wkmU95pK5df3GufyYYU=
|
||||
github.com/marten-seemann/qtls-go1-19 v0.1.0/go.mod h1:5HTDWtVudo/WFsHKRNuOhWlbdjrfs5JHrYb0wIJqGpI=
|
||||
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
@@ -211,28 +206,21 @@ 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.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY=
|
||||
github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o=
|
||||
github.com/mdlayher/netlink v1.6.0 h1:rOHX5yl7qnlpiVkFWoqccueppMtXzeziFjWAjLg6sz0=
|
||||
github.com/mdlayher/netlink v1.6.0/go.mod h1:0o3PlBmGst1xve7wQ7j/hwpNaFaH4qCRyWCdcZk8/vA=
|
||||
github.com/mdlayher/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-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/socket v0.1.1/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5Awbj+qDs=
|
||||
github.com/mdlayher/socket v0.2.3 h1:XZA2X2TjdOwNoNPVPclRCURoX/hokBY8nkTmRZFEheM=
|
||||
github.com/mdlayher/socket v0.2.3/go.mod h1:bz12/FozYNH/VbvC3q7TRIK/Y6dH1kCKsXaUeXi/FmY=
|
||||
github.com/mdlayher/socket v0.4.0 h1:280wsy40IC9M9q1uPGcLBwXpcTQDtoGwVt+BNoITxIw=
|
||||
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/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI=
|
||||
github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk=
|
||||
github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q=
|
||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
@@ -244,6 +232,16 @@ github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qR
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||
github.com/quic-go/qtls-go1-18 v0.2.0 h1:5ViXqBZ90wpUcZS0ge79rf029yx0dYB0McyPJwqqj7U=
|
||||
github.com/quic-go/qtls-go1-18 v0.2.0/go.mod h1:moGulGHK7o6O8lSPSZNoOwcLvJKJ85vVNc7oJFD65bc=
|
||||
github.com/quic-go/qtls-go1-19 v0.2.0 h1:Cvn2WdhyViFUHoOqK52i51k4nDX8EwIh5VJiVM4nttk=
|
||||
github.com/quic-go/qtls-go1-19 v0.2.0/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI=
|
||||
github.com/quic-go/qtls-go1-20 v0.1.0 h1:d1PK3ErFy9t7zxKsG3NXBJXZjp/kMLoIb3y/kV54oAI=
|
||||
github.com/quic-go/qtls-go1-20 v0.1.0/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM=
|
||||
github.com/quic-go/quic-go v0.32.0 h1:lY02md31s1JgPiiyfqJijpu/UX/Iun304FI3yUqX7tA=
|
||||
github.com/quic-go/quic-go v0.32.0/go.mod h1:/fCsKANhQIeD5l76c2JFU+07gVE3KaA0FP+0zMWwfwo=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
@@ -282,9 +280,8 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs
|
||||
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/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-20220204230159-dac05f7d2cb4 h1:hl6sK6aFgTLISijk6xIzeqnPzQcsLqqvL6vEfTPinME=
|
||||
github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4/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-20221213070652-c3537552635f/go.mod h1:IogEAUBXDEwX7oR/BMmCctShYs80ql4hF0ySdzGxf7E=
|
||||
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.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
@@ -306,8 +303,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-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-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM=
|
||||
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
|
||||
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-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -318,8 +315,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 h1:tnebWN09GYg9OLPss1KXj8txwZc6X6uMr6VFdcGNbHw=
|
||||
golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
||||
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o=
|
||||
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@@ -344,11 +341,10 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
|
||||
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-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
@@ -372,7 +368,6 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
@@ -389,8 +384,8 @@ golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qx
|
||||
golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.5.1-0.20230105164244-f8411da775a6 h1:pKt/LWZC6+FwNujj5E7DdVyWcbtQvKqPuN0GPKWMyB8=
|
||||
golang.org/x/net v0.5.1-0.20230105164244-f8411da775a6/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -414,7 +409,6 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -427,11 +421,8 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -445,7 +436,6 @@ golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-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-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -456,14 +446,12 @@ golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-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-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-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -471,9 +459,11 @@ 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-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-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.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
|
||||
golang.org/x/sys v0.4.0/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/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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -484,8 +474,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
|
||||
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -534,14 +524,13 @@ golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82u
|
||||
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
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.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.4.1-0.20221208213631-3f74d914ae6d h1:9ZNWAi4CYhNv60mXGgAncgq7SGc5qa7C8VZV8Tg7Ggs=
|
||||
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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -642,14 +631,9 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -666,5 +650,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/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
tailscale.com v1.34.1 h1:tqm9Ww4ltyYp3IPe7vCGch6tT6j5G/WXPQ6BrVZ6pdI=
|
||||
tailscale.com v1.34.1/go.mod h1:ZsBP7rjzzB2rp+UCOumr9DAe0EQ6OPivwSXcz/BrekQ=
|
||||
tailscale.com v1.38.3 h1:2aX3+u0Re8QcN6nq7zf9Aa4ZCR2Nf6Imv3isqdQrb58=
|
||||
tailscale.com v1.38.3/go.mod h1:UWLQxcd8dz+lds2I+HpfXSruHrvXM1j4zd4zdx86t7w=
|
||||
|
||||
@@ -7,27 +7,25 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld"
|
||||
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
||||
)
|
||||
|
||||
const (
|
||||
apiDomain = "api.controld.com"
|
||||
resolverDataURL = "https://api.controld.com/utility"
|
||||
InvalidConfigCode = 40401
|
||||
)
|
||||
|
||||
const bootstrapDNS = "76.76.2.0:53"
|
||||
|
||||
var Dialer = &net.Dialer{
|
||||
Resolver: &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
d := net.Dialer{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
return d.DialContext(ctx, "udp", bootstrapDNS)
|
||||
},
|
||||
},
|
||||
}
|
||||
var (
|
||||
resolveAPIDomainOnce sync.Once
|
||||
apiDomainIP string
|
||||
)
|
||||
|
||||
// ResolverConfig represents Control D resolver data.
|
||||
type ResolverConfig struct {
|
||||
@@ -70,7 +68,51 @@ func FetchResolverConfig(uid string) (*ResolverConfig, error) {
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return Dialer.DialContext(ctx, network, addr)
|
||||
// We experiment hanging in TLS handshake when connecting to ControlD API
|
||||
// with ipv6. So prefer ipv4 if available.
|
||||
proto := "tcp6"
|
||||
if ctrldnet.SupportsIPv4() {
|
||||
proto = "tcp4"
|
||||
}
|
||||
resolveAPIDomainOnce.Do(func() {
|
||||
r, err := ctrld.NewResolver(&ctrld.UpstreamConfig{Type: ctrld.ResolverTypeOS})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
msg := new(dns.Msg)
|
||||
dnsType := dns.TypeAAAA
|
||||
if proto == "tcp4" {
|
||||
dnsType = dns.TypeA
|
||||
}
|
||||
msg.SetQuestion(apiDomain+".", dnsType)
|
||||
msg.RecursionDesired = true
|
||||
answer, err := r.Resolve(ctx, msg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if answer.Rcode != dns.RcodeSuccess || len(answer.Answer) == 0 {
|
||||
return
|
||||
}
|
||||
for _, record := range answer.Answer {
|
||||
switch ar := record.(type) {
|
||||
case *dns.A:
|
||||
apiDomainIP = ar.A.String()
|
||||
return
|
||||
case *dns.AAAA:
|
||||
apiDomainIP = ar.AAAA.String()
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
if apiDomainIP != "" {
|
||||
if _, port, _ := net.SplitHostPort(addr); port != "" {
|
||||
return ctrldnet.Dialer.DialContext(ctx, proto, net.JoinHostPort(apiDomainIP, port))
|
||||
}
|
||||
}
|
||||
return ctrldnet.Dialer.DialContext(ctx, proto, addr)
|
||||
}
|
||||
client := http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const utilityURL = "https://api.controld.com/utility"
|
||||
@@ -24,7 +25,7 @@ func TestFetchResolverConfig(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got, err := FetchResolverConfig(tc.uid)
|
||||
assert.False(t, (err != nil) != tc.wantErr)
|
||||
require.False(t, (err != nil) != tc.wantErr, err)
|
||||
if !tc.wantErr {
|
||||
assert.NotEmpty(t, got.DOH)
|
||||
}
|
||||
|
||||
153
internal/dns/debian_resolvconf.go
Normal file
153
internal/dns/debian_resolvconf.go
Normal file
@@ -0,0 +1,153 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux || freebsd || openbsd
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
//go:embed resolvconf-workaround.sh
|
||||
var workaroundScript []byte
|
||||
|
||||
// resolvconfConfigName is the name of the config submitted to
|
||||
// resolvconf.
|
||||
// The name starts with 'tun' in order to match the hardcoded
|
||||
// interface order in debian resolvconf, which will place this
|
||||
// configuration ahead of regular network links. In theory, this
|
||||
// doesn't matter because we then fix things up to ensure our config
|
||||
// is the only one in use, but in case that fails, this will make our
|
||||
// configuration slightly preferred.
|
||||
// The 'inet' suffix has no specific meaning, but conventionally
|
||||
// resolvconf implementations encourage adding a suffix roughly
|
||||
// indicating where the config came from, and "inet" is the "none of
|
||||
// the above" value (rather than, say, "ppp" or "dhcp").
|
||||
const resolvconfConfigName = "ctrld.inet"
|
||||
|
||||
// resolvconfLibcHookPath is the directory containing libc update
|
||||
// scripts, which are run by Debian resolvconf when /etc/resolv.conf
|
||||
// has been updated.
|
||||
const resolvconfLibcHookPath = "/etc/resolvconf/update-libc.d"
|
||||
|
||||
// resolvconfHookPath is the name of the libc hook script we install
|
||||
// to force Ctrld's DNS config to take effect.
|
||||
var resolvconfHookPath = filepath.Join(resolvconfLibcHookPath, "ctrld")
|
||||
|
||||
// resolvconfManager manages DNS configuration using the Debian
|
||||
// implementation of the `resolvconf` program, written by Thomas Hood.
|
||||
type resolvconfManager struct {
|
||||
logf logger.Logf
|
||||
listRecordsPath string
|
||||
interfacesDir string
|
||||
scriptInstalled bool // libc update script has been installed
|
||||
}
|
||||
|
||||
var _ OSConfigurator = (*resolvconfManager)(nil)
|
||||
|
||||
func newDebianResolvconfManager(logf logger.Logf) (*resolvconfManager, error) {
|
||||
ret := &resolvconfManager{
|
||||
logf: logf,
|
||||
listRecordsPath: "/lib/resolvconf/list-records",
|
||||
interfacesDir: "/etc/resolvconf/run/interface", // panic fallback if nothing seems to work
|
||||
}
|
||||
|
||||
if _, err := os.Stat(ret.listRecordsPath); os.IsNotExist(err) {
|
||||
// This might be a Debian system from before the big /usr
|
||||
// merge, try /usr instead.
|
||||
ret.listRecordsPath = "/usr" + ret.listRecordsPath
|
||||
}
|
||||
// The runtime directory is currently (2020-04) canonically
|
||||
// /etc/resolvconf/run, but the manpage is making noise about
|
||||
// switching to /run/resolvconf and dropping the /etc path. So,
|
||||
// let's probe the possible directories and use the first one
|
||||
// that works.
|
||||
for _, path := range []string{
|
||||
"/etc/resolvconf/run/interface",
|
||||
"/run/resolvconf/interface",
|
||||
"/var/run/resolvconf/interface",
|
||||
} {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
ret.interfacesDir = path
|
||||
break
|
||||
}
|
||||
}
|
||||
if ret.interfacesDir == "" {
|
||||
// None of the paths seem to work, use the canonical location
|
||||
// that the current manpage says to use.
|
||||
ret.interfacesDir = "/etc/resolvconf/run/interfaces"
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (m *resolvconfManager) deleteCtrldConfig() error {
|
||||
cmd := exec.Command("resolvconf", "-d", resolvconfConfigName)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("running %s: %s", cmd, out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *resolvconfManager) SetDNS(config OSConfig) error {
|
||||
if !m.scriptInstalled {
|
||||
m.logf("injecting resolvconf workaround script")
|
||||
if err := os.MkdirAll(resolvconfLibcHookPath, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := atomicfile.WriteFile(resolvconfHookPath, workaroundScript, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
m.scriptInstalled = true
|
||||
}
|
||||
|
||||
if config.IsZero() {
|
||||
if err := m.deleteCtrldConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
stdin := new(bytes.Buffer)
|
||||
writeResolvConf(stdin, config.Nameservers, config.SearchDomains) // dns_direct.go
|
||||
|
||||
// This resolvconf implementation doesn't support exclusive
|
||||
// mode or interface priorities, so it will end up blending
|
||||
// our configuration with other sources. However, this will
|
||||
// get fixed up by the script we injected above.
|
||||
cmd := exec.Command("resolvconf", "-a", resolvconfConfigName)
|
||||
cmd.Stdin = stdin
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("running %s: %s", cmd, out)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *resolvconfManager) Close() error {
|
||||
if err := m.deleteCtrldConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if m.scriptInstalled {
|
||||
m.logf("removing resolvconf workaround script")
|
||||
os.Remove(resolvconfHookPath) // Best-effort
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *resolvconfManager) Mode() string {
|
||||
return "resolvconf"
|
||||
}
|
||||
@@ -144,6 +144,10 @@ type directManager struct {
|
||||
lastWarnContents []byte // last resolv.conf contents that we warned about
|
||||
}
|
||||
|
||||
func newDirectManager(logf logger.Logf) *directManager {
|
||||
return newDirectManagerOnFS(logf, directFS{})
|
||||
}
|
||||
|
||||
func newDirectManagerOnFS(logf logger.Logf, fs wholeFileFS) *directManager {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
m := &directManager{
|
||||
|
||||
39
internal/dns/manager_freebsd.go
Normal file
39
internal/dns/manager_freebsd.go
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func NewOSConfigurator(logf logger.Logf, _ string) (OSConfigurator, error) {
|
||||
bs, err := os.ReadFile("/etc/resolv.conf")
|
||||
if os.IsNotExist(err) {
|
||||
return newDirectManager(logf), nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading /etc/resolv.conf: %w", err)
|
||||
}
|
||||
|
||||
switch resolvOwner(bs) {
|
||||
case "resolvconf":
|
||||
switch resolvconfStyle() {
|
||||
case "":
|
||||
return newDirectManager(logf), nil
|
||||
case "debian":
|
||||
return newDebianResolvconfManager(logf)
|
||||
case "openresolv":
|
||||
return newOpenresolvManager()
|
||||
default:
|
||||
logf("[unexpected] got unknown flavor of resolvconf %q, falling back to direct manager", resolvconfStyle())
|
||||
return newDirectManager(logf), nil
|
||||
}
|
||||
default:
|
||||
return newDirectManager(logf), nil
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,10 @@ func NewOSConfigurator(logf logger.Logf, interfaceName string) (ret OSConfigurat
|
||||
return newResolvedManager(logf, interfaceName)
|
||||
case "network-manager":
|
||||
return newNMManager(interfaceName)
|
||||
case "debian-resolvconf":
|
||||
return newDebianResolvconfManager(logf)
|
||||
case "openresolv":
|
||||
return newOpenresolvManager()
|
||||
default:
|
||||
logf("[unexpected] detected unknown DNS mode %q, using direct manager as last resort", mode)
|
||||
return newDirectManagerOnFS(logf, env.fs), nil
|
||||
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"github.com/josharian/native"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/endian"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -31,6 +31,8 @@ type nmManager struct {
|
||||
dnsManager dbus.BusObject
|
||||
}
|
||||
|
||||
var _ OSConfigurator = (*nmManager)(nil)
|
||||
|
||||
func newNMManager(interfaceName string) (*nmManager, error) {
|
||||
conn, err := dbus.SystemBus()
|
||||
if err != nil {
|
||||
@@ -129,7 +131,7 @@ func (m *nmManager) trySet(ctx context.Context, config OSConfig) error {
|
||||
for _, ip := range config.Nameservers {
|
||||
b := ip.As16()
|
||||
if ip.Is4() {
|
||||
dnsv4 = append(dnsv4, endian.Native.Uint32(b[12:]))
|
||||
dnsv4 = append(dnsv4, native.Endian.Uint32(b[12:]))
|
||||
} else {
|
||||
dnsv6 = append(dnsv6, b[:])
|
||||
}
|
||||
|
||||
57
internal/dns/openresolv.go
Normal file
57
internal/dns/openresolv.go
Normal file
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux || freebsd || openbsd
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// openresolvManager manages DNS configuration using the openresolv
|
||||
// implementation of the `resolvconf` program.
|
||||
type openresolvManager struct{}
|
||||
|
||||
var _ OSConfigurator = (*openresolvManager)(nil)
|
||||
|
||||
func newOpenresolvManager() (openresolvManager, error) {
|
||||
return openresolvManager{}, nil
|
||||
}
|
||||
|
||||
func (m openresolvManager) deleteTailscaleConfig() error {
|
||||
cmd := exec.Command("resolvconf", "-f", "-d", "ctrld")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("running %s: %s", cmd, out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m openresolvManager) SetDNS(config OSConfig) error {
|
||||
if config.IsZero() {
|
||||
return m.deleteTailscaleConfig()
|
||||
}
|
||||
|
||||
var stdin bytes.Buffer
|
||||
writeResolvConf(&stdin, config.Nameservers, config.SearchDomains)
|
||||
|
||||
cmd := exec.Command("resolvconf", "-m", "0", "-x", "-a", "ctrld")
|
||||
cmd.Stdin = &stdin
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("running %s: %s", cmd, out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m openresolvManager) Close() error {
|
||||
return m.deleteTailscaleConfig()
|
||||
}
|
||||
|
||||
func (m openresolvManager) Mode() string {
|
||||
return "resolvconf"
|
||||
}
|
||||
@@ -13,6 +13,8 @@ import (
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
var _ OSConfigurator = (*directManager)(nil)
|
||||
|
||||
// An OSConfigurator applies DNS settings to the operating system.
|
||||
type OSConfigurator interface {
|
||||
// SetDNS updates the OS's DNS configuration to match cfg.
|
||||
|
||||
63
internal/dns/resolvconf-workaround.sh
Normal file
63
internal/dns/resolvconf-workaround.sh
Normal file
@@ -0,0 +1,63 @@
|
||||
#!/bin/sh
|
||||
# Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
# Use of this source code is governed by a BSD-style
|
||||
# license that can be found in the LICENSE file.
|
||||
#
|
||||
# This script is a workaround for a vpn-unfriendly behavior of the
|
||||
# original resolvconf by Thomas Hood. Unlike the `openresolv`
|
||||
# implementation (whose binary is also called resolvconf,
|
||||
# confusingly), the original resolvconf lacks a way to specify
|
||||
# "exclusive mode" for a provider configuration. In practice, this
|
||||
# means that if Ctrld wants to install a DNS configuration, that
|
||||
# config will get "blended" with the configs from other sources,
|
||||
# rather than override those other sources.
|
||||
#
|
||||
# This script gets installed at /etc/resolvconf/update-libc.d, which
|
||||
# is a directory of hook scripts that get run after resolvconf's libc
|
||||
# helper has finished rewriting /etc/resolv.conf. It's meant to notify
|
||||
# consumers of resolv.conf of a new configuration.
|
||||
#
|
||||
# Instead, we use that hook mechanism to reach into resolvconf's
|
||||
# stuff, and rewrite the libc-generated resolv.conf to exclusively
|
||||
# contain Ctrld's configuration - effectively implementing
|
||||
# exclusive mode ourselves in post-production.
|
||||
|
||||
set -e
|
||||
|
||||
if [ -n "$CTRLD_RESOLVCONF_HOOK_LOOP" ]; then
|
||||
# Hook script being invoked by itself, skip.
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ ! -f ctrld.inet ]; then
|
||||
# Ctrld isn't trying to manage DNS, do nothing.
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! grep resolvconf /etc/resolv.conf >/dev/null; then
|
||||
# resolvconf isn't managing /etc/resolv.conf, do nothing.
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Write out a modified /etc/resolv.conf containing just our config.
|
||||
(
|
||||
if [ -f /etc/resolvconf/resolv.conf.d/head ]; then
|
||||
cat /etc/resolvconf/resolv.conf.d/head
|
||||
fi
|
||||
echo "# Ctrld workaround applied to set exclusive DNS configuration."
|
||||
cat tun-tailscale.inet
|
||||
if [ -f /etc/resolvconf/resolv.conf.d/base ]; then
|
||||
# Keep options and sortlist, discard other base things since
|
||||
# they're the things we're trying to override.
|
||||
grep -e 'sortlist ' -e 'options ' /etc/resolvconf/resolv.conf.d/base || true
|
||||
fi
|
||||
if [ -f /etc/resolvconf/resolv.conf.d/tail ]; then
|
||||
cat /etc/resolvconf/resolv.conf.d/tail
|
||||
fi
|
||||
) >/etc/resolv.conf
|
||||
|
||||
if [ -d /etc/resolvconf/update-libc.d ] ; then
|
||||
# Re-notify libc watchers that we've changed resolv.conf again.
|
||||
export CTRLD_RESOLVCONF_HOOK_LOOP=1
|
||||
exec run-parts /etc/resolvconf/update-libc.d
|
||||
fi
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/strs"
|
||||
)
|
||||
|
||||
// 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.TrimSpace(line)
|
||||
|
||||
if s, ok := strs.CutPrefix(line, "nameserver"); ok {
|
||||
if s, ok := strings.CutPrefix(line, "nameserver"); ok {
|
||||
nameserver := strings.TrimSpace(s)
|
||||
if len(nameserver) == len(s) {
|
||||
return nil, fmt.Errorf("missing space after \"nameserver\" in %q", line)
|
||||
@@ -76,7 +75,7 @@ func Parse(r io.Reader) (*Config, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
if s, ok := strs.CutPrefix(line, "search"); ok {
|
||||
if s, ok := strings.CutPrefix(line, "search"); ok {
|
||||
domains := strings.TrimSpace(s)
|
||||
if len(domains) == len(s) {
|
||||
// No leading space?!
|
||||
|
||||
@@ -37,14 +37,45 @@ const reconfigTimeout = time.Second
|
||||
// Clients connect to the bus and walk that same hierarchy to invoke
|
||||
// RPCs, get/set properties, or listen for signals.
|
||||
const (
|
||||
dbusResolvedObject = "org.freedesktop.resolve1"
|
||||
dbusResolvedPath dbus.ObjectPath = "/org/freedesktop/resolve1"
|
||||
dbusResolvedInterface = "org.freedesktop.resolve1.Manager"
|
||||
dbusPath dbus.ObjectPath = "/org/freedesktop/DBus"
|
||||
dbusInterface = "org.freedesktop.DBus"
|
||||
dbusOwnerSignal = "NameOwnerChanged" // broadcast when a well-known name's owning process changes.
|
||||
dbusResolvedObject = "org.freedesktop.resolve1"
|
||||
dbusNetworkdObject = "org.freedesktop.network1"
|
||||
dbusResolvedPath dbus.ObjectPath = "/org/freedesktop/resolve1"
|
||||
dbusNetworkdPath dbus.ObjectPath = "/org/freedesktop/network1"
|
||||
dbusResolvedInterface = "org.freedesktop.resolve1.Manager"
|
||||
dbusNetworkdInterface = "org.freedesktop.network1.Manager"
|
||||
dbusPath dbus.ObjectPath = "/org/freedesktop/DBus"
|
||||
dbusInterface = "org.freedesktop.DBus"
|
||||
dbusOwnerSignal = "NameOwnerChanged" // broadcast when a well-known name's owning process changes.
|
||||
dbusResolvedErrorLinkBusy = "org.freedesktop.resolve1.LinkBusy"
|
||||
)
|
||||
|
||||
var (
|
||||
dbusSetLinkDNS string
|
||||
dbusSetLinkDomains string
|
||||
dbusSetLinkDefaultRoute string
|
||||
dbusSetLinkLLMNR string
|
||||
dbusSetLinkMulticastDNS string
|
||||
dbusSetLinkDNSSEC string
|
||||
dbusSetLinkDNSOverTLS string
|
||||
dbusFlushCaches string
|
||||
dbusRevertLink string
|
||||
)
|
||||
|
||||
func setDbusMethods(dbusInterface string) {
|
||||
dbusSetLinkDNS = dbusInterface + ".SetLinkDNS"
|
||||
dbusSetLinkDomains = dbusInterface + ".SetLinkDomains"
|
||||
dbusSetLinkDefaultRoute = dbusInterface + ".SetLinkDefaultRoute"
|
||||
dbusSetLinkLLMNR = dbusInterface + ".SetLinkLLMNR"
|
||||
dbusSetLinkMulticastDNS = dbusInterface + ".SetLinkMulticastDNS"
|
||||
dbusSetLinkDNSSEC = dbusInterface + ".SetLinkDNSSEC"
|
||||
dbusSetLinkDNSOverTLS = dbusInterface + ".SetLinkDNSOverTLS"
|
||||
dbusFlushCaches = dbusInterface + ".FlushCaches"
|
||||
dbusRevertLink = dbusInterface + ".RevertLink"
|
||||
if dbusInterface == dbusNetworkdInterface {
|
||||
dbusRevertLink = dbusInterface + ".RevertLinkDNS"
|
||||
}
|
||||
}
|
||||
|
||||
type resolvedLinkNameserver struct {
|
||||
Family int32
|
||||
Address []byte
|
||||
@@ -70,8 +101,11 @@ type resolvedManager struct {
|
||||
ifidx int
|
||||
|
||||
configCR chan changeRequest // tracks OSConfigs changes and error responses
|
||||
revertCh chan struct{}
|
||||
}
|
||||
|
||||
var _ OSConfigurator = (*resolvedManager)(nil)
|
||||
|
||||
func newResolvedManager(logf logger.Logf, interfaceName string) (*resolvedManager, error) {
|
||||
iface, err := net.InterfaceByName(interfaceName)
|
||||
if err != nil {
|
||||
@@ -89,6 +123,7 @@ func newResolvedManager(logf logger.Logf, interfaceName string) (*resolvedManage
|
||||
ifidx: iface.Index,
|
||||
|
||||
configCR: make(chan changeRequest),
|
||||
revertCh: make(chan struct{}),
|
||||
}
|
||||
|
||||
go mgr.run(ctx)
|
||||
@@ -117,6 +152,16 @@ func (m *resolvedManager) SetDNS(config OSConfig) error {
|
||||
}
|
||||
}
|
||||
|
||||
func newResolvedObject(conn *dbus.Conn) dbus.BusObject {
|
||||
setDbusMethods(dbusResolvedInterface)
|
||||
return conn.Object(dbusResolvedObject, dbusResolvedPath)
|
||||
}
|
||||
|
||||
func newNetworkdObject(conn *dbus.Conn) dbus.BusObject {
|
||||
setDbusMethods(dbusNetworkdInterface)
|
||||
return conn.Object(dbusNetworkdObject, dbusNetworkdPath)
|
||||
}
|
||||
|
||||
func (m *resolvedManager) run(ctx context.Context) {
|
||||
var (
|
||||
conn *dbus.Conn
|
||||
@@ -131,6 +176,22 @@ func (m *resolvedManager) run(ctx context.Context) {
|
||||
}
|
||||
}()
|
||||
|
||||
newManager := newResolvedObject
|
||||
func() {
|
||||
conn, err := dbus.SystemBus()
|
||||
if err != nil {
|
||||
m.logf("dbus connection error: %v", err)
|
||||
return
|
||||
}
|
||||
rManager = newManager(conn)
|
||||
if call := rManager.CallWithContext(ctx, dbusRevertLink, 0, m.ifidx); call.Err != nil {
|
||||
if dbusErr, ok := call.Err.(dbus.Error); ok && dbusErr.Name == dbusResolvedErrorLinkBusy {
|
||||
m.logf("[v1] Using %s as manager", dbusNetworkdObject)
|
||||
newManager = newNetworkdObject
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Reconnect the systemBus if disconnected.
|
||||
reconnect := func() error {
|
||||
var err error
|
||||
@@ -151,7 +212,7 @@ func (m *resolvedManager) run(ctx context.Context) {
|
||||
return err
|
||||
}
|
||||
|
||||
rManager = conn.Object(dbusResolvedObject, dbus.ObjectPath(dbusResolvedPath))
|
||||
rManager = newManager(conn)
|
||||
|
||||
// Only receive the DBus signals we need to resync our config on
|
||||
// resolved restart. Failure to set filters isn't a fatal error,
|
||||
@@ -160,6 +221,9 @@ func (m *resolvedManager) run(ctx context.Context) {
|
||||
if err = conn.AddMatchSignal(dbus.WithMatchObjectPath(dbusPath), dbus.WithMatchInterface(dbusInterface), dbus.WithMatchMember(dbusOwnerSignal), dbus.WithMatchArg(0, dbusResolvedObject)); err != nil {
|
||||
m.logf("[v1] Setting DBus signal filter failed: %v", err)
|
||||
}
|
||||
if err = conn.AddMatchSignal(dbus.WithMatchObjectPath(dbusPath), dbus.WithMatchInterface(dbusInterface), dbus.WithMatchMember(dbusOwnerSignal), dbus.WithMatchArg(0, dbusNetworkdObject)); err != nil {
|
||||
m.logf("[v1] Setting DBus signal filter failed: %v", err)
|
||||
}
|
||||
conn.Signal(signals)
|
||||
|
||||
// Reset backoff and SetNSOSHealth after successful on reconnect.
|
||||
@@ -179,13 +243,15 @@ func (m *resolvedManager) run(ctx context.Context) {
|
||||
if rManager == nil {
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
// RevertLink resets all per-interface settings on systemd-resolved to defaults.
|
||||
// When ctx goes away systemd-resolved auto reverts.
|
||||
// Keeping for potential use in future refactor.
|
||||
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".RevertLink", 0, m.ifidx); call.Err != nil {
|
||||
if call := rManager.CallWithContext(ctx, dbusRevertLink, 0, m.ifidx); call.Err != nil {
|
||||
m.logf("[v1] RevertLink: %v", call.Err)
|
||||
return
|
||||
}
|
||||
cancel()
|
||||
close(m.revertCh)
|
||||
return
|
||||
case configCR := <-m.configCR:
|
||||
// Track and update sync with latest config change.
|
||||
@@ -223,7 +289,7 @@ func (m *resolvedManager) run(ctx context.Context) {
|
||||
if len(signal.Body) != 3 {
|
||||
m.logf("[unexpected] DBus NameOwnerChanged len(Body) = %d, want 3")
|
||||
}
|
||||
if name, ok := signal.Body[0].(string); !ok || name != dbusResolvedObject {
|
||||
if name, ok := signal.Body[0].(string); !ok || (name != dbusResolvedObject && name != dbusNetworkdObject) {
|
||||
continue
|
||||
}
|
||||
newOwner, ok := signal.Body[2].(string)
|
||||
@@ -271,7 +337,7 @@ func (m *resolvedManager) setConfigOverDBus(ctx context.Context, rManager dbus.B
|
||||
}
|
||||
}
|
||||
err := rManager.CallWithContext(
|
||||
ctx, dbusResolvedInterface+".SetLinkDNS", 0,
|
||||
ctx, dbusSetLinkDNS, 0,
|
||||
m.ifidx, linkNameservers,
|
||||
).Store()
|
||||
if err != nil {
|
||||
@@ -311,14 +377,14 @@ func (m *resolvedManager) setConfigOverDBus(ctx context.Context, rManager dbus.B
|
||||
}
|
||||
|
||||
err = rManager.CallWithContext(
|
||||
ctx, dbusResolvedInterface+".SetLinkDomains", 0,
|
||||
ctx, dbusSetLinkDomains, 0,
|
||||
m.ifidx, linkDomains,
|
||||
).Store()
|
||||
if err != nil && err.Error() == "Argument list too long" { // TODO: better error match
|
||||
// Issue 3188: older systemd-resolved had argument length limits.
|
||||
// Trim out the *.arpa. entries and try again.
|
||||
err = rManager.CallWithContext(
|
||||
ctx, dbusResolvedInterface+".SetLinkDomains", 0,
|
||||
ctx, dbusSetLinkDomains, 0,
|
||||
m.ifidx, linkDomainsWithoutReverseDNS(linkDomains),
|
||||
).Store()
|
||||
}
|
||||
@@ -326,7 +392,7 @@ func (m *resolvedManager) setConfigOverDBus(ctx context.Context, rManager dbus.B
|
||||
return fmt.Errorf("setLinkDomains: %w", err)
|
||||
}
|
||||
|
||||
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".SetLinkDefaultRoute", 0, m.ifidx, len(config.MatchDomains) == 0); call.Err != nil {
|
||||
if call := rManager.CallWithContext(ctx, dbusSetLinkDefaultRoute, 0, m.ifidx, len(config.MatchDomains) == 0); call.Err != nil {
|
||||
if dbusErr, ok := call.Err.(dbus.Error); ok && dbusErr.Name == dbus.ErrMsgUnknownMethod.Name {
|
||||
// on some older systems like Kubuntu 18.04.6 with systemd 237 method SetLinkDefaultRoute is absent,
|
||||
// but otherwise it's working good
|
||||
@@ -341,33 +407,37 @@ func (m *resolvedManager) setConfigOverDBus(ctx context.Context, rManager dbus.B
|
||||
// or something).
|
||||
|
||||
// Disable LLMNR, we don't do multicast.
|
||||
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".SetLinkLLMNR", 0, m.ifidx, "no"); call.Err != nil {
|
||||
if call := rManager.CallWithContext(ctx, dbusSetLinkLLMNR, 0, m.ifidx, "no"); call.Err != nil {
|
||||
m.logf("[v1] failed to disable LLMNR: %v", call.Err)
|
||||
}
|
||||
|
||||
// Disable mdns.
|
||||
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".SetLinkMulticastDNS", 0, m.ifidx, "no"); call.Err != nil {
|
||||
if call := rManager.CallWithContext(ctx, dbusSetLinkMulticastDNS, 0, m.ifidx, "no"); call.Err != nil {
|
||||
m.logf("[v1] failed to disable mdns: %v", call.Err)
|
||||
}
|
||||
|
||||
// We don't support dnssec consistently right now, force it off to
|
||||
// avoid partial failures when we split DNS internally.
|
||||
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".SetLinkDNSSEC", 0, m.ifidx, "no"); call.Err != nil {
|
||||
if call := rManager.CallWithContext(ctx, dbusSetLinkDNSSEC, 0, m.ifidx, "no"); call.Err != nil {
|
||||
m.logf("[v1] failed to disable DNSSEC: %v", call.Err)
|
||||
}
|
||||
|
||||
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".SetLinkDNSOverTLS", 0, m.ifidx, "no"); call.Err != nil {
|
||||
if call := rManager.CallWithContext(ctx, dbusSetLinkDNSOverTLS, 0, m.ifidx, "no"); call.Err != nil {
|
||||
m.logf("[v1] failed to disable DoT: %v", call.Err)
|
||||
}
|
||||
|
||||
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".FlushCaches", 0); call.Err != nil {
|
||||
m.logf("failed to flush resolved DNS cache: %v", call.Err)
|
||||
if rManager.Path() == dbusResolvedPath {
|
||||
if call := rManager.CallWithContext(ctx, dbusFlushCaches, 0); call.Err != nil {
|
||||
m.logf("failed to flush resolved DNS cache: %v", call.Err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *resolvedManager) Close() error {
|
||||
m.cancel() // stops the 'run' method goroutine
|
||||
<-m.revertCh
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
114
internal/net/net.go
Normal file
114
internal/net/net.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package net
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"tailscale.com/logtail/backoff"
|
||||
)
|
||||
|
||||
const (
|
||||
controldIPv6Test = "ipv6.controld.io"
|
||||
controldIPv4Test = "ipv4.controld.io"
|
||||
bootstrapDNS = "76.76.2.0:53"
|
||||
)
|
||||
|
||||
var Dialer = &net.Dialer{
|
||||
Resolver: &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
d := net.Dialer{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
return d.DialContext(ctx, "udp", bootstrapDNS)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const probeStackTimeout = 2 * time.Second
|
||||
|
||||
var probeStackDialer = &net.Dialer{
|
||||
Resolver: Dialer.Resolver,
|
||||
Timeout: probeStackTimeout,
|
||||
}
|
||||
|
||||
var (
|
||||
stackOnce atomic.Pointer[sync.Once]
|
||||
ipv4Enabled bool
|
||||
ipv6Enabled bool
|
||||
canListenIPv6Local bool
|
||||
hasNetworkUp bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
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 {
|
||||
_, err := probeStackDialer.DialContext(ctx, "tcp6", net.JoinHostPort(controldIPv6Test, "80"))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func supportListenIPv6Local() bool {
|
||||
if ln, err := net.Listen("tcp6", "[::1]:0"); err == nil {
|
||||
ln.Close()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func probeStack() {
|
||||
b := backoff.NewBackoff("probeStack", func(format string, args ...any) {}, 5*time.Second)
|
||||
for {
|
||||
if _, err := probeStackDialer.Dial("udp", bootstrapDNS); err == nil {
|
||||
hasNetworkUp = true
|
||||
break
|
||||
} else {
|
||||
b.BackOff(context.Background(), err)
|
||||
}
|
||||
}
|
||||
ipv4Enabled = supportIPv4()
|
||||
ipv6Enabled = supportIPv6(context.Background())
|
||||
canListenIPv6Local = supportListenIPv6Local()
|
||||
}
|
||||
|
||||
func Up() bool {
|
||||
stackOnce.Load().Do(probeStack)
|
||||
return hasNetworkUp
|
||||
}
|
||||
|
||||
func SupportsIPv4() bool {
|
||||
stackOnce.Load().Do(probeStack)
|
||||
return ipv4Enabled
|
||||
}
|
||||
|
||||
func SupportsIPv6() bool {
|
||||
stackOnce.Load().Do(probeStack)
|
||||
return ipv6Enabled
|
||||
}
|
||||
|
||||
func SupportsIPv6ListenLocal() bool {
|
||||
stackOnce.Load().Do(probeStack)
|
||||
return canListenIPv6Local
|
||||
}
|
||||
|
||||
// IPv6Available is like SupportsIPv6, but always do the check without caching.
|
||||
func IPv6Available(ctx context.Context) bool {
|
||||
return supportIPv6(ctx)
|
||||
}
|
||||
|
||||
// IsIPv6 checks if the provided IP is v6.
|
||||
//
|
||||
//lint:ignore U1000 use in os_windows.go
|
||||
func IsIPv6(ip string) bool {
|
||||
parsedIP := net.ParseIP(ip)
|
||||
return parsedIP != nil && parsedIP.To4() == nil && parsedIP.To16() != nil
|
||||
}
|
||||
24
internal/net/net_test.go
Normal file
24
internal/net/net_test.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package net
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestProbeStackTimeout(t *testing.T) {
|
||||
done := make(chan struct{})
|
||||
started := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
close(started)
|
||||
supportIPv6(context.Background())
|
||||
}()
|
||||
|
||||
<-started
|
||||
select {
|
||||
case <-time.After(probeStackTimeout + time.Second):
|
||||
t.Error("probeStack timeout is not enforce")
|
||||
case <-done:
|
||||
}
|
||||
}
|
||||
29
nameservers.go
Normal file
29
nameservers.go
Normal file
@@ -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
|
||||
}
|
||||
75
nameservers_bsd.go
Normal file
75
nameservers_bsd.go
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
97
nameservers_linux.go
Normal file
97
nameservers_linux.go
Normal file
@@ -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
|
||||
}
|
||||
11
nameservers_test.go
Normal file
11
nameservers_test.go
Normal file
@@ -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()
|
||||
}
|
||||
@@ -2,70 +2,59 @@ package ctrld
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func nameservers() []string {
|
||||
aas, err := adapterAddresses()
|
||||
func dnsFns() []dnsFn {
|
||||
return []dnsFn{dnsFromAdapter}
|
||||
}
|
||||
|
||||
func dnsFromAdapter() []string {
|
||||
aas, err := winipcfg.GetAdaptersAddresses(syscall.AF_UNSPEC, winipcfg.GAAFlagIncludeGateways|winipcfg.GAAFlagIncludePrefix)
|
||||
if err != 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 dns := aa.FirstDnsServerAddress; dns != nil; dns = dns.Next {
|
||||
sa, err := dns.Address.Sockaddr.Sockaddr()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
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"))
|
||||
for dns := aa.FirstDNSServerAddress; dns != nil; dns = dns.Next {
|
||||
do(dns.Address)
|
||||
}
|
||||
for gw := aa.FirstGatewayAddress; gw != nil; gw = gw.Next {
|
||||
do(gw.Address)
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
43
resolver.go
43
resolver.go
@@ -5,7 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync/atomic"
|
||||
"sync"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
@@ -51,22 +51,49 @@ func NewResolver(uc *UpstreamConfig) (Resolver, error) {
|
||||
|
||||
type osResolver struct {
|
||||
nameservers []string
|
||||
next atomic.Uint32
|
||||
}
|
||||
|
||||
type osResolverResult struct {
|
||||
answer *dns.Msg
|
||||
err error
|
||||
}
|
||||
|
||||
// Resolve performs DNS resolvers using OS default nameservers. Nameserver is chosen from
|
||||
// available nameservers with a roundrobin algorithm.
|
||||
func (o *osResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) {
|
||||
numServers := uint32(len(o.nameservers))
|
||||
numServers := len(o.nameservers)
|
||||
if numServers == 0 {
|
||||
return nil, errors.New("no nameservers available")
|
||||
}
|
||||
next := o.next.Add(1)
|
||||
server := o.nameservers[(next-1)%numServers]
|
||||
dnsClient := &dns.Client{Net: "udp"}
|
||||
answer, _, err := dnsClient.ExchangeContext(ctx, msg, server)
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
return answer, err
|
||||
dnsClient := &dns.Client{Net: "udp"}
|
||||
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 {
|
||||
go func(server string) {
|
||||
defer wg.Done()
|
||||
answer, _, err := dnsClient.ExchangeContext(ctx, msg, server)
|
||||
ch <- &osResolverResult{answer: answer, err: err}
|
||||
}(server)
|
||||
}
|
||||
|
||||
errs := make([]error, 0, numServers)
|
||||
for res := range ch {
|
||||
if res.err == nil {
|
||||
cancel()
|
||||
return res.answer, res.err
|
||||
}
|
||||
errs = append(errs, res.err)
|
||||
}
|
||||
|
||||
return nil, joinErrors(errs...)
|
||||
}
|
||||
|
||||
func newDialer(dnsAddress string) *net.Dialer {
|
||||
|
||||
53
resolver_test.go
Normal file
53
resolver_test.go
Normal file
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
27
scripts/upx.sh
Executable file
27
scripts/upx.sh
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -ex
|
||||
|
||||
binary=$1
|
||||
|
||||
if [ -z "$binary" ]; then
|
||||
echo >&2 "Usage: $0 <binary>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "$binary" in
|
||||
*_freebsd_*)
|
||||
echo >&2 "upx does not work with freebsd binary yet"
|
||||
exit 0
|
||||
;;
|
||||
*_windows_arm*)
|
||||
echo >&2 "upx does not work with windows arm/arm64 binary yet"
|
||||
exit 0
|
||||
;;
|
||||
*_darwin_*)
|
||||
echo >&2 "upx claims to work with darwin binary, but testing show that it is broken"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
upx -- "$binary"
|
||||
Reference in New Issue
Block a user