mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-02-03 22:18:39 +00:00
Compare commits
140 Commits
release-br
...
v1.4.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e30ad31e0f | ||
|
|
81e0bad739 | ||
|
|
7d07d738dc | ||
|
|
0fae584e65 | ||
|
|
9e83085f2a | ||
|
|
41a00c68ac | ||
|
|
e3b99bf339 | ||
|
|
5007a87d3a | ||
|
|
60e65a37a6 | ||
|
|
d37d0e942c | ||
|
|
98042d8dbd | ||
|
|
af4b826b68 | ||
|
|
253a57ca01 | ||
|
|
caf98b4dfe | ||
|
|
398f71fd00 | ||
|
|
e1301ade96 | ||
|
|
7a23f82192 | ||
|
|
715bcc4aa1 | ||
|
|
0c74838740 | ||
|
|
4b05b6da7b | ||
|
|
375844ff1a | ||
|
|
1d207379cb | ||
|
|
fb49cb71e3 | ||
|
|
9618efbcde | ||
|
|
bb2210b06a | ||
|
|
917052723d | ||
|
|
fef85cadeb | ||
|
|
4a05fb6b28 | ||
|
|
6644ce53f2 | ||
|
|
72f0b89fdc | ||
|
|
41a97a6609 | ||
|
|
38064d6ad5 | ||
|
|
ae6945cedf | ||
|
|
3132d1b032 | ||
|
|
2716ae29bd | ||
|
|
1c50c2b6af | ||
|
|
cf6d16b439 | ||
|
|
60686f55ff | ||
|
|
47d7ace3a7 | ||
|
|
2d3779ec27 | ||
|
|
595071b608 | ||
|
|
57ef717080 | ||
|
|
eb27d1482b | ||
|
|
f57972ead7 | ||
|
|
168eaf538b | ||
|
|
1560455ca3 | ||
|
|
028475a193 | ||
|
|
f7a6dbe39b | ||
|
|
e573a490c9 | ||
|
|
ce3281e70d | ||
|
|
0fbfd160c9 | ||
|
|
20759017e6 | ||
|
|
69e0aab73e | ||
|
|
7ed6733fb7 | ||
|
|
9718ab8579 | ||
|
|
2687a4a018 | ||
|
|
2d9c60dea1 | ||
|
|
841be069b7 | ||
|
|
7833132917 | ||
|
|
e9e63b0983 | ||
|
|
4df470b869 | ||
|
|
89600f6091 | ||
|
|
f986a575e8 | ||
|
|
9c2fe8d21f | ||
|
|
8bcbb9249e | ||
|
|
a95d50c0af | ||
|
|
5db7d3577b | ||
|
|
c53a0ca1c4 | ||
|
|
6fd3d1788a | ||
|
|
087c1975e5 | ||
|
|
3713cbecc3 | ||
|
|
6046789fa4 | ||
|
|
3ea69b180c | ||
|
|
db6e977e3a | ||
|
|
a5c776c846 | ||
|
|
5a566c028a | ||
|
|
ff43c74d8d | ||
|
|
3c7255569c | ||
|
|
4a92ec4d2d | ||
|
|
9bbccb4082 | ||
|
|
4f62314646 | ||
|
|
cb49d0d947 | ||
|
|
89f7874fc6 | ||
|
|
221917e80b | ||
|
|
37d41bd215 | ||
|
|
8a96b8bec4 | ||
|
|
02ee113b95 | ||
|
|
f71dd78915 | ||
|
|
cd5619a05b | ||
|
|
a63a30c76b | ||
|
|
f5ba8be182 | ||
|
|
a9f76322bd | ||
|
|
ed39269c80 | ||
|
|
09426dcd36 | ||
|
|
17941882a9 | ||
|
|
70ab8032a0 | ||
|
|
8360bdc50a | ||
|
|
6837176ec7 | ||
|
|
5e9b4244e7 | ||
|
|
9b6a308958 | ||
|
|
71e327653a | ||
|
|
a56711796f | ||
|
|
09495f2a7c | ||
|
|
484643e114 | ||
|
|
da91aabc35 | ||
|
|
c654398981 | ||
|
|
47a90ec2a1 | ||
|
|
2875e22d0b | ||
|
|
c5d14e0075 | ||
|
|
84e06c363c | ||
|
|
5b9ccc5065 | ||
|
|
6ca1a7ccc7 | ||
|
|
9d666be5d4 | ||
|
|
65de7edcde | ||
|
|
0cdff0d368 | ||
|
|
f87220a908 | ||
|
|
30ea0c6499 | ||
|
|
9501e35c60 | ||
|
|
5ac9d17bdf | ||
|
|
cb14992ddc | ||
|
|
e88372fc8c | ||
|
|
b320662d67 | ||
|
|
ce353cd4d9 | ||
|
|
4befd33866 | ||
|
|
4b36e3ac44 | ||
|
|
f507bc8f9e | ||
|
|
14c88f4a6d | ||
|
|
3e388c2857 | ||
|
|
cfe1209d61 | ||
|
|
5a88a7c22c | ||
|
|
8c661c4401 | ||
|
|
e6f256d640 | ||
|
|
ede354166b | ||
|
|
282a8ce78e | ||
|
|
08fe04f1ee | ||
|
|
082d14a9ba | ||
|
|
617674ce43 | ||
|
|
7088df58dd | ||
|
|
9cbd9b3e44 | ||
|
|
e6586fd360 |
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: ["windows-latest", "ubuntu-latest", "macOS-latest"]
|
||||
go: ["1.21.x"]
|
||||
go: ["1.23.x"]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -21,6 +21,6 @@ jobs:
|
||||
- run: "go test -race ./..."
|
||||
- uses: dominikh/staticcheck-action@v1.2.0
|
||||
with:
|
||||
version: "2023.1.2"
|
||||
version: "2024.1.1"
|
||||
install-go: false
|
||||
cache-key: ${{ matrix.go }}
|
||||
|
||||
15
cmd/cli/ad_others.go
Normal file
15
cmd/cli/ad_others.go
Normal file
@@ -0,0 +1,15 @@
|
||||
//go:build !windows
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"github.com/Control-D-Inc/ctrld"
|
||||
)
|
||||
|
||||
// addExtraSplitDnsRule adds split DNS rule if present.
|
||||
func addExtraSplitDnsRule(_ *ctrld.Config) bool { return false }
|
||||
|
||||
// getActiveDirectoryDomain returns AD domain name of this computer.
|
||||
func getActiveDirectoryDomain() (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
73
cmd/cli/ad_windows.go
Normal file
73
cmd/cli/ad_windows.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/microsoft/wmi/pkg/base/host"
|
||||
hh "github.com/microsoft/wmi/pkg/hardware/host"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld"
|
||||
)
|
||||
|
||||
// addExtraSplitDnsRule adds split DNS rule for domain if it's part of active directory.
|
||||
func addExtraSplitDnsRule(cfg *ctrld.Config) bool {
|
||||
domain, err := getActiveDirectoryDomain()
|
||||
if err != nil {
|
||||
mainLog.Load().Debug().Msgf("unable to get active directory domain: %v", err)
|
||||
return false
|
||||
}
|
||||
if domain == "" {
|
||||
mainLog.Load().Debug().Msg("no active directory domain found")
|
||||
return false
|
||||
}
|
||||
// Network rules are lowercase during toml config marshaling,
|
||||
// lowercase the domain here too for consistency.
|
||||
domain = strings.ToLower(domain)
|
||||
domainRuleAdded := addSplitDnsRule(cfg, domain)
|
||||
wildcardDomainRuleRuleAdded := addSplitDnsRule(cfg, "*."+strings.TrimPrefix(domain, "."))
|
||||
return domainRuleAdded || wildcardDomainRuleRuleAdded
|
||||
}
|
||||
|
||||
// addSplitDnsRule adds split-rule for given domain if there's no existed rule.
|
||||
// The return value indicates whether the split-rule was added or not.
|
||||
func addSplitDnsRule(cfg *ctrld.Config, domain string) bool {
|
||||
for n, lc := range cfg.Listener {
|
||||
if lc.Policy == nil {
|
||||
lc.Policy = &ctrld.ListenerPolicyConfig{}
|
||||
}
|
||||
for _, rule := range lc.Policy.Rules {
|
||||
if _, ok := rule[domain]; ok {
|
||||
mainLog.Load().Debug().Msgf("split-rule %q already existed for listener.%s", domain, n)
|
||||
return false
|
||||
}
|
||||
}
|
||||
mainLog.Load().Debug().Msgf("adding split-rule %q for listener.%s", domain, n)
|
||||
lc.Policy.Rules = append(lc.Policy.Rules, ctrld.Rule{domain: []string{}})
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// getActiveDirectoryDomain returns AD domain name of this computer.
|
||||
func getActiveDirectoryDomain() (string, error) {
|
||||
log.SetOutput(io.Discard)
|
||||
defer log.SetOutput(os.Stderr)
|
||||
whost := host.NewWmiLocalHost()
|
||||
cs, err := hh.GetComputerSystem(whost)
|
||||
if cs != nil {
|
||||
defer cs.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
pod, err := cs.GetPropertyPartOfDomain()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if pod {
|
||||
return cs.GetPropertyDomain()
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
71
cmd/cli/ad_windows_test.go
Normal file
71
cmd/cli/ad_windows_test.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld"
|
||||
"github.com/Control-D-Inc/ctrld/testhelper"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_getActiveDirectoryDomain(t *testing.T) {
|
||||
start := time.Now()
|
||||
domain, err := getActiveDirectoryDomain()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("Using Windows API takes: %d", time.Since(start).Milliseconds())
|
||||
|
||||
start = time.Now()
|
||||
domainPowershell, err := getActiveDirectoryDomainPowershell()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("Using Powershell takes: %d", time.Since(start).Milliseconds())
|
||||
|
||||
if domain != domainPowershell {
|
||||
t.Fatalf("result mismatch, want: %v, got: %v", domainPowershell, domain)
|
||||
}
|
||||
}
|
||||
|
||||
func getActiveDirectoryDomainPowershell() (string, error) {
|
||||
cmd := "$obj = Get-WmiObject Win32_ComputerSystem; if ($obj.PartOfDomain) { $obj.Domain }"
|
||||
output, err := powershell(cmd)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get domain name: %w, output:\n\n%s", err, string(output))
|
||||
}
|
||||
return string(output), nil
|
||||
}
|
||||
|
||||
func Test_addSplitDnsRule(t *testing.T) {
|
||||
newCfg := func(domains ...string) *ctrld.Config {
|
||||
cfg := testhelper.SampleConfig(t)
|
||||
lc := cfg.Listener["0"]
|
||||
for _, domain := range domains {
|
||||
lc.Policy.Rules = append(lc.Policy.Rules, ctrld.Rule{domain: []string{}})
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg *ctrld.Config
|
||||
domain string
|
||||
added bool
|
||||
}{
|
||||
{"added", newCfg(), "example.com", true},
|
||||
{"TLD existed", newCfg("example.com"), "*.example.com", true},
|
||||
{"wildcard existed", newCfg("*.example.com"), "example.com", true},
|
||||
{"not added TLD", newCfg("example.com", "*.example.com"), "example.com", false},
|
||||
{"not added wildcard", newCfg("example.com", "*.example.com"), "*.example.com", false},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
added := addSplitDnsRule(tc.cfg, tc.domain)
|
||||
assert.Equal(t, tc.added, added)
|
||||
})
|
||||
}
|
||||
}
|
||||
1218
cmd/cli/cli.go
1218
cmd/cli/cli.go
File diff suppressed because it is too large
Load Diff
1362
cmd/cli/commands.go
Normal file
1362
cmd/cli/commands.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,10 @@ func newControlClient(addr string) *controlClient {
|
||||
}
|
||||
|
||||
func (c *controlClient) post(path string, data io.Reader) (*http.Response, error) {
|
||||
// for log/send, set the timeout to 5 minutes
|
||||
if path == sendLogsPath {
|
||||
c.c.Timeout = time.Minute * 5
|
||||
}
|
||||
return c.c.Post("http://unix"+path, contentTypeJson, data)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ package cli
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -11,10 +13,10 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/kardianos/service"
|
||||
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld"
|
||||
"github.com/Control-D-Inc/ctrld/internal/controld"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -25,8 +27,16 @@ const (
|
||||
deactivationPath = "/deactivation"
|
||||
cdPath = "/cd"
|
||||
ifacePath = "/iface"
|
||||
viewLogsPath = "/log/view"
|
||||
sendLogsPath = "/log/send"
|
||||
)
|
||||
|
||||
type ifaceResponse struct {
|
||||
Name string `json:"name"`
|
||||
All bool `json:"all"`
|
||||
OK bool `json:"ok"`
|
||||
}
|
||||
|
||||
type controlServer struct {
|
||||
server *http.Server
|
||||
mux *http.ServeMux
|
||||
@@ -152,8 +162,25 @@ func (p *prog) registerControlServerHandler() {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
p.cs.register(deactivationPath, http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
|
||||
// Non-cd mode or pin code not set, always allowing deactivation.
|
||||
if cdUID == "" || deactivationPinNotSet() {
|
||||
// Non-cd mode always allowing deactivation.
|
||||
if cdUID == "" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// Re-fetch pin code from API.
|
||||
if rc, err := controld.FetchResolverConfig(cdUID, rootCmd.Version, cdDev); rc != nil {
|
||||
if rc.DeactivationPin != nil {
|
||||
cdDeactivationPin.Store(*rc.DeactivationPin)
|
||||
} else {
|
||||
cdDeactivationPin.Store(defaultDeactivationPin)
|
||||
}
|
||||
} else {
|
||||
mainLog.Load().Warn().Err(err).Msg("could not re-fetch deactivation pin code")
|
||||
}
|
||||
|
||||
// If pin code not set, allowing deactivation.
|
||||
if deactivationPinNotSet() {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
@@ -167,7 +194,7 @@ func (p *prog) registerControlServerHandler() {
|
||||
|
||||
code := http.StatusForbidden
|
||||
switch req.Pin {
|
||||
case cdDeactivationPin:
|
||||
case cdDeactivationPin.Load():
|
||||
code = http.StatusOK
|
||||
case defaultDeactivationPin:
|
||||
// If the pin code was set, but users do not provide --pin, return proper code to client.
|
||||
@@ -184,15 +211,76 @@ func (p *prog) registerControlServerHandler() {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
}))
|
||||
p.cs.register(ifacePath, http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
|
||||
res := &ifaceResponse{Name: iface}
|
||||
// p.setDNS is only called when running as a service
|
||||
if !service.Interactive() {
|
||||
<-p.csSetDnsDone
|
||||
if p.csSetDnsOk {
|
||||
w.Write([]byte(iface))
|
||||
return
|
||||
res.Name = p.runningIface
|
||||
res.All = p.requiredMultiNICsConfig
|
||||
res.OK = true
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
if err := json.NewEncoder(w).Encode(res); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
http.Error(w, fmt.Sprintf("could not marshal iface data: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}))
|
||||
p.cs.register(viewLogsPath, http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
|
||||
lr, err := p.logReader()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer lr.r.Close()
|
||||
if lr.size == 0 {
|
||||
w.WriteHeader(http.StatusMovedPermanently)
|
||||
return
|
||||
}
|
||||
data, err := io.ReadAll(lr.r)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("could not read log: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if err := json.NewEncoder(w).Encode(&logViewResponse{Data: string(data)}); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
http.Error(w, fmt.Sprintf("could not marshal log data: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}))
|
||||
p.cs.register(sendLogsPath, http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
|
||||
if time.Since(p.internalLogSent) < logSentInterval {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
r, err := p.logReader()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if r.size == 0 {
|
||||
w.WriteHeader(http.StatusMovedPermanently)
|
||||
return
|
||||
}
|
||||
req := &controld.LogsRequest{
|
||||
UID: cdUID,
|
||||
Data: r.r,
|
||||
}
|
||||
mainLog.Load().Debug().Msg("sending log file to ControlD server")
|
||||
resp := logSentResponse{Size: r.size}
|
||||
if err := controld.SendLogs(req, cdDev); err != nil {
|
||||
mainLog.Load().Error().Msgf("could not send log file to ControlD server: %v", err)
|
||||
resp.Error = err.Error()
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
} else {
|
||||
mainLog.Load().Debug().Msg("sending log file successfully")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
if err := json.NewEncoder(w).Encode(&resp); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
p.internalLogSent = time.Now()
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,9 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -16,9 +18,9 @@ import (
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/netaddr"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/types/logger"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld"
|
||||
"github.com/Control-D-Inc/ctrld/internal/controld"
|
||||
@@ -41,7 +43,7 @@ const (
|
||||
var osUpstreamConfig = &ctrld.UpstreamConfig{
|
||||
Name: "OS resolver",
|
||||
Type: ctrld.ResolverTypeOS,
|
||||
Timeout: 2000,
|
||||
Timeout: 3000,
|
||||
}
|
||||
|
||||
var privateUpstreamConfig = &ctrld.UpstreamConfig{
|
||||
@@ -50,6 +52,12 @@ var privateUpstreamConfig = &ctrld.UpstreamConfig{
|
||||
Timeout: 2000,
|
||||
}
|
||||
|
||||
var localUpstreamConfig = &ctrld.UpstreamConfig{
|
||||
Name: "Local resolver",
|
||||
Type: ctrld.ResolverTypeLocal,
|
||||
Timeout: 2000,
|
||||
}
|
||||
|
||||
// proxyRequest contains data for proxying a DNS query to upstream.
|
||||
type proxyRequest struct {
|
||||
msg *dns.Msg
|
||||
@@ -76,7 +84,13 @@ type upstreamForResult struct {
|
||||
srcAddr string
|
||||
}
|
||||
|
||||
func (p *prog) serveDNS(listenerNum string) error {
|
||||
func (p *prog) serveDNS(mainCtx context.Context, listenerNum string) error {
|
||||
// Start network monitoring
|
||||
if err := p.monitorNetworkChanges(mainCtx); err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("Failed to start network monitoring")
|
||||
// Don't return here as we still want DNS service to run
|
||||
}
|
||||
|
||||
listenerConfig := p.cfg.Listener[listenerNum]
|
||||
// make sure ip is allocated
|
||||
if allocErr := p.allocateIP(listenerConfig.IP); allocErr != nil {
|
||||
@@ -106,11 +120,18 @@ func (p *prog) serveDNS(listenerNum string) error {
|
||||
go p.detectLoop(m)
|
||||
q := m.Question[0]
|
||||
domain := canonicalName(q.Name)
|
||||
if domain == selfCheckInternalTestDomain {
|
||||
switch {
|
||||
case domain == "":
|
||||
answer := new(dns.Msg)
|
||||
answer.SetRcode(m, dns.RcodeFormatError)
|
||||
_ = w.WriteMsg(answer)
|
||||
return
|
||||
case domain == selfCheckInternalTestDomain:
|
||||
answer := resolveInternalDomainTestQuery(ctx, domain, m)
|
||||
_ = w.WriteMsg(answer)
|
||||
return
|
||||
}
|
||||
|
||||
if _, ok := p.cacheFlushDomainsMap[domain]; ok && p.cache != nil {
|
||||
p.cache.Purge()
|
||||
ctrld.Log(ctx, mainLog.Load().Debug(), "received query %q, local cache is purged", domain)
|
||||
@@ -149,6 +170,7 @@ func (p *prog) serveDNS(listenerNum string) error {
|
||||
ufr: ur,
|
||||
})
|
||||
go p.doSelfUninstall(pr.answer)
|
||||
|
||||
answer = pr.answer
|
||||
rtt := time.Since(t)
|
||||
ctrld.Log(ctx, mainLog.Load().Debug(), "received response of %d bytes in %s", answer.Len(), rtt)
|
||||
@@ -166,6 +188,7 @@ func (p *prog) serveDNS(listenerNum string) error {
|
||||
go func() {
|
||||
p.WithLabelValuesInc(statsQueriesCount, labelValues...)
|
||||
p.WithLabelValuesInc(statsClientQueriesCount, []string{ci.IP, ci.Mac, ci.Hostname}...)
|
||||
p.forceFetchingAPI(domain)
|
||||
}()
|
||||
if err := w.WriteMsg(answer); err != nil {
|
||||
ctrld.Log(ctx, mainLog.Load().Error().Err(err), "serveDNS: failed to send DNS response to client")
|
||||
@@ -408,11 +431,20 @@ func (p *prog) proxy(ctx context.Context, req *proxyRequest) *proxyResponse {
|
||||
upstreams := req.ufr.upstreams
|
||||
serveStaleCache := p.cache != nil && p.cfg.Service.CacheServeStale
|
||||
upstreamConfigs := p.upstreamConfigsFromUpstreamNumbers(upstreams)
|
||||
|
||||
if len(upstreamConfigs) == 0 {
|
||||
upstreamConfigs = []*ctrld.UpstreamConfig{osUpstreamConfig}
|
||||
upstreams = []string{upstreamOS}
|
||||
}
|
||||
|
||||
if p.isAdDomainQuery(req.msg) {
|
||||
ctrld.Log(ctx, mainLog.Load().Debug(),
|
||||
"AD domain query detected for %s in domain %s",
|
||||
req.msg.Question[0].Name, p.adDomain)
|
||||
upstreamConfigs = []*ctrld.UpstreamConfig{localUpstreamConfig}
|
||||
upstreams = []string{upstreamOS}
|
||||
}
|
||||
|
||||
res := &proxyResponse{}
|
||||
|
||||
// LAN/PTR lookup flow:
|
||||
@@ -426,6 +458,11 @@ func (p *prog) proxy(ctx context.Context, req *proxyRequest) *proxyResponse {
|
||||
ctrld.Log(ctx, mainLog.Load().Debug(), "%s, %s, %s -> %v", req.ufr.matchedPolicy, req.ufr.matchedNetwork, req.ufr.matchedRule, upstreams)
|
||||
} else {
|
||||
switch {
|
||||
case isSrvLookup(req.msg):
|
||||
upstreams = []string{upstreamOS}
|
||||
upstreamConfigs = []*ctrld.UpstreamConfig{osUpstreamConfig}
|
||||
ctx = ctrld.LanQueryCtx(ctx)
|
||||
ctrld.Log(ctx, mainLog.Load().Debug(), "SRV record lookup, using upstreams: %v", upstreams)
|
||||
case isPrivatePtrLookup(req.msg):
|
||||
isLanOrPtrQuery = true
|
||||
if answer := p.proxyPrivatePtrLookup(ctx, req.msg); answer != nil {
|
||||
@@ -433,7 +470,8 @@ func (p *prog) proxy(ctx context.Context, req *proxyRequest) *proxyResponse {
|
||||
res.clientInfo = true
|
||||
return res
|
||||
}
|
||||
upstreams, upstreamConfigs = p.upstreamsAndUpstreamConfigForLanAndPtr(upstreams, upstreamConfigs)
|
||||
upstreams, upstreamConfigs = p.upstreamsAndUpstreamConfigForPtr(upstreams, upstreamConfigs)
|
||||
ctx = ctrld.LanQueryCtx(ctx)
|
||||
ctrld.Log(ctx, mainLog.Load().Debug(), "private PTR lookup, using upstreams: %v", upstreams)
|
||||
case isLanHostnameQuery(req.msg):
|
||||
isLanOrPtrQuery = true
|
||||
@@ -442,7 +480,9 @@ func (p *prog) proxy(ctx context.Context, req *proxyRequest) *proxyResponse {
|
||||
res.clientInfo = true
|
||||
return res
|
||||
}
|
||||
upstreams, upstreamConfigs = p.upstreamsAndUpstreamConfigForLanAndPtr(upstreams, upstreamConfigs)
|
||||
upstreams = []string{upstreamOS}
|
||||
upstreamConfigs = []*ctrld.UpstreamConfig{osUpstreamConfig}
|
||||
ctx = ctrld.LanQueryCtx(ctx)
|
||||
ctrld.Log(ctx, mainLog.Load().Debug(), "lan hostname lookup, using upstreams: %v", upstreams)
|
||||
default:
|
||||
ctrld.Log(ctx, mainLog.Load().Debug(), "no explicit policy matched, using default routing -> %v", upstreams)
|
||||
@@ -469,8 +509,8 @@ func (p *prog) proxy(ctx context.Context, req *proxyRequest) *proxyResponse {
|
||||
staleAnswer = answer
|
||||
}
|
||||
}
|
||||
resolve1 := func(n int, upstreamConfig *ctrld.UpstreamConfig, msg *dns.Msg) (*dns.Msg, error) {
|
||||
ctrld.Log(ctx, mainLog.Load().Debug(), "sending query to %s: %s", upstreams[n], upstreamConfig.Name)
|
||||
resolve1 := func(upstream string, upstreamConfig *ctrld.UpstreamConfig, msg *dns.Msg) (*dns.Msg, error) {
|
||||
ctrld.Log(ctx, mainLog.Load().Debug(), "sending query to %s: %s", upstream, upstreamConfig.Name)
|
||||
dnsResolver, err := ctrld.NewResolver(upstreamConfig)
|
||||
if err != nil {
|
||||
ctrld.Log(ctx, mainLog.Load().Error().Err(err), "failed to create resolver")
|
||||
@@ -485,42 +525,53 @@ func (p *prog) proxy(ctx context.Context, req *proxyRequest) *proxyResponse {
|
||||
}
|
||||
return dnsResolver.Resolve(resolveCtx, msg)
|
||||
}
|
||||
resolve := func(n int, upstreamConfig *ctrld.UpstreamConfig, msg *dns.Msg) *dns.Msg {
|
||||
resolve := func(upstream string, upstreamConfig *ctrld.UpstreamConfig, msg *dns.Msg) *dns.Msg {
|
||||
if upstreamConfig.UpstreamSendClientInfo() && req.ci != nil {
|
||||
ctrld.Log(ctx, mainLog.Load().Debug(), "including client info with the request")
|
||||
ctx = context.WithValue(ctx, ctrld.ClientInfoCtxKey{}, req.ci)
|
||||
}
|
||||
answer, err := resolve1(n, upstreamConfig, msg)
|
||||
answer, err := resolve1(upstream, upstreamConfig, msg)
|
||||
// if we have an answer, we should reset the failure count
|
||||
// we dont use reset here since we dont want to prevent failure counts from being incremented
|
||||
if answer != nil {
|
||||
p.um.mu.Lock()
|
||||
p.um.failureReq[upstream] = 0
|
||||
p.um.down[upstream] = false
|
||||
p.um.mu.Unlock()
|
||||
return answer
|
||||
}
|
||||
|
||||
ctrld.Log(ctx, mainLog.Load().Error().Err(err), "failed to resolve query")
|
||||
|
||||
// increase failure count when there is no answer
|
||||
// rehardless of what kind of error we get
|
||||
p.um.increaseFailureCount(upstream)
|
||||
|
||||
if err != nil {
|
||||
ctrld.Log(ctx, mainLog.Load().Error().Err(err), "failed to resolve query")
|
||||
if errNetworkError(err) {
|
||||
p.um.increaseFailureCount(upstreams[n])
|
||||
if p.um.isDown(upstreams[n]) {
|
||||
go p.um.checkUpstream(upstreams[n], upstreamConfig)
|
||||
}
|
||||
}
|
||||
// For timeout error (i.e: context deadline exceed), force re-bootstrapping.
|
||||
var e net.Error
|
||||
if errors.As(err, &e) && e.Timeout() {
|
||||
upstreamConfig.ReBootstrap()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return answer
|
||||
|
||||
return nil
|
||||
}
|
||||
for n, upstreamConfig := range upstreamConfigs {
|
||||
if upstreamConfig == nil {
|
||||
continue
|
||||
}
|
||||
logger := mainLog.Load().Debug().
|
||||
Str("upstream", upstreamConfig.String()).
|
||||
Str("query", req.msg.Question[0].Name).
|
||||
Bool("is_ad_query", p.isAdDomainQuery(req.msg)).
|
||||
Bool("is_lan_query", isLanOrPtrQuery)
|
||||
|
||||
if p.isLoop(upstreamConfig) {
|
||||
mainLog.Load().Warn().Msgf("dns loop detected, upstream: %q, endpoint: %q", upstreamConfig.Name, upstreamConfig.Endpoint)
|
||||
ctrld.Log(ctx, logger, "DNS loop detected")
|
||||
continue
|
||||
}
|
||||
if p.um.isDown(upstreams[n]) {
|
||||
ctrld.Log(ctx, mainLog.Load().Warn(), "%s is down", upstreams[n])
|
||||
continue
|
||||
}
|
||||
answer := resolve(n, upstreamConfig, req.msg)
|
||||
answer := resolve(upstreams[n], upstreamConfig, req.msg)
|
||||
if answer == nil {
|
||||
if serveStaleCache && staleAnswer != nil {
|
||||
ctrld.Log(ctx, mainLog.Load().Debug(), "serving stale cached response")
|
||||
@@ -567,13 +618,49 @@ func (p *prog) proxy(ctx context.Context, req *proxyRequest) *proxyResponse {
|
||||
return res
|
||||
}
|
||||
ctrld.Log(ctx, mainLog.Load().Error(), "all %v endpoints failed", upstreams)
|
||||
|
||||
// if we have no healthy upstreams, trigger recovery flow
|
||||
if p.recoverOnUpstreamFailure() {
|
||||
if p.um.countHealthy(upstreams) == 0 {
|
||||
p.recoveryCancelMu.Lock()
|
||||
if p.recoveryCancel == nil {
|
||||
var reason RecoveryReason
|
||||
if upstreams[0] == upstreamOS {
|
||||
reason = RecoveryReasonOSFailure
|
||||
} else {
|
||||
reason = RecoveryReasonRegularFailure
|
||||
}
|
||||
mainLog.Load().Debug().Msgf("No healthy upstreams, triggering recovery with reason: %v", reason)
|
||||
go p.handleRecovery(reason)
|
||||
} else {
|
||||
mainLog.Load().Debug().Msg("Recovery already in progress; skipping duplicate trigger from down detection")
|
||||
}
|
||||
p.recoveryCancelMu.Unlock()
|
||||
} else {
|
||||
mainLog.Load().Debug().Msg("One upstream is down but at least one is healthy; skipping recovery trigger")
|
||||
}
|
||||
}
|
||||
|
||||
// attempt query to OS resolver while as a retry catch all
|
||||
if upstreams[0] != upstreamOS {
|
||||
ctrld.Log(ctx, mainLog.Load().Debug(), "attempting query to OS resolver as a retry catch all")
|
||||
answer := resolve(upstreamOS, osUpstreamConfig, req.msg)
|
||||
if answer != nil {
|
||||
ctrld.Log(ctx, mainLog.Load().Debug(), "OS resolver retry query successful")
|
||||
res.answer = answer
|
||||
res.upstream = osUpstreamConfig.Endpoint
|
||||
return res
|
||||
}
|
||||
ctrld.Log(ctx, mainLog.Load().Debug(), "OS resolver retry query failed")
|
||||
}
|
||||
|
||||
answer := new(dns.Msg)
|
||||
answer.SetRcode(req.msg, dns.RcodeServerFailure)
|
||||
res.answer = answer
|
||||
return res
|
||||
}
|
||||
|
||||
func (p *prog) upstreamsAndUpstreamConfigForLanAndPtr(upstreams []string, upstreamConfigs []*ctrld.UpstreamConfig) ([]string, []*ctrld.UpstreamConfig) {
|
||||
func (p *prog) upstreamsAndUpstreamConfigForPtr(upstreams []string, upstreamConfigs []*ctrld.UpstreamConfig) ([]string, []*ctrld.UpstreamConfig) {
|
||||
if len(p.localUpstreams) > 0 {
|
||||
tmp := make([]string, 0, len(p.localUpstreams)+len(upstreams))
|
||||
tmp = append(tmp, p.localUpstreams...)
|
||||
@@ -592,6 +679,14 @@ func (p *prog) upstreamConfigsFromUpstreamNumbers(upstreams []string) []*ctrld.U
|
||||
return upstreamConfigs
|
||||
}
|
||||
|
||||
func (p *prog) isAdDomainQuery(msg *dns.Msg) bool {
|
||||
if p.adDomain == "" {
|
||||
return false
|
||||
}
|
||||
cDomainName := canonicalName(msg.Question[0].Name)
|
||||
return dns.IsSubDomain(p.adDomain, cDomainName)
|
||||
}
|
||||
|
||||
// canonicalName returns canonical name from FQDN with "." trimmed.
|
||||
func canonicalName(fqdn string) string {
|
||||
q := strings.TrimSpace(fqdn)
|
||||
@@ -602,14 +697,15 @@ func canonicalName(fqdn string) string {
|
||||
return q
|
||||
}
|
||||
|
||||
// wildcardMatches reports whether string str matches the wildcard pattern.
|
||||
// wildcardMatches reports whether string str matches the wildcard pattern in case-insensitive manner.
|
||||
func wildcardMatches(wildcard, str string) bool {
|
||||
// Wildcard match.
|
||||
wildCardParts := strings.Split(wildcard, "*")
|
||||
wildCardParts := strings.Split(strings.ToLower(wildcard), "*")
|
||||
if len(wildCardParts) != 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
str = strings.ToLower(str)
|
||||
switch {
|
||||
case len(wildCardParts[0]) > 0 && len(wildCardParts[1]) > 0:
|
||||
// Domain must match both prefix and suffix.
|
||||
@@ -817,7 +913,7 @@ func (p *prog) getClientInfo(remoteIP string, msg *dns.Msg) *ctrld.ClientInfo {
|
||||
} else {
|
||||
ci.Hostname = p.ciTable.LookupHostname(ci.IP, ci.Mac)
|
||||
}
|
||||
ci.Self = queryFromSelf(ci.IP)
|
||||
ci.Self = p.queryFromSelf(ci.IP)
|
||||
// If this is a query from self, but ci.IP is not loopback IP,
|
||||
// try using hostname mapping for lookback IP if presents.
|
||||
if ci.Self {
|
||||
@@ -887,29 +983,59 @@ func (p *prog) selfUninstallCoolOfPeriod() {
|
||||
p.selfUninstallMu.Unlock()
|
||||
}
|
||||
|
||||
// forceFetchingAPI sends signal to force syncing API config if run in cd mode,
|
||||
// and the domain == "cdUID.verify.controld.com"
|
||||
func (p *prog) forceFetchingAPI(domain string) {
|
||||
if cdUID == "" {
|
||||
return
|
||||
}
|
||||
resolverID, parent, _ := strings.Cut(domain, ".")
|
||||
if resolverID != cdUID {
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case cdDev && parent == "verify.controld.dev":
|
||||
// match ControlD dev
|
||||
case parent == "verify.controld.com":
|
||||
// match ControlD
|
||||
default:
|
||||
return
|
||||
}
|
||||
_ = p.apiForceReloadGroup.DoChan("force_sync_api", func() (interface{}, error) {
|
||||
p.apiForceReloadCh <- struct{}{}
|
||||
// Wait here to prevent abusing API if we are flooded.
|
||||
time.Sleep(timeDurationOrDefault(p.cfg.Service.ForceRefetchWaitTime, 30) * time.Second)
|
||||
return nil, nil
|
||||
})
|
||||
}
|
||||
|
||||
// timeDurationOrDefault returns time duration value from n if not nil.
|
||||
// Otherwise, it returns time duration value defaultN.
|
||||
func timeDurationOrDefault(n *int, defaultN int) time.Duration {
|
||||
if n != nil && *n > 0 {
|
||||
return time.Duration(*n)
|
||||
}
|
||||
return time.Duration(defaultN)
|
||||
}
|
||||
|
||||
// queryFromSelf reports whether the input IP is from device running ctrld.
|
||||
func queryFromSelf(ip string) bool {
|
||||
func (p *prog) queryFromSelf(ip string) bool {
|
||||
if val, ok := p.queryFromSelfMap.Load(ip); ok {
|
||||
return val.(bool)
|
||||
}
|
||||
netIP := netip.MustParseAddr(ip)
|
||||
ifaces, err := interfaces.GetList()
|
||||
regularIPs, loopbackIPs, err := netmon.LocalAddresses()
|
||||
if err != nil {
|
||||
mainLog.Load().Warn().Err(err).Msg("could not get interfaces list")
|
||||
mainLog.Load().Warn().Err(err).Msg("could not get local addresses")
|
||||
return false
|
||||
}
|
||||
for _, iface := range ifaces {
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
mainLog.Load().Warn().Err(err).Msgf("could not get interfaces addresses: %s", iface.Name)
|
||||
continue
|
||||
}
|
||||
for _, a := range addrs {
|
||||
switch v := a.(type) {
|
||||
case *net.IPNet:
|
||||
if pfx, ok := netaddr.FromStdIPNet(v); ok && pfx.Addr().Compare(netIP) == 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, localIP := range slices.Concat(regularIPs, loopbackIPs) {
|
||||
if localIP.Compare(netIP) == 0 {
|
||||
p.queryFromSelfMap.Store(ip, true)
|
||||
return true
|
||||
}
|
||||
}
|
||||
p.queryFromSelfMap.Store(ip, false)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -985,7 +1111,16 @@ func isLanHostnameQuery(m *dns.Msg) bool {
|
||||
name := strings.TrimSuffix(q.Name, ".")
|
||||
return !strings.Contains(name, ".") ||
|
||||
strings.HasSuffix(name, ".domain") ||
|
||||
strings.HasSuffix(name, ".lan")
|
||||
strings.HasSuffix(name, ".lan") ||
|
||||
strings.HasSuffix(name, ".local")
|
||||
}
|
||||
|
||||
// isSrvLookup reports whether DNS message is a SRV query.
|
||||
func isSrvLookup(m *dns.Msg) bool {
|
||||
if m == nil || len(m.Question) == 0 {
|
||||
return false
|
||||
}
|
||||
return m.Question[0].Qtype == dns.TypeSRV
|
||||
}
|
||||
|
||||
// isWanClient reports whether the input is a WAN address.
|
||||
@@ -1018,3 +1153,406 @@ func resolveInternalDomainTestQuery(ctx context.Context, domain string, m *dns.M
|
||||
answer.SetReply(m)
|
||||
return answer
|
||||
}
|
||||
|
||||
// FlushDNSCache flushes the DNS cache on macOS.
|
||||
func FlushDNSCache() error {
|
||||
// if not macOS, return
|
||||
if runtime.GOOS != "darwin" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Flush the DNS cache via mDNSResponder.
|
||||
// This is typically needed on modern macOS systems.
|
||||
if out, err := exec.Command("killall", "-HUP", "mDNSResponder").CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to flush mDNSResponder: %w, output: %s", err, string(out))
|
||||
}
|
||||
|
||||
// Optionally, flush the directory services cache.
|
||||
if out, err := exec.Command("dscacheutil", "-flushcache").CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to flush dscacheutil: %w, output: %s", err, string(out))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// monitorNetworkChanges starts monitoring for network interface changes
|
||||
func (p *prog) monitorNetworkChanges(ctx context.Context) error {
|
||||
mon, err := netmon.New(logger.WithPrefix(mainLog.Load().Printf, "netmon: "))
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating network monitor: %w", err)
|
||||
}
|
||||
|
||||
mon.RegisterChangeCallback(func(delta *netmon.ChangeDelta) {
|
||||
// Get map of valid interfaces
|
||||
validIfaces := validInterfacesMap()
|
||||
|
||||
isMajorChange := mon.IsMajorChangeFrom(delta.Old, delta.New)
|
||||
|
||||
mainLog.Load().Debug().
|
||||
Interface("old_state", delta.Old).
|
||||
Interface("new_state", delta.New).
|
||||
Bool("is_major_change", isMajorChange).
|
||||
Msg("Network change detected")
|
||||
|
||||
changed := false
|
||||
activeInterfaceExists := false
|
||||
var changeIPs []netip.Prefix
|
||||
// Check each valid interface for changes
|
||||
for ifaceName := range validIfaces {
|
||||
oldIface, oldExists := delta.Old.Interface[ifaceName]
|
||||
newIface, newExists := delta.New.Interface[ifaceName]
|
||||
if !newExists {
|
||||
continue
|
||||
}
|
||||
|
||||
oldIPs := delta.Old.InterfaceIPs[ifaceName]
|
||||
newIPs := delta.New.InterfaceIPs[ifaceName]
|
||||
|
||||
// if a valid interface did not exist in old
|
||||
// check that its up and has usable IPs
|
||||
if !oldExists {
|
||||
// The interface is new (was not present in the old state).
|
||||
usableNewIPs := filterUsableIPs(newIPs)
|
||||
if newIface.IsUp() && len(usableNewIPs) > 0 {
|
||||
changed = true
|
||||
changeIPs = usableNewIPs
|
||||
mainLog.Load().Debug().
|
||||
Str("interface", ifaceName).
|
||||
Interface("new_ips", usableNewIPs).
|
||||
Msg("Interface newly appeared (was not present in old state)")
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Filter new IPs to only those that are usable.
|
||||
usableNewIPs := filterUsableIPs(newIPs)
|
||||
|
||||
// Check if interface is up and has usable IPs.
|
||||
if newIface.IsUp() && len(usableNewIPs) > 0 {
|
||||
activeInterfaceExists = true
|
||||
}
|
||||
|
||||
// Compare interface states and IPs (interfaceIPsEqual will itself filter the IPs).
|
||||
if !interfaceStatesEqual(&oldIface, &newIface) || !interfaceIPsEqual(oldIPs, newIPs) {
|
||||
if newIface.IsUp() && len(usableNewIPs) > 0 {
|
||||
changed = true
|
||||
changeIPs = usableNewIPs
|
||||
mainLog.Load().Debug().
|
||||
Str("interface", ifaceName).
|
||||
Interface("old_ips", oldIPs).
|
||||
Interface("new_ips", usableNewIPs).
|
||||
Msg("Interface state or IPs changed")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !changed {
|
||||
mainLog.Load().Debug().Msg("Ignoring interface change - no valid interfaces affected")
|
||||
return
|
||||
}
|
||||
|
||||
if !activeInterfaceExists {
|
||||
mainLog.Load().Debug().Msg("No active interfaces found, skipping reinitialization")
|
||||
return
|
||||
}
|
||||
|
||||
// Get IPs from default route interface in new state
|
||||
selfIP := defaultRouteIP()
|
||||
var ipv6 string
|
||||
|
||||
if delta.New.DefaultRouteInterface != "" {
|
||||
mainLog.Load().Debug().Msgf("default route interface: %s, IPs: %v", delta.New.DefaultRouteInterface, delta.New.InterfaceIPs[delta.New.DefaultRouteInterface])
|
||||
for _, ip := range delta.New.InterfaceIPs[delta.New.DefaultRouteInterface] {
|
||||
ipAddr, _ := netip.ParsePrefix(ip.String())
|
||||
addr := ipAddr.Addr()
|
||||
if selfIP == "" && addr.Is4() {
|
||||
mainLog.Load().Debug().Msgf("checking IP: %s", addr.String())
|
||||
if !addr.IsLoopback() && !addr.IsLinkLocalUnicast() {
|
||||
selfIP = addr.String()
|
||||
}
|
||||
}
|
||||
if addr.Is6() && !addr.IsLoopback() && !addr.IsLinkLocalUnicast() {
|
||||
ipv6 = addr.String()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If no default route interface is set yet, use the changed IPs
|
||||
mainLog.Load().Debug().Msgf("no default route interface found, using changed IPs: %v", changeIPs)
|
||||
for _, ip := range changeIPs {
|
||||
ipAddr, _ := netip.ParsePrefix(ip.String())
|
||||
addr := ipAddr.Addr()
|
||||
if selfIP == "" && addr.Is4() {
|
||||
mainLog.Load().Debug().Msgf("checking IP: %s", addr.String())
|
||||
if !addr.IsLoopback() && !addr.IsLinkLocalUnicast() {
|
||||
selfIP = addr.String()
|
||||
}
|
||||
}
|
||||
if addr.Is6() && !addr.IsLoopback() && !addr.IsLinkLocalUnicast() {
|
||||
ipv6 = addr.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ip := net.ParseIP(selfIP); ip != nil {
|
||||
ctrld.SetDefaultLocalIPv4(ip)
|
||||
if !isMobile() && p.ciTable != nil {
|
||||
p.ciTable.SetSelfIP(selfIP)
|
||||
}
|
||||
}
|
||||
if ip := net.ParseIP(ipv6); ip != nil {
|
||||
ctrld.SetDefaultLocalIPv6(ip)
|
||||
}
|
||||
mainLog.Load().Debug().Msgf("Set default local IPv4: %s, IPv6: %s", selfIP, ipv6)
|
||||
|
||||
if p.recoverOnUpstreamFailure() {
|
||||
p.handleRecovery(RecoveryReasonNetworkChange)
|
||||
}
|
||||
})
|
||||
|
||||
mon.Start()
|
||||
mainLog.Load().Debug().Msg("Network monitor started")
|
||||
return nil
|
||||
}
|
||||
|
||||
// interfaceStatesEqual compares two interface states
|
||||
func interfaceStatesEqual(a, b *netmon.Interface) bool {
|
||||
if a == nil || b == nil {
|
||||
return a == b
|
||||
}
|
||||
return a.IsUp() == b.IsUp()
|
||||
}
|
||||
|
||||
// filterUsableIPs is a helper that returns only "usable" IP prefixes,
|
||||
// filtering out link-local, loopback, multicast, unspecified, broadcast, or CGNAT addresses.
|
||||
func filterUsableIPs(prefixes []netip.Prefix) []netip.Prefix {
|
||||
var usable []netip.Prefix
|
||||
for _, p := range prefixes {
|
||||
addr := p.Addr()
|
||||
if addr.IsLinkLocalUnicast() ||
|
||||
addr.IsLoopback() ||
|
||||
addr.IsMulticast() ||
|
||||
addr.IsUnspecified() ||
|
||||
addr.IsLinkLocalMulticast() ||
|
||||
(addr.Is4() && addr.String() == "255.255.255.255") ||
|
||||
tsaddr.CGNATRange().Contains(addr) {
|
||||
continue
|
||||
}
|
||||
usable = append(usable, p)
|
||||
}
|
||||
return usable
|
||||
}
|
||||
|
||||
// Modified interfaceIPsEqual compares only the usable (non-link local, non-loopback, etc.) IP addresses.
|
||||
func interfaceIPsEqual(a, b []netip.Prefix) bool {
|
||||
aUsable := filterUsableIPs(a)
|
||||
bUsable := filterUsableIPs(b)
|
||||
if len(aUsable) != len(bUsable) {
|
||||
return false
|
||||
}
|
||||
|
||||
aMap := make(map[string]bool)
|
||||
for _, ip := range aUsable {
|
||||
aMap[ip.String()] = true
|
||||
}
|
||||
for _, ip := range bUsable {
|
||||
if !aMap[ip.String()] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// checkUpstreamOnce sends a test query to the specified upstream.
|
||||
// Returns nil if the upstream responds successfully.
|
||||
func (p *prog) checkUpstreamOnce(upstream string, uc *ctrld.UpstreamConfig) error {
|
||||
mainLog.Load().Debug().Msgf("Starting check for upstream: %s", upstream)
|
||||
|
||||
resolver, err := ctrld.NewResolver(uc)
|
||||
if err != nil {
|
||||
mainLog.Load().Error().Err(err).Msgf("Failed to create resolver for upstream %s", upstream)
|
||||
return err
|
||||
}
|
||||
|
||||
msg := new(dns.Msg)
|
||||
msg.SetQuestion(".", dns.TypeNS)
|
||||
|
||||
timeout := 1000 * time.Millisecond
|
||||
if uc.Timeout > 0 {
|
||||
timeout = time.Millisecond * time.Duration(uc.Timeout)
|
||||
}
|
||||
mainLog.Load().Debug().Msgf("Timeout for upstream %s: %s", upstream, timeout)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
uc.ReBootstrap()
|
||||
mainLog.Load().Debug().Msgf("Rebootstrapping resolver for upstream: %s", upstream)
|
||||
|
||||
start := time.Now()
|
||||
_, err = resolver.Resolve(ctx, msg)
|
||||
duration := time.Since(start)
|
||||
|
||||
if err != nil {
|
||||
mainLog.Load().Error().Err(err).Msgf("Upstream %s check failed after %v", upstream, duration)
|
||||
} else {
|
||||
mainLog.Load().Debug().Msgf("Upstream %s responded successfully in %v", upstream, duration)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// handleRecovery performs a unified recovery by removing DNS settings,
|
||||
// canceling existing recovery checks for network changes, but coalescing duplicate
|
||||
// upstream failure recoveries, waiting for recovery to complete (using a cancellable context without timeout),
|
||||
// and then re-applying the DNS settings.
|
||||
func (p *prog) handleRecovery(reason RecoveryReason) {
|
||||
mainLog.Load().Debug().Msg("Starting recovery process: removing DNS settings")
|
||||
|
||||
// For network changes, cancel any existing recovery check because the network state has changed.
|
||||
if reason == RecoveryReasonNetworkChange {
|
||||
p.recoveryCancelMu.Lock()
|
||||
if p.recoveryCancel != nil {
|
||||
mainLog.Load().Debug().Msg("Cancelling existing recovery check (network change)")
|
||||
p.recoveryCancel()
|
||||
p.recoveryCancel = nil
|
||||
}
|
||||
p.recoveryCancelMu.Unlock()
|
||||
} else {
|
||||
// For upstream failures, if a recovery is already in progress, do nothing new.
|
||||
p.recoveryCancelMu.Lock()
|
||||
if p.recoveryCancel != nil {
|
||||
mainLog.Load().Debug().Msg("Upstream recovery already in progress; skipping duplicate trigger")
|
||||
p.recoveryCancelMu.Unlock()
|
||||
return
|
||||
}
|
||||
p.recoveryCancelMu.Unlock()
|
||||
}
|
||||
|
||||
// Create a new recovery context without a fixed timeout.
|
||||
p.recoveryCancelMu.Lock()
|
||||
recoveryCtx, cancel := context.WithCancel(context.Background())
|
||||
p.recoveryCancel = cancel
|
||||
p.recoveryCancelMu.Unlock()
|
||||
|
||||
// Immediately remove our DNS settings from the interface.
|
||||
// set recoveryRunning to true to prevent watchdogs from putting the listener back on the interface
|
||||
p.recoveryRunning.Store(true)
|
||||
p.resetDNS()
|
||||
|
||||
// For an OS failure, reinitialize OS resolver nameservers immediately.
|
||||
if reason == RecoveryReasonOSFailure {
|
||||
mainLog.Load().Debug().Msg("OS resolver failure detected; reinitializing OS resolver nameservers")
|
||||
ns := ctrld.InitializeOsResolver(true)
|
||||
if len(ns) == 0 {
|
||||
mainLog.Load().Warn().Msg("No nameservers found for OS resolver; using existing values")
|
||||
} else {
|
||||
mainLog.Load().Info().Msgf("Reinitialized OS resolver with nameservers: %v", ns)
|
||||
}
|
||||
}
|
||||
|
||||
// Build upstream map based on the recovery reason.
|
||||
upstreams := p.buildRecoveryUpstreams(reason)
|
||||
|
||||
// Wait indefinitely until one of the upstreams recovers.
|
||||
recovered, err := p.waitForUpstreamRecovery(recoveryCtx, upstreams)
|
||||
if err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("Recovery canceled; DNS settings remain removed")
|
||||
p.recoveryCancelMu.Lock()
|
||||
p.recoveryCancel = nil
|
||||
p.recoveryCancelMu.Unlock()
|
||||
return
|
||||
}
|
||||
mainLog.Load().Info().Msgf("Upstream %q recovered; re-applying DNS settings", recovered)
|
||||
|
||||
// reset the upstream failure count and down state
|
||||
p.um.reset(recovered)
|
||||
|
||||
// For network changes we also reinitialize the OS resolver.
|
||||
if reason == RecoveryReasonNetworkChange {
|
||||
ns := ctrld.InitializeOsResolver(true)
|
||||
if len(ns) == 0 {
|
||||
mainLog.Load().Warn().Msg("No nameservers found for OS resolver during network-change recovery; using existing values")
|
||||
} else {
|
||||
mainLog.Load().Info().Msgf("Reinitialized OS resolver with nameservers: %v", ns)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply our DNS settings back and log the interface state.
|
||||
p.setDNS()
|
||||
p.logInterfacesState()
|
||||
|
||||
// allow watchdogs to put the listener back on the interface if its changed for any reason
|
||||
p.recoveryRunning.Store(false)
|
||||
|
||||
// Clear the recovery cancellation for a clean slate.
|
||||
p.recoveryCancelMu.Lock()
|
||||
p.recoveryCancel = nil
|
||||
p.recoveryCancelMu.Unlock()
|
||||
}
|
||||
|
||||
// waitForUpstreamRecovery checks the provided upstreams concurrently until one recovers.
|
||||
// It returns the name of the recovered upstream or an error if the check times out.
|
||||
func (p *prog) waitForUpstreamRecovery(ctx context.Context, upstreams map[string]*ctrld.UpstreamConfig) (string, error) {
|
||||
recoveredCh := make(chan string, 1)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
mainLog.Load().Debug().Msgf("Starting upstream recovery check for %d upstreams", len(upstreams))
|
||||
|
||||
for name, uc := range upstreams {
|
||||
wg.Add(1)
|
||||
go func(name string, uc *ctrld.UpstreamConfig) {
|
||||
defer wg.Done()
|
||||
mainLog.Load().Debug().Msgf("Starting recovery check loop for upstream: %s", name)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
mainLog.Load().Debug().Msgf("Context canceled for upstream %s", name)
|
||||
return
|
||||
default:
|
||||
// checkUpstreamOnce will reset any failure counters on success.
|
||||
if err := p.checkUpstreamOnce(name, uc); err == nil {
|
||||
mainLog.Load().Debug().Msgf("Upstream %s recovered successfully", name)
|
||||
select {
|
||||
case recoveredCh <- name:
|
||||
mainLog.Load().Debug().Msgf("Sent recovery notification for upstream %s", name)
|
||||
default:
|
||||
mainLog.Load().Debug().Msg("Recovery channel full, another upstream already recovered")
|
||||
}
|
||||
return
|
||||
}
|
||||
mainLog.Load().Debug().Msgf("Upstream %s check failed, sleeping before retry", name)
|
||||
time.Sleep(checkUpstreamBackoffSleep)
|
||||
}
|
||||
}
|
||||
}(name, uc)
|
||||
}
|
||||
|
||||
var recovered string
|
||||
select {
|
||||
case recovered = <-recoveredCh:
|
||||
case <-ctx.Done():
|
||||
return "", ctx.Err()
|
||||
}
|
||||
wg.Wait()
|
||||
return recovered, nil
|
||||
}
|
||||
|
||||
// buildRecoveryUpstreams constructs the map of upstream configurations to test.
|
||||
// For OS failures we supply the manual OS resolver upstream configuration.
|
||||
// For network change or regular failure we use the upstreams defined in p.cfg (ignoring OS).
|
||||
func (p *prog) buildRecoveryUpstreams(reason RecoveryReason) map[string]*ctrld.UpstreamConfig {
|
||||
upstreams := make(map[string]*ctrld.UpstreamConfig)
|
||||
switch reason {
|
||||
case RecoveryReasonOSFailure:
|
||||
upstreams[upstreamOS] = osUpstreamConfig
|
||||
case RecoveryReasonNetworkChange, RecoveryReasonRegularFailure:
|
||||
// Use all configured upstreams except any OS type.
|
||||
for k, uc := range p.cfg.Upstream {
|
||||
if uc.Type != ctrld.ResolverTypeOS {
|
||||
upstreams[upstreamPrefix+k] = uc
|
||||
}
|
||||
}
|
||||
}
|
||||
return upstreams
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ func Test_wildcardMatches(t *testing.T) {
|
||||
{"domain - suffix not match other", "suffix.*", "suffix1.windscribe.com", false},
|
||||
{"domain - both", "suffix.*.windscribe.com", "suffix.anything.windscribe.com", true},
|
||||
{"domain - both not match", "suffix.*.windscribe.com", "suffix1.suffix.windscribe.com", false},
|
||||
{"domain - case-insensitive", "*.WINDSCRIBE.com", "anything.windscribe.com", true},
|
||||
{"mac - prefix", "*:98:05:b4:2b", "d4:67:98:05:b4:2b", true},
|
||||
{"mac - prefix not match other s", "*:98:05:b4:2b", "0d:ba:54:09:94:2c", false},
|
||||
{"mac - prefix not match s in name", "*:98:05:b4:2b", "e4:67:97:05:b4:2b", false},
|
||||
@@ -74,6 +75,7 @@ func Test_canonicalName(t *testing.T) {
|
||||
|
||||
func Test_prog_upstreamFor(t *testing.T) {
|
||||
cfg := testhelper.SampleConfig(t)
|
||||
cfg.Service.LeakOnUpstreamFailure = func(v bool) *bool { return &v }(false)
|
||||
p := &prog{cfg: cfg}
|
||||
p.um = newUpstreamMonitor(p.cfg)
|
||||
p.lanLoopGuard = newLoopGuard()
|
||||
@@ -364,6 +366,9 @@ func Test_isLanHostnameQuery(t *testing.T) {
|
||||
{"A not LAN", newDnsMsgWithHostname("example.com", dns.TypeA), false},
|
||||
{"AAAA not LAN", newDnsMsgWithHostname("example.com", dns.TypeAAAA), false},
|
||||
{"Not A or AAAA", newDnsMsgWithHostname("foo", dns.TypeTXT), false},
|
||||
{".domain", newDnsMsgWithHostname("foo.domain", dns.TypeA), true},
|
||||
{".lan", newDnsMsgWithHostname("foo.lan", dns.TypeA), true},
|
||||
{".local", newDnsMsgWithHostname("foo.local", dns.TypeA), true},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
@@ -413,6 +418,26 @@ func Test_isPrivatePtrLookup(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_isSrvLookup(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
msg *dns.Msg
|
||||
isSrvLookup bool
|
||||
}{
|
||||
{"SRV", newDnsMsgWithHostname("foo", dns.TypeSRV), true},
|
||||
{"Not SRV", newDnsMsgWithHostname("foo", dns.TypeNone), false},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := isSrvLookup(tc.msg); tc.isSrvLookup != got {
|
||||
t.Errorf("unexpected result, want: %v, got: %v", tc.isSrvLookup, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_isWanClient(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
14
cmd/cli/hostname.go
Normal file
14
cmd/cli/hostname.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package cli
|
||||
|
||||
import "regexp"
|
||||
|
||||
// validHostname reports whether hostname is a valid hostname.
|
||||
// A valid hostname contains 3 -> 64 characters and conform to RFC1123.
|
||||
func validHostname(hostname string) bool {
|
||||
hostnameLen := len(hostname)
|
||||
if hostnameLen < 3 || hostnameLen > 64 {
|
||||
return false
|
||||
}
|
||||
validHostnameRfc1123 := regexp.MustCompile(`^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$`)
|
||||
return validHostnameRfc1123.MatchString(hostname)
|
||||
}
|
||||
35
cmd/cli/hostname_test.go
Normal file
35
cmd/cli/hostname_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_validHostname(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
hostname string
|
||||
valid bool
|
||||
}{
|
||||
{"localhost", "localhost", true},
|
||||
{"localdomain", "localhost.localdomain", true},
|
||||
{"localhost6", "localhost6.localdomain6", true},
|
||||
{"ip6", "ip6-localhost", true},
|
||||
{"non-domain", "controld", true},
|
||||
{"domain", "controld.com", true},
|
||||
{"empty", "", false},
|
||||
{"min length", "fo", false},
|
||||
{"max length", strings.Repeat("a", 65), false},
|
||||
{"special char", "foo!", false},
|
||||
{"non-ascii", "fooΩ", false},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.hostname, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.True(t, validHostname(tc.hostname) == tc.valid)
|
||||
})
|
||||
}
|
||||
}
|
||||
186
cmd/cli/log_writer.go
Normal file
186
cmd/cli/log_writer.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld"
|
||||
)
|
||||
|
||||
const (
|
||||
logWriterSize = 1024 * 1024 * 5 // 5 MB
|
||||
logWriterSmallSize = 1024 * 1024 * 1 // 1 MB
|
||||
logWriterInitialSize = 32 * 1024 // 32 KB
|
||||
logSentInterval = time.Minute
|
||||
logStartEndMarker = "\n\n=== INIT_END ===\n\n"
|
||||
logLogEndMarker = "\n\n=== LOG_END ===\n\n"
|
||||
logWarnEndMarker = "\n\n=== WARN_END ===\n\n"
|
||||
)
|
||||
|
||||
type logViewResponse struct {
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
type logSentResponse struct {
|
||||
Size int64 `json:"size"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type logReader struct {
|
||||
r io.ReadCloser
|
||||
size int64
|
||||
}
|
||||
|
||||
// logWriter is an internal buffer to keep track of runtime log when no logging is enabled.
|
||||
type logWriter struct {
|
||||
mu sync.Mutex
|
||||
buf bytes.Buffer
|
||||
size int
|
||||
}
|
||||
|
||||
// newLogWriter creates an internal log writer.
|
||||
func newLogWriter() *logWriter {
|
||||
return newLogWriterWithSize(logWriterSize)
|
||||
}
|
||||
|
||||
// newSmallLogWriter creates an internal log writer with small buffer size.
|
||||
func newSmallLogWriter() *logWriter {
|
||||
return newLogWriterWithSize(logWriterSmallSize)
|
||||
}
|
||||
|
||||
// newLogWriterWithSize creates an internal log writer with a given buffer size.
|
||||
func newLogWriterWithSize(size int) *logWriter {
|
||||
lw := &logWriter{size: size}
|
||||
return lw
|
||||
}
|
||||
|
||||
func (lw *logWriter) Write(p []byte) (int, error) {
|
||||
lw.mu.Lock()
|
||||
defer lw.mu.Unlock()
|
||||
|
||||
// If writing p causes overflows, discard old data.
|
||||
if lw.buf.Len()+len(p) > lw.size {
|
||||
buf := lw.buf.Bytes()
|
||||
buf = buf[:logWriterInitialSize]
|
||||
if idx := bytes.LastIndex(buf, []byte("\n")); idx != -1 {
|
||||
buf = buf[:idx]
|
||||
}
|
||||
lw.buf.Reset()
|
||||
lw.buf.Write(buf)
|
||||
lw.buf.WriteString(logStartEndMarker) // indicate that the log was truncated.
|
||||
}
|
||||
// If p is bigger than buffer size, truncate p by half until its size is smaller.
|
||||
for len(p)+lw.buf.Len() > lw.size {
|
||||
p = p[len(p)/2:]
|
||||
}
|
||||
return lw.buf.Write(p)
|
||||
}
|
||||
|
||||
// initInternalLogging performs internal logging if there's no log enabled.
|
||||
func (p *prog) initInternalLogging(writers []io.Writer) {
|
||||
if !p.needInternalLogging() {
|
||||
return
|
||||
}
|
||||
p.initInternalLogWriterOnce.Do(func() {
|
||||
mainLog.Load().Notice().Msg("internal logging enabled")
|
||||
p.internalLogWriter = newLogWriter()
|
||||
p.internalLogSent = time.Now().Add(-logSentInterval)
|
||||
p.internalWarnLogWriter = newSmallLogWriter()
|
||||
})
|
||||
p.mu.Lock()
|
||||
lw := p.internalLogWriter
|
||||
wlw := p.internalWarnLogWriter
|
||||
p.mu.Unlock()
|
||||
// If ctrld was run without explicit verbose level,
|
||||
// run the internal logging at debug level, so we could
|
||||
// have enough information for troubleshooting.
|
||||
if verbose == 0 {
|
||||
for i := range writers {
|
||||
w := &zerolog.FilteredLevelWriter{
|
||||
Writer: zerolog.LevelWriterAdapter{Writer: writers[i]},
|
||||
Level: zerolog.NoticeLevel,
|
||||
}
|
||||
writers[i] = w
|
||||
}
|
||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||
}
|
||||
writers = append(writers, lw)
|
||||
writers = append(writers, &zerolog.FilteredLevelWriter{
|
||||
Writer: zerolog.LevelWriterAdapter{Writer: wlw},
|
||||
Level: zerolog.WarnLevel,
|
||||
})
|
||||
multi := zerolog.MultiLevelWriter(writers...)
|
||||
l := mainLog.Load().Output(multi).With().Logger()
|
||||
mainLog.Store(&l)
|
||||
ctrld.ProxyLogger.Store(&l)
|
||||
}
|
||||
|
||||
// needInternalLogging reports whether prog needs to run internal logging.
|
||||
func (p *prog) needInternalLogging() bool {
|
||||
// Do not run in non-cd mode.
|
||||
if cdUID == "" {
|
||||
return false
|
||||
}
|
||||
// Do not run if there's already log file.
|
||||
if p.cfg.Service.LogPath != "" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *prog) logReader() (*logReader, error) {
|
||||
if p.needInternalLogging() {
|
||||
p.mu.Lock()
|
||||
lw := p.internalLogWriter
|
||||
wlw := p.internalWarnLogWriter
|
||||
p.mu.Unlock()
|
||||
if lw == nil {
|
||||
return nil, errors.New("nil internal log writer")
|
||||
}
|
||||
if wlw == nil {
|
||||
return nil, errors.New("nil internal warn log writer")
|
||||
}
|
||||
// Normal log content.
|
||||
lw.mu.Lock()
|
||||
lwReader := bytes.NewReader(lw.buf.Bytes())
|
||||
lwSize := lw.buf.Len()
|
||||
lw.mu.Unlock()
|
||||
// Warn log content.
|
||||
wlw.mu.Lock()
|
||||
wlwReader := bytes.NewReader(wlw.buf.Bytes())
|
||||
wlwSize := wlw.buf.Len()
|
||||
wlw.mu.Unlock()
|
||||
reader := io.MultiReader(lwReader, bytes.NewReader([]byte(logLogEndMarker)), wlwReader)
|
||||
lr := &logReader{r: io.NopCloser(reader)}
|
||||
lr.size = int64(lwSize + wlwSize)
|
||||
if lr.size == 0 {
|
||||
return nil, errors.New("internal log is empty")
|
||||
}
|
||||
return lr, nil
|
||||
}
|
||||
if p.cfg.Service.LogPath == "" {
|
||||
return &logReader{r: io.NopCloser(strings.NewReader(""))}, nil
|
||||
}
|
||||
f, err := os.Open(normalizeLogFilePath(p.cfg.Service.LogPath))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lr := &logReader{r: f}
|
||||
if st, err := f.Stat(); err == nil {
|
||||
lr.size = st.Size()
|
||||
} else {
|
||||
return nil, fmt.Errorf("f.Stat: %w", err)
|
||||
}
|
||||
if lr.size == 0 {
|
||||
return nil, errors.New("log file is empty")
|
||||
}
|
||||
return lr, nil
|
||||
}
|
||||
49
cmd/cli/log_writer_test.go
Normal file
49
cmd/cli/log_writer_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_logWriter_Write(t *testing.T) {
|
||||
size := 64 * 1024
|
||||
lw := &logWriter{size: size}
|
||||
lw.buf.Grow(lw.size)
|
||||
data := strings.Repeat("A", size)
|
||||
lw.Write([]byte(data))
|
||||
if lw.buf.String() != data {
|
||||
t.Fatalf("unexpected buf content: %v", lw.buf.String())
|
||||
}
|
||||
newData := "B"
|
||||
halfData := strings.Repeat("A", len(data)/2) + logStartEndMarker
|
||||
lw.Write([]byte(newData))
|
||||
if lw.buf.String() != halfData+newData {
|
||||
t.Fatalf("unexpected new buf content: %v", lw.buf.String())
|
||||
}
|
||||
|
||||
bigData := strings.Repeat("B", 256*1024)
|
||||
expected := halfData + strings.Repeat("B", 16*1024)
|
||||
lw.Write([]byte(bigData))
|
||||
if lw.buf.String() != expected {
|
||||
t.Fatalf("unexpected big buf content: %v", lw.buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func Test_logWriter_ConcurrentWrite(t *testing.T) {
|
||||
size := 64 * 1024
|
||||
lw := &logWriter{size: size}
|
||||
n := 10
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(n)
|
||||
for i := 0; i < n; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
lw.Write([]byte(strings.Repeat("A", i)))
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
if lw.buf.Len() > lw.size {
|
||||
t.Fatalf("unexpected buf size: %v, content: %q", lw.buf.Len(), lw.buf.String())
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ var (
|
||||
silent bool
|
||||
cdUID string
|
||||
cdOrg string
|
||||
customHostname string
|
||||
cdDev bool
|
||||
iface string
|
||||
ifaceStartStop string
|
||||
@@ -45,9 +46,10 @@ var (
|
||||
)
|
||||
|
||||
const (
|
||||
cdUidFlagName = "cd"
|
||||
cdOrgFlagName = "cd-org"
|
||||
nextdnsFlagName = "nextdns"
|
||||
cdUidFlagName = "cd"
|
||||
cdOrgFlagName = "cd-org"
|
||||
customHostnameFlagName = "custom-hostname"
|
||||
nextdnsFlagName = "nextdns"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -65,11 +67,8 @@ func Main() {
|
||||
}
|
||||
|
||||
func normalizeLogFilePath(logFilePath string) string {
|
||||
// In cleanup mode, we always want the full log file path.
|
||||
if !cleanup {
|
||||
if logFilePath == "" || filepath.IsAbs(logFilePath) || service.Interactive() {
|
||||
return logFilePath
|
||||
}
|
||||
if logFilePath == "" || filepath.IsAbs(logFilePath) || service.Interactive() {
|
||||
return logFilePath
|
||||
}
|
||||
if homedir != "" {
|
||||
return filepath.Join(homedir, logFilePath)
|
||||
@@ -102,9 +101,23 @@ func initConsoleLogging() {
|
||||
}
|
||||
|
||||
// initLogging initializes global logging setup.
|
||||
func initLogging() {
|
||||
func initLogging() []io.Writer {
|
||||
zerolog.TimeFieldFormat = time.RFC3339 + ".000"
|
||||
initLoggingWithBackup(true)
|
||||
return initLoggingWithBackup(true)
|
||||
}
|
||||
|
||||
// initInteractiveLogging is like initLogging, but the ProxyLogger is discarded
|
||||
// to be used for all interactive commands.
|
||||
//
|
||||
// Current log file config will also be ignored.
|
||||
func initInteractiveLogging() {
|
||||
old := cfg.Service.LogPath
|
||||
cfg.Service.LogPath = ""
|
||||
zerolog.TimeFieldFormat = time.RFC3339 + ".000"
|
||||
initLoggingWithBackup(false)
|
||||
cfg.Service.LogPath = old
|
||||
l := zerolog.New(io.Discard)
|
||||
ctrld.ProxyLogger.Store(&l)
|
||||
}
|
||||
|
||||
// initLoggingWithBackup initializes log setup base on current config.
|
||||
@@ -113,8 +126,8 @@ func initLogging() {
|
||||
// This is only used in runCmd for special handling in case of logging config
|
||||
// change in cd mode. Without special reason, the caller should use initLogging
|
||||
// wrapper instead of calling this function directly.
|
||||
func initLoggingWithBackup(doBackup bool) {
|
||||
writers := []io.Writer{io.Discard}
|
||||
func initLoggingWithBackup(doBackup bool) []io.Writer {
|
||||
var writers []io.Writer
|
||||
if logFilePath := normalizeLogFilePath(cfg.Service.LogPath); logFilePath != "" {
|
||||
// Create parent directory if necessary.
|
||||
if err := os.MkdirAll(filepath.Dir(logFilePath), 0750); err != nil {
|
||||
@@ -152,21 +165,22 @@ func initLoggingWithBackup(doBackup bool) {
|
||||
switch {
|
||||
case silent:
|
||||
zerolog.SetGlobalLevel(zerolog.NoLevel)
|
||||
return
|
||||
return writers
|
||||
case verbose == 1:
|
||||
logLevel = "info"
|
||||
case verbose > 1:
|
||||
logLevel = "debug"
|
||||
}
|
||||
if logLevel == "" {
|
||||
return
|
||||
return writers
|
||||
}
|
||||
level, err := zerolog.ParseLevel(logLevel)
|
||||
if err != nil {
|
||||
mainLog.Load().Warn().Err(err).Msg("could not set log level")
|
||||
return
|
||||
return writers
|
||||
}
|
||||
zerolog.SetGlobalLevel(level)
|
||||
return writers
|
||||
}
|
||||
|
||||
func initCache() {
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
package cli
|
||||
|
||||
import "strings"
|
||||
|
||||
// Copied from https://gist.github.com/Ultraporing/fe52981f678be6831f747c206a4861cb
|
||||
|
||||
// Mac Address parts to look for, and identify non-physical devices. There may be more, update me!
|
||||
var macAddrPartsToFilter = []string{
|
||||
"00:03:FF", // Microsoft Hyper-V, Virtual Server, Virtual PC
|
||||
"0A:00:27", // VirtualBox
|
||||
"00:00:00:00:00", // Teredo Tunneling Pseudo-Interface
|
||||
"00:50:56", // VMware ESX 3, Server, Workstation, Player
|
||||
"00:1C:14", // VMware ESX 3, Server, Workstation, Player
|
||||
"00:0C:29", // VMware ESX 3, Server, Workstation, Player
|
||||
"00:05:69", // VMware ESX 3, Server, Workstation, Player
|
||||
"00:1C:42", // Microsoft Hyper-V, Virtual Server, Virtual PC
|
||||
"00:0F:4B", // Virtual Iron 4
|
||||
"00:16:3E", // Red Hat Xen, Oracle VM, XenSource, Novell Xen
|
||||
"08:00:27", // Sun xVM VirtualBox
|
||||
"7A:79", // Hamachi
|
||||
}
|
||||
|
||||
// Filters the possible physical interface address by comparing it to known popular VM Software addresses
|
||||
// and Teredo Tunneling Pseudo-Interface.
|
||||
//
|
||||
//lint:ignore U1000 use in net_windows.go
|
||||
func isPhysicalInterface(addr string) bool {
|
||||
for _, macPart := range macAddrPartsToFilter {
|
||||
if strings.HasPrefix(strings.ToLower(addr), strings.ToLower(macPart)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -9,17 +9,18 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func patchNetIfaceName(iface *net.Interface) error {
|
||||
func patchNetIfaceName(iface *net.Interface) (bool, error) {
|
||||
b, err := exec.Command("networksetup", "-listnetworkserviceorder").Output()
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
patched := false
|
||||
if name := networkServiceName(iface.Name, bytes.NewReader(b)); name != "" {
|
||||
patched = true
|
||||
iface.Name = name
|
||||
mainLog.Load().Debug().Str("network_service", name).Msg("found network service name for interface")
|
||||
}
|
||||
return nil
|
||||
return patched, nil
|
||||
}
|
||||
|
||||
func networkServiceName(ifaceName string, r io.Reader) string {
|
||||
@@ -49,6 +50,7 @@ func validInterface(iface *net.Interface, validIfacesMap map[string]struct{}) bo
|
||||
return ok
|
||||
}
|
||||
|
||||
// validInterfacesMap returns a set of all valid hardware ports.
|
||||
func validInterfacesMap() map[string]struct{} {
|
||||
b, err := exec.Command("networksetup", "-listallhardwareports").Output()
|
||||
if err != nil {
|
||||
|
||||
52
cmd/cli/net_linux.go
Normal file
52
cmd/cli/net_linux.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/net/netmon"
|
||||
)
|
||||
|
||||
func patchNetIfaceName(iface *net.Interface) (bool, error) { return true, nil }
|
||||
|
||||
// validInterface reports whether the *net.Interface is a valid one.
|
||||
// Only non-virtual interfaces are considered valid.
|
||||
func validInterface(iface *net.Interface, validIfacesMap map[string]struct{}) bool {
|
||||
_, ok := validIfacesMap[iface.Name]
|
||||
return ok
|
||||
}
|
||||
|
||||
// validInterfacesMap returns a set containing non virtual interfaces.
|
||||
func validInterfacesMap() map[string]struct{} {
|
||||
m := make(map[string]struct{})
|
||||
vis := virtualInterfaces()
|
||||
netmon.ForeachInterface(func(i netmon.Interface, prefixes []netip.Prefix) {
|
||||
if _, existed := vis[i.Name]; existed {
|
||||
return
|
||||
}
|
||||
m[i.Name] = struct{}{}
|
||||
})
|
||||
// Fallback to default route interface if found nothing.
|
||||
if len(m) == 0 {
|
||||
defaultRoute, err := netmon.DefaultRoute()
|
||||
if err != nil {
|
||||
return m
|
||||
}
|
||||
m[defaultRoute.InterfaceName] = struct{}{}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// virtualInterfaces returns a map of virtual interfaces on current machine.
|
||||
func virtualInterfaces() map[string]struct{} {
|
||||
s := make(map[string]struct{})
|
||||
entries, _ := os.ReadDir("/sys/devices/virtual/net")
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
s[strings.TrimSpace(entry.Name())] = struct{}{}
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -1,11 +1,22 @@
|
||||
//go:build !darwin && !windows
|
||||
//go:build !darwin && !windows && !linux
|
||||
|
||||
package cli
|
||||
|
||||
import "net"
|
||||
import (
|
||||
"net"
|
||||
|
||||
func patchNetIfaceName(iface *net.Interface) error { return nil }
|
||||
"tailscale.com/net/netmon"
|
||||
)
|
||||
|
||||
func patchNetIfaceName(iface *net.Interface) (bool, error) { return true, nil }
|
||||
|
||||
func validInterface(iface *net.Interface, validIfacesMap map[string]struct{}) bool { return true }
|
||||
|
||||
func validInterfacesMap() map[string]struct{} { return nil }
|
||||
// validInterfacesMap returns a set containing only default route interfaces.
|
||||
func validInterfacesMap() map[string]struct{} {
|
||||
defaultRoute, err := netmon.DefaultRoute()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return map[string]struct{}{defaultRoute.InterfaceName: {}}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,93 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/microsoft/wmi/pkg/base/host"
|
||||
"github.com/microsoft/wmi/pkg/base/instance"
|
||||
"github.com/microsoft/wmi/pkg/base/query"
|
||||
"github.com/microsoft/wmi/pkg/constant"
|
||||
"github.com/microsoft/wmi/pkg/hardware/network/netadapter"
|
||||
)
|
||||
|
||||
func patchNetIfaceName(iface *net.Interface) error {
|
||||
return nil
|
||||
func patchNetIfaceName(iface *net.Interface) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// validInterface reports whether the *net.Interface is a valid one.
|
||||
// On Windows, only physical interfaces are considered valid.
|
||||
func validInterface(iface *net.Interface, validIfacesMap map[string]struct{}) bool {
|
||||
if iface == nil {
|
||||
return false
|
||||
}
|
||||
if isPhysicalInterface(iface.HardwareAddr.String()) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
_, ok := validIfacesMap[iface.Name]
|
||||
return ok
|
||||
}
|
||||
|
||||
func validInterfacesMap() map[string]struct{} { return nil }
|
||||
// validInterfacesMap returns a set of all physical interfaces.
|
||||
func validInterfacesMap() map[string]struct{} {
|
||||
m := make(map[string]struct{})
|
||||
for _, ifaceName := range validInterfaces() {
|
||||
m[ifaceName] = struct{}{}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// validInterfaces returns a list of all physical interfaces.
|
||||
func validInterfaces() []string {
|
||||
log.SetOutput(io.Discard)
|
||||
defer log.SetOutput(os.Stderr)
|
||||
whost := host.NewWmiLocalHost()
|
||||
q := query.NewWmiQuery("MSFT_NetAdapter")
|
||||
instances, err := instance.GetWmiInstancesFromHost(whost, string(constant.StadardCimV2), q)
|
||||
if instances != nil {
|
||||
defer instances.Close()
|
||||
}
|
||||
if err != nil {
|
||||
mainLog.Load().Warn().Err(err).Msg("failed to get wmi network adapter")
|
||||
return nil
|
||||
}
|
||||
var adapters []string
|
||||
for _, i := range instances {
|
||||
adapter, err := netadapter.NewNetworkAdapter(i)
|
||||
if err != nil {
|
||||
mainLog.Load().Warn().Err(err).Msg("failed to get network adapter")
|
||||
continue
|
||||
}
|
||||
|
||||
name, err := adapter.GetPropertyName()
|
||||
if err != nil {
|
||||
mainLog.Load().Warn().Err(err).Msg("failed to get interface name")
|
||||
continue
|
||||
}
|
||||
|
||||
// From: https://learn.microsoft.com/en-us/previous-versions/windows/desktop/legacy/hh968170(v=vs.85)
|
||||
//
|
||||
// "Indicates if a connector is present on the network adapter. This value is set to TRUE
|
||||
// if this is a physical adapter or FALSE if this is not a physical adapter."
|
||||
physical, err := adapter.GetPropertyConnectorPresent()
|
||||
if err != nil {
|
||||
mainLog.Load().Debug().Str("method", "validInterfaces").Str("interface", name).Msg("failed to get network adapter connector present property")
|
||||
continue
|
||||
}
|
||||
if !physical {
|
||||
mainLog.Load().Debug().Str("method", "validInterfaces").Str("interface", name).Msg("skipping non-physical adapter")
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if it's a hardware interface. Checking only for connector present is not enough
|
||||
// because some interfaces are not physical but have a connector.
|
||||
hardware, err := adapter.GetPropertyHardwareInterface()
|
||||
if err != nil {
|
||||
mainLog.Load().Debug().Str("method", "validInterfaces").Str("interface", name).Msg("failed to get network adapter hardware interface property")
|
||||
continue
|
||||
}
|
||||
if !hardware {
|
||||
mainLog.Load().Debug().Str("method", "validInterfaces").Str("interface", name).Msg("skipping non-hardware interface")
|
||||
continue
|
||||
}
|
||||
|
||||
adapters = append(adapters, name)
|
||||
}
|
||||
return adapters
|
||||
}
|
||||
|
||||
42
cmd/cli/net_windows_test.go
Normal file
42
cmd/cli/net_windows_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Test_validInterfaces(t *testing.T) {
|
||||
verbose = 3
|
||||
initConsoleLogging()
|
||||
start := time.Now()
|
||||
ifaces := validInterfaces()
|
||||
t.Logf("Using Windows API takes: %d", time.Since(start).Milliseconds())
|
||||
|
||||
start = time.Now()
|
||||
ifacesPowershell := validInterfacesPowershell()
|
||||
t.Logf("Using Powershell takes: %d", time.Since(start).Milliseconds())
|
||||
|
||||
slices.Sort(ifaces)
|
||||
slices.Sort(ifacesPowershell)
|
||||
if !slices.Equal(ifaces, ifacesPowershell) {
|
||||
t.Fatalf("result mismatch, want: %v, got: %v", ifacesPowershell, ifaces)
|
||||
}
|
||||
}
|
||||
|
||||
func validInterfacesPowershell() []string {
|
||||
out, err := powershell("Get-NetAdapter -Physical | Select-Object -ExpandProperty Name")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var res []string
|
||||
scanner := bufio.NewScanner(bytes.NewReader(out))
|
||||
for scanner.Scan() {
|
||||
ifaceName := strings.TrimSpace(scanner.Text())
|
||||
res = append(res, ifaceName)
|
||||
}
|
||||
return res
|
||||
}
|
||||
@@ -70,11 +70,6 @@ func resetDnsIgnoreUnusableInterface(iface *net.Interface) error {
|
||||
|
||||
// TODO(cuonglm): use system API
|
||||
func resetDNS(iface *net.Interface) error {
|
||||
if ns := savedStaticNameservers(iface); len(ns) > 0 {
|
||||
if err := setDNS(iface, ns); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
cmd := "networksetup"
|
||||
args := []string{"-setdnsservers", iface.Name, "empty"}
|
||||
if out, err := exec.Command(cmd, args...).CombinedOutput(); err != nil {
|
||||
@@ -83,6 +78,15 @@ func resetDNS(iface *net.Interface) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// restoreDNS restores the DNS settings of the given interface.
|
||||
// this should only be executed upon turning off the ctrld service.
|
||||
func restoreDNS(iface *net.Interface) (err error) {
|
||||
if ns := savedStaticNameservers(iface); len(ns) > 0 {
|
||||
err = setDNS(iface, ns)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func currentDNS(_ *net.Interface) []string {
|
||||
return resolvconffile.NameServers("")
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ import (
|
||||
"net/netip"
|
||||
"os/exec"
|
||||
|
||||
"tailscale.com/control/controlknobs"
|
||||
"tailscale.com/health"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld/internal/dns"
|
||||
"github.com/Control-D-Inc/ctrld/internal/resolvconffile"
|
||||
)
|
||||
@@ -36,7 +39,7 @@ func setDnsIgnoreUnusableInterface(iface *net.Interface, nameservers []string) e
|
||||
|
||||
// set the dns server for the provided network interface
|
||||
func setDNS(iface *net.Interface, nameservers []string) error {
|
||||
r, err := dns.NewOSConfigurator(logf, iface.Name)
|
||||
r, err := dns.NewOSConfigurator(logf, &health.Tracker{}, &controlknobs.Knobs{}, iface.Name)
|
||||
if err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("failed to create DNS OS configurator")
|
||||
return err
|
||||
@@ -60,7 +63,7 @@ func resetDnsIgnoreUnusableInterface(iface *net.Interface) error {
|
||||
}
|
||||
|
||||
func resetDNS(iface *net.Interface) error {
|
||||
r, err := dns.NewOSConfigurator(logf, iface.Name)
|
||||
r, err := dns.NewOSConfigurator(logf, &health.Tracker{}, &controlknobs.Knobs{}, iface.Name)
|
||||
if err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("failed to create DNS OS configurator")
|
||||
return err
|
||||
@@ -73,6 +76,12 @@ func resetDNS(iface *net.Interface) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// restoreDNS restores the DNS settings of the given interface.
|
||||
// this should only be executed upon turning off the ctrld service.
|
||||
func restoreDNS(iface *net.Interface) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
func currentDNS(_ *net.Interface) []string {
|
||||
return resolvconffile.NameServers("")
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ import (
|
||||
"github.com/insomniacslk/dhcp/dhcpv4/nclient4"
|
||||
"github.com/insomniacslk/dhcp/dhcpv6"
|
||||
"github.com/insomniacslk/dhcp/dhcpv6/client6"
|
||||
"tailscale.com/control/controlknobs"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/util/dnsname"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld/internal/dns"
|
||||
@@ -54,7 +56,7 @@ func setDnsIgnoreUnusableInterface(iface *net.Interface, nameservers []string) e
|
||||
}
|
||||
|
||||
func setDNS(iface *net.Interface, nameservers []string) error {
|
||||
r, err := dns.NewOSConfigurator(logf, iface.Name)
|
||||
r, err := dns.NewOSConfigurator(logf, &health.Tracker{}, &controlknobs.Knobs{}, iface.Name)
|
||||
if err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("failed to create DNS OS configurator")
|
||||
return err
|
||||
@@ -136,7 +138,7 @@ func resetDNS(iface *net.Interface) (err error) {
|
||||
if exe, _ := exec.LookPath("/lib/systemd/systemd-networkd"); exe != "" {
|
||||
_ = exec.Command("systemctl", "start", "systemd-networkd").Run()
|
||||
}
|
||||
if r, oerr := dns.NewOSConfigurator(logf, iface.Name); oerr == nil {
|
||||
if r, oerr := dns.NewOSConfigurator(logf, &health.Tracker{}, &controlknobs.Knobs{}, iface.Name); oerr == nil {
|
||||
_ = r.SetDNS(dns.OSConfig{})
|
||||
if err := r.Close(); err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("failed to rollback DNS setting")
|
||||
@@ -193,6 +195,12 @@ func resetDNS(iface *net.Interface) (err error) {
|
||||
})
|
||||
}
|
||||
|
||||
// restoreDNS restores the DNS settings of the given interface.
|
||||
// this should only be executed upon turning off the ctrld service.
|
||||
func restoreDNS(iface *net.Interface) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
func currentDNS(iface *net.Interface) []string {
|
||||
for _, fn := range []getDNS{getDNSByResolvectl, getDNSBySystemdResolved, getDNSByNmcli, resolvconffile.NameServers} {
|
||||
if ns := fn(iface.Name); len(ns) > 0 {
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/registry"
|
||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||
|
||||
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
||||
)
|
||||
|
||||
const (
|
||||
v4InterfaceKeyPathFormat = `HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces\`
|
||||
v6InterfaceKeyPathFormat = `HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters\Interfaces\`
|
||||
v4InterfaceKeyPathFormat = `SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces\`
|
||||
v6InterfaceKeyPathFormat = `SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters\Interfaces\`
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -38,25 +42,55 @@ func setDNS(iface *net.Interface, nameservers []string) error {
|
||||
setDNSOnce.Do(func() {
|
||||
// If there's a Dns server running, that means we are on AD with Dns feature enabled.
|
||||
// Configuring the Dns server to forward queries to ctrld instead.
|
||||
if windowsHasLocalDnsServerRunning() {
|
||||
if hasLocalDnsServerRunning() {
|
||||
file := absHomeDir(windowsForwardersFilename)
|
||||
oldForwardersContent, _ := os.ReadFile(file)
|
||||
if err := os.WriteFile(file, []byte(strings.Join(nameservers, ",")), 0600); err != nil {
|
||||
hasLocalIPv6Listener := needLocalIPv6Listener()
|
||||
forwarders := slices.DeleteFunc(slices.Clone(nameservers), func(s string) bool {
|
||||
if !hasLocalIPv6Listener {
|
||||
return false
|
||||
}
|
||||
return s == "::1"
|
||||
})
|
||||
if err := os.WriteFile(file, []byte(strings.Join(forwarders, ",")), 0600); err != nil {
|
||||
mainLog.Load().Warn().Err(err).Msg("could not save forwarders settings")
|
||||
}
|
||||
oldForwarders := strings.Split(string(oldForwardersContent), ",")
|
||||
if err := addDnsServerForwarders(nameservers, oldForwarders); err != nil {
|
||||
if err := addDnsServerForwarders(forwarders, oldForwarders); err != nil {
|
||||
mainLog.Load().Warn().Err(err).Msg("could not set forwarders settings")
|
||||
}
|
||||
}
|
||||
})
|
||||
primaryDNS := nameservers[0]
|
||||
if err := setPrimaryDNS(iface, primaryDNS, true); err != nil {
|
||||
return err
|
||||
luid, err := winipcfg.LUIDFromIndex(uint32(iface.Index))
|
||||
if err != nil {
|
||||
return fmt.Errorf("setDNS: %w", err)
|
||||
}
|
||||
if len(nameservers) > 1 {
|
||||
secondaryDNS := nameservers[1]
|
||||
_ = addSecondaryDNS(iface, secondaryDNS)
|
||||
var (
|
||||
serversV4 []netip.Addr
|
||||
serversV6 []netip.Addr
|
||||
)
|
||||
for _, ns := range nameservers {
|
||||
if addr, err := netip.ParseAddr(ns); err == nil {
|
||||
if addr.Is4() {
|
||||
serversV4 = append(serversV4, addr)
|
||||
} else {
|
||||
serversV6 = append(serversV6, addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(serversV4) == 0 && len(serversV6) == 0 {
|
||||
return errors.New("invalid DNS nameservers")
|
||||
}
|
||||
if len(serversV4) > 0 {
|
||||
if err := luid.SetDNS(windows.AF_INET, serversV4, nil); err != nil {
|
||||
return fmt.Errorf("could not set DNS ipv4: %w", err)
|
||||
}
|
||||
}
|
||||
if len(serversV6) > 0 {
|
||||
if err := luid.SetDNS(windows.AF_INET6, serversV6, nil); err != nil {
|
||||
return fmt.Errorf("could not set DNS ipv6: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -70,7 +104,7 @@ func resetDnsIgnoreUnusableInterface(iface *net.Interface) error {
|
||||
func resetDNS(iface *net.Interface) error {
|
||||
resetDNSOnce.Do(func() {
|
||||
// See corresponding comment in setDNS.
|
||||
if windowsHasLocalDnsServerRunning() {
|
||||
if hasLocalDnsServerRunning() {
|
||||
file := absHomeDir(windowsForwardersFilename)
|
||||
content, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
@@ -85,18 +119,23 @@ func resetDNS(iface *net.Interface) error {
|
||||
}
|
||||
})
|
||||
|
||||
// Restoring ipv6 first.
|
||||
if ctrldnet.SupportsIPv6ListenLocal() {
|
||||
if output, err := netsh("interface", "ipv6", "set", "dnsserver", strconv.Itoa(iface.Index), "dhcp"); err != nil {
|
||||
mainLog.Load().Warn().Err(err).Msgf("failed to reset ipv6 DNS: %s", string(output))
|
||||
}
|
||||
}
|
||||
// Restoring ipv4 DHCP.
|
||||
output, err := netsh("interface", "ipv4", "set", "dnsserver", strconv.Itoa(iface.Index), "dhcp")
|
||||
luid, err := winipcfg.LUIDFromIndex(uint32(iface.Index))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", string(output), err)
|
||||
return fmt.Errorf("resetDNS: %w", err)
|
||||
}
|
||||
// If there's static DNS saved, restoring it.
|
||||
// Restoring DHCP settings.
|
||||
if err := luid.SetDNS(windows.AF_INET, nil, nil); err != nil {
|
||||
return fmt.Errorf("could not reset DNS ipv4: %w", err)
|
||||
}
|
||||
if err := luid.SetDNS(windows.AF_INET6, nil, nil); err != nil {
|
||||
return fmt.Errorf("could not reset DNS ipv6: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// restoreDNS restores the DNS settings of the given interface.
|
||||
// this should only be executed upon turning off the ctrld service.
|
||||
func restoreDNS(iface *net.Interface) (err error) {
|
||||
if nss := savedStaticNameservers(iface); len(nss) > 0 {
|
||||
v4ns := make([]string, 0, 2)
|
||||
v6ns := make([]string, 0, 2)
|
||||
@@ -112,52 +151,15 @@ func resetDNS(iface *net.Interface) error {
|
||||
if len(ns) == 0 {
|
||||
continue
|
||||
}
|
||||
primaryDNS := ns[0]
|
||||
if err := setPrimaryDNS(iface, primaryDNS, false); err != nil {
|
||||
mainLog.Load().Debug().Msgf("setting static DNS for interface %q", iface.Name)
|
||||
err = setDNS(iface, ns)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(ns) > 1 {
|
||||
secondaryDNS := ns[1]
|
||||
_ = addSecondaryDNS(iface, secondaryDNS)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setPrimaryDNS(iface *net.Interface, dns string, disablev6 bool) error {
|
||||
ipVer := "ipv4"
|
||||
if ctrldnet.IsIPv6(dns) {
|
||||
ipVer = "ipv6"
|
||||
}
|
||||
idx := strconv.Itoa(iface.Index)
|
||||
output, err := netsh("interface", ipVer, "set", "dnsserver", idx, "static", dns)
|
||||
if err != nil {
|
||||
mainLog.Load().Error().Err(err).Msgf("failed to set primary DNS: %s", string(output))
|
||||
return err
|
||||
}
|
||||
if disablev6 && ipVer == "ipv4" && ctrldnet.SupportsIPv6ListenLocal() {
|
||||
// Disable IPv6 DNS, so the query will be fallback to IPv4.
|
||||
_, _ = netsh("interface", "ipv6", "set", "dnsserver", idx, "static", "::1", "primary")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func addSecondaryDNS(iface *net.Interface, dns string) error {
|
||||
ipVer := "ipv4"
|
||||
if ctrldnet.IsIPv6(dns) {
|
||||
ipVer = "ipv6"
|
||||
}
|
||||
output, err := netsh("interface", ipVer, "add", "dns", strconv.Itoa(iface.Index), dns, "index=2")
|
||||
if err != nil {
|
||||
mainLog.Load().Warn().Err(err).Msgf("failed to add secondary DNS: %s", string(output))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func netsh(args ...string) ([]byte, error) {
|
||||
return exec.Command("netsh", args...).Output()
|
||||
return err
|
||||
}
|
||||
|
||||
func currentDNS(iface *net.Interface) []string {
|
||||
@@ -182,25 +184,33 @@ func currentDNS(iface *net.Interface) []string {
|
||||
func currentStaticDNS(iface *net.Interface) ([]string, error) {
|
||||
luid, err := winipcfg.LUIDFromIndex(uint32(iface.Index))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("winipcfg.LUIDFromIndex: %w", err)
|
||||
}
|
||||
guid, err := luid.GUID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("luid.GUID: %w", err)
|
||||
}
|
||||
var ns []string
|
||||
for _, path := range []string{v4InterfaceKeyPathFormat, v6InterfaceKeyPathFormat} {
|
||||
interfaceKeyPath := path + guid.String()
|
||||
found := false
|
||||
interfaceKeyPath := path + guid.String()
|
||||
k, err := registry.OpenKey(registry.LOCAL_MACHINE, interfaceKeyPath, registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", interfaceKeyPath, err)
|
||||
}
|
||||
for _, key := range []string{"NameServer", "ProfileNameServer"} {
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
cmd := fmt.Sprintf(`Get-ItemPropertyValue -Path "%s" -Name "%s"`, interfaceKeyPath, key)
|
||||
out, err := powershell(cmd)
|
||||
if err == nil && len(out) > 0 {
|
||||
value, _, err := k.GetStringValue(key)
|
||||
if err != nil && !errors.Is(err, registry.ErrNotExist) {
|
||||
return nil, fmt.Errorf("%s: %w", key, err)
|
||||
}
|
||||
if len(value) > 0 {
|
||||
found = true
|
||||
ns = append(ns, strings.Split(string(out), ",")...)
|
||||
for _, e := range strings.Split(value, ",") {
|
||||
ns = append(ns, strings.TrimRight(e, "\x00"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -246,3 +256,9 @@ func removeDnsServerForwarders(nameservers []string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// powershell runs the given powershell command.
|
||||
func powershell(cmd string) ([]byte, error) {
|
||||
out, err := exec.Command("powershell", "-Command", cmd).CombinedOutput()
|
||||
return bytes.TrimSpace(out), err
|
||||
}
|
||||
|
||||
68
cmd/cli/os_windows_test.go
Normal file
68
cmd/cli/os_windows_test.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||
)
|
||||
|
||||
func Test_currentStaticDNS(t *testing.T) {
|
||||
iface, err := net.InterfaceByName(defaultIfaceName())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
start := time.Now()
|
||||
staticDns, err := currentStaticDNS(iface)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("Using Windows API takes: %d", time.Since(start).Milliseconds())
|
||||
|
||||
start = time.Now()
|
||||
staticDnsPowershell, err := currentStaticDnsPowershell(iface)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("Using Powershell takes: %d", time.Since(start).Milliseconds())
|
||||
|
||||
slices.Sort(staticDns)
|
||||
slices.Sort(staticDnsPowershell)
|
||||
if !slices.Equal(staticDns, staticDnsPowershell) {
|
||||
t.Fatalf("result mismatch, want: %v, got: %v", staticDnsPowershell, staticDns)
|
||||
}
|
||||
}
|
||||
|
||||
func currentStaticDnsPowershell(iface *net.Interface) ([]string, error) {
|
||||
luid, err := winipcfg.LUIDFromIndex(uint32(iface.Index))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
guid, err := luid.GUID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var ns []string
|
||||
for _, path := range []string{"HKLM:\\" + v4InterfaceKeyPathFormat, "HKLM:\\" + v6InterfaceKeyPathFormat} {
|
||||
interfaceKeyPath := path + guid.String()
|
||||
found := false
|
||||
for _, key := range []string{"NameServer", "ProfileNameServer"} {
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
cmd := fmt.Sprintf(`Get-ItemPropertyValue -Path "%s" -Name "%s"`, interfaceKeyPath, key)
|
||||
out, err := powershell(cmd)
|
||||
if err == nil && len(out) > 0 {
|
||||
found = true
|
||||
for _, e := range strings.Split(string(out), ",") {
|
||||
ns = append(ns, strings.TrimRight(e, "\x00"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ns, nil
|
||||
}
|
||||
594
cmd/cli/prog.go
594
cmd/cli/prog.go
@@ -24,7 +24,8 @@ import (
|
||||
"github.com/kardianos/service"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/spf13/viper"
|
||||
"tailscale.com/net/interfaces"
|
||||
"golang.org/x/sync/singleflight"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/tsaddr"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld"
|
||||
@@ -44,6 +45,18 @@ const (
|
||||
upstreamOS = upstreamPrefix + "os"
|
||||
upstreamPrivate = upstreamPrefix + "private"
|
||||
dnsWatchdogDefaultInterval = 20 * time.Second
|
||||
ctrldServiceName = "ctrld"
|
||||
)
|
||||
|
||||
// RecoveryReason provides context for why we are waiting for recovery.
|
||||
// recovery involves removing the listener IP from the interface and
|
||||
// waiting for the upstreams to work before returning
|
||||
type RecoveryReason int
|
||||
|
||||
const (
|
||||
RecoveryReasonNetworkChange RecoveryReason = iota
|
||||
RecoveryReasonRegularFailure
|
||||
RecoveryReasonOSFailure
|
||||
)
|
||||
|
||||
// ControlSocketName returns name for control unix socket.
|
||||
@@ -60,41 +73,53 @@ var logf = func(format string, args ...any) {
|
||||
}
|
||||
|
||||
var svcConfig = &service.Config{
|
||||
Name: "ctrld",
|
||||
Name: ctrldServiceName,
|
||||
DisplayName: "Control-D Helper Service",
|
||||
Description: "A highly configurable, multi-protocol DNS forwarding proxy",
|
||||
Option: service.KeyValue{},
|
||||
}
|
||||
|
||||
var useSystemdResolved = false
|
||||
|
||||
type prog struct {
|
||||
mu sync.Mutex
|
||||
waitCh chan struct{}
|
||||
stopCh chan struct{}
|
||||
reloadCh chan struct{} // For Windows.
|
||||
reloadDoneCh chan struct{}
|
||||
apiReloadCh chan *ctrld.Config
|
||||
logConn net.Conn
|
||||
cs *controlServer
|
||||
csSetDnsDone chan struct{}
|
||||
csSetDnsOk bool
|
||||
dnsWatchDogOnce sync.Once
|
||||
dnsWg sync.WaitGroup
|
||||
dnsWatcherStopCh chan struct{}
|
||||
mu sync.Mutex
|
||||
waitCh chan struct{}
|
||||
stopCh chan struct{}
|
||||
reloadCh chan struct{} // For Windows.
|
||||
reloadDoneCh chan struct{}
|
||||
apiReloadCh chan *ctrld.Config
|
||||
apiForceReloadCh chan struct{}
|
||||
apiForceReloadGroup singleflight.Group
|
||||
logConn net.Conn
|
||||
cs *controlServer
|
||||
csSetDnsDone chan struct{}
|
||||
csSetDnsOk bool
|
||||
dnsWg sync.WaitGroup
|
||||
dnsWatcherClosedOnce sync.Once
|
||||
dnsWatcherStopCh chan struct{}
|
||||
rc *controld.ResolverConfig
|
||||
|
||||
cfg *ctrld.Config
|
||||
localUpstreams []string
|
||||
ptrNameservers []string
|
||||
appCallback *AppCallback
|
||||
cache dnscache.Cacher
|
||||
cacheFlushDomainsMap map[string]struct{}
|
||||
sema semaphore
|
||||
ciTable *clientinfo.Table
|
||||
um *upstreamMonitor
|
||||
router router.Router
|
||||
ptrLoopGuard *loopGuard
|
||||
lanLoopGuard *loopGuard
|
||||
metricsQueryStats atomic.Bool
|
||||
cfg *ctrld.Config
|
||||
localUpstreams []string
|
||||
ptrNameservers []string
|
||||
appCallback *AppCallback
|
||||
cache dnscache.Cacher
|
||||
cacheFlushDomainsMap map[string]struct{}
|
||||
sema semaphore
|
||||
ciTable *clientinfo.Table
|
||||
um *upstreamMonitor
|
||||
router router.Router
|
||||
ptrLoopGuard *loopGuard
|
||||
lanLoopGuard *loopGuard
|
||||
metricsQueryStats atomic.Bool
|
||||
queryFromSelfMap sync.Map
|
||||
initInternalLogWriterOnce sync.Once
|
||||
internalLogWriter *logWriter
|
||||
internalWarnLogWriter *logWriter
|
||||
internalLogSent time.Time
|
||||
runningIface string
|
||||
requiredMultiNICsConfig bool
|
||||
adDomain string
|
||||
|
||||
selfUninstallMu sync.Mutex
|
||||
refusedQueryCount int
|
||||
@@ -104,6 +129,10 @@ type prog struct {
|
||||
loopMu sync.Mutex
|
||||
loop map[string]bool
|
||||
|
||||
recoveryCancelMu sync.Mutex
|
||||
recoveryCancel context.CancelFunc
|
||||
recoveryRunning atomic.Bool
|
||||
|
||||
started chan struct{}
|
||||
onStartedDone chan struct{}
|
||||
onStarted []func()
|
||||
@@ -154,11 +183,13 @@ func (p *prog) runWait() {
|
||||
|
||||
if newCfg == nil {
|
||||
newCfg = &ctrld.Config{}
|
||||
confFile := v.ConfigFileUsed()
|
||||
v := viper.NewWithOptions(viper.KeyDelimiter("::"))
|
||||
ctrld.InitConfig(v, "ctrld")
|
||||
if configPath != "" {
|
||||
v.SetConfigFile(configPath)
|
||||
confFile = configPath
|
||||
}
|
||||
v.SetConfigFile(confFile)
|
||||
if err := v.ReadInConfig(); err != nil {
|
||||
logger.Err(err).Msg("could not read new config")
|
||||
waitOldRunDone()
|
||||
@@ -170,10 +201,14 @@ func (p *prog) runWait() {
|
||||
continue
|
||||
}
|
||||
if cdUID != "" {
|
||||
if err := processCDFlags(newCfg); err != nil {
|
||||
if rc, err := processCDFlags(newCfg); err != nil {
|
||||
logger.Err(err).Msg("could not fetch ControlD config")
|
||||
waitOldRunDone()
|
||||
continue
|
||||
} else {
|
||||
p.mu.Lock()
|
||||
p.rc = rc
|
||||
p.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,6 +236,7 @@ func (p *prog) runWait() {
|
||||
continue
|
||||
}
|
||||
|
||||
addExtraSplitDnsRule(newCfg)
|
||||
if err := writeConfigFile(newCfg); err != nil {
|
||||
logger.Err(err).Msg("could not write new config")
|
||||
}
|
||||
@@ -224,6 +260,11 @@ func (p *prog) runWait() {
|
||||
}
|
||||
|
||||
func (p *prog) preRun() {
|
||||
if iface == "auto" {
|
||||
iface = defaultIfaceName()
|
||||
p.requiredMultiNICsConfig = requiredMultiNICsConfig()
|
||||
}
|
||||
p.runningIface = iface
|
||||
if runtime.GOOS == "darwin" {
|
||||
p.onStopped = append(p.onStopped, func() {
|
||||
if !service.Interactive() {
|
||||
@@ -236,9 +277,12 @@ func (p *prog) preRun() {
|
||||
func (p *prog) postRun() {
|
||||
if !service.Interactive() {
|
||||
p.resetDNS()
|
||||
ns := ctrld.InitializeOsResolver()
|
||||
ns := ctrld.InitializeOsResolver(false)
|
||||
mainLog.Load().Debug().Msgf("initialized OS resolver with nameservers: %v", ns)
|
||||
p.setDNS()
|
||||
p.csSetDnsDone <- struct{}{}
|
||||
close(p.csSetDnsDone)
|
||||
p.logInterfacesState()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,47 +292,79 @@ func (p *prog) apiConfigReload() {
|
||||
return
|
||||
}
|
||||
|
||||
secs := 3600
|
||||
if p.cfg.Service.RefetchTime != nil && *p.cfg.Service.RefetchTime > 0 {
|
||||
secs = *p.cfg.Service.RefetchTime
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(time.Duration(secs) * time.Second)
|
||||
ticker := time.NewTicker(timeDurationOrDefault(p.cfg.Service.RefetchTime, 3600) * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
logger := mainLog.Load().With().Str("mode", "api-reload").Logger()
|
||||
logger.Debug().Msg("starting custom config reload timer")
|
||||
lastUpdated := time.Now().Unix()
|
||||
|
||||
doReloadApiConfig := func(forced bool, logger zerolog.Logger) {
|
||||
resolverConfig, err := controld.FetchResolverConfig(cdUID, rootCmd.Version, cdDev)
|
||||
selfUninstallCheck(err, p, logger)
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("could not fetch resolver config")
|
||||
return
|
||||
}
|
||||
|
||||
if resolverConfig.DeactivationPin != nil {
|
||||
newDeactivationPin := *resolverConfig.DeactivationPin
|
||||
curDeactivationPin := cdDeactivationPin.Load()
|
||||
switch {
|
||||
case curDeactivationPin != defaultDeactivationPin:
|
||||
logger.Debug().Msg("saving deactivation pin")
|
||||
case curDeactivationPin != newDeactivationPin:
|
||||
logger.Debug().Msg("update deactivation pin")
|
||||
}
|
||||
cdDeactivationPin.Store(newDeactivationPin)
|
||||
} else {
|
||||
cdDeactivationPin.Store(defaultDeactivationPin)
|
||||
}
|
||||
|
||||
p.mu.Lock()
|
||||
rc := p.rc
|
||||
p.rc = resolverConfig
|
||||
p.mu.Unlock()
|
||||
noCustomConfig := resolverConfig.Ctrld.CustomConfig == ""
|
||||
noExcludeListChanged := true
|
||||
if rc != nil {
|
||||
slices.Sort(rc.Exclude)
|
||||
slices.Sort(resolverConfig.Exclude)
|
||||
noExcludeListChanged = slices.Equal(rc.Exclude, resolverConfig.Exclude)
|
||||
}
|
||||
if noCustomConfig && noExcludeListChanged {
|
||||
return
|
||||
}
|
||||
|
||||
if noCustomConfig && !noExcludeListChanged {
|
||||
logger.Debug().Msg("exclude list changes detected, reloading...")
|
||||
p.apiReloadCh <- nil
|
||||
return
|
||||
}
|
||||
|
||||
if resolverConfig.Ctrld.CustomLastUpdate > lastUpdated || forced {
|
||||
lastUpdated = time.Now().Unix()
|
||||
cfg := &ctrld.Config{}
|
||||
if err := validateCdRemoteConfig(resolverConfig, cfg); err != nil {
|
||||
logger.Warn().Err(err).Msg("skipping invalid custom config")
|
||||
if _, err := controld.UpdateCustomLastFailed(cdUID, rootCmd.Version, cdDev, true); err != nil {
|
||||
logger.Error().Err(err).Msg("could not mark custom last update failed")
|
||||
}
|
||||
return
|
||||
}
|
||||
setListenerDefaultValue(cfg)
|
||||
logger.Debug().Msg("custom config changes detected, reloading...")
|
||||
p.apiReloadCh <- cfg
|
||||
} else {
|
||||
logger.Debug().Msg("custom config does not change")
|
||||
}
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-p.apiForceReloadCh:
|
||||
doReloadApiConfig(true, logger.With().Bool("forced", true).Logger())
|
||||
case <-ticker.C:
|
||||
resolverConfig, err := controld.FetchResolverConfig(cdUID, rootCmd.Version, cdDev)
|
||||
selfUninstallCheck(err, p, logger)
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("could not fetch resolver config")
|
||||
continue
|
||||
}
|
||||
|
||||
if resolverConfig.Ctrld.CustomConfig == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if resolverConfig.Ctrld.CustomLastUpdate > lastUpdated {
|
||||
lastUpdated = time.Now().Unix()
|
||||
cfg := &ctrld.Config{}
|
||||
if err := validateCdRemoteConfig(resolverConfig, cfg); err != nil {
|
||||
logger.Warn().Err(err).Msg("skipping invalid custom config")
|
||||
if _, err := controld.UpdateCustomLastFailed(cdUID, rootCmd.Version, cdDev, true); err != nil {
|
||||
logger.Error().Err(err).Msg("could not mark custom last update failed")
|
||||
}
|
||||
break
|
||||
}
|
||||
setListenerDefaultValue(cfg)
|
||||
logger.Debug().Msg("custom config changes detected, reloading...")
|
||||
p.apiReloadCh <- cfg
|
||||
} else {
|
||||
logger.Debug().Msg("custom config does not change")
|
||||
}
|
||||
doReloadApiConfig(false, logger)
|
||||
case <-p.stopCh:
|
||||
return
|
||||
}
|
||||
@@ -301,7 +377,11 @@ func (p *prog) setupUpstream(cfg *ctrld.Config) {
|
||||
isControlDUpstream := false
|
||||
for n := range cfg.Upstream {
|
||||
uc := cfg.Upstream[n]
|
||||
sdns := uc.Type == ctrld.ResolverTypeSDNS
|
||||
uc.Init()
|
||||
if sdns {
|
||||
mainLog.Load().Debug().Msgf("initialized DNS Stamps with endpoint: %s, type: %s", uc.Endpoint, uc.Type)
|
||||
}
|
||||
isControlDUpstream = isControlDUpstream || uc.IsControlD()
|
||||
if uc.BootstrapIP == "" {
|
||||
uc.SetupBootstrapIP()
|
||||
@@ -371,6 +451,10 @@ func (p *prog) run(reload bool, reloadCh chan struct{}) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if domain, err := getActiveDirectoryDomain(); err == nil && domain != "" && hasLocalDnsServerRunning() {
|
||||
mainLog.Load().Debug().Msgf("active directory domain: %s", domain)
|
||||
p.adDomain = domain
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(p.cfg.Listener))
|
||||
@@ -399,12 +483,7 @@ func (p *prog) run(reload bool, reloadCh chan struct{}) {
|
||||
}
|
||||
}
|
||||
p.setupUpstream(p.cfg)
|
||||
p.ciTable = clientinfo.NewTable(&cfg, defaultRouteIP(), cdUID, p.ptrNameservers)
|
||||
if leaseFile := p.cfg.Service.DHCPLeaseFile; leaseFile != "" {
|
||||
mainLog.Load().Debug().Msgf("watching custom lease file: %s", leaseFile)
|
||||
format := ctrld.LeaseFileFormat(p.cfg.Service.DHCPLeaseFileFormat)
|
||||
p.ciTable.AddLeaseFile(leaseFile, format)
|
||||
}
|
||||
p.setupClientInfoDiscover(defaultRouteIP())
|
||||
}
|
||||
|
||||
// context for managing spawn goroutines.
|
||||
@@ -416,8 +495,7 @@ func (p *prog) run(reload bool, reloadCh chan struct{}) {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
p.ciTable.Init()
|
||||
p.ciTable.RefreshLoop(ctx)
|
||||
p.runClientInfoDiscover(ctx)
|
||||
}()
|
||||
go p.watchLinkState(ctx)
|
||||
}
|
||||
@@ -433,9 +511,10 @@ func (p *prog) run(reload bool, reloadCh chan struct{}) {
|
||||
}
|
||||
addr := net.JoinHostPort(listenerConfig.IP, strconv.Itoa(listenerConfig.Port))
|
||||
mainLog.Load().Info().Msgf("starting DNS server on listener.%s: %s", listenerNum, addr)
|
||||
if err := p.serveDNS(listenerNum); err != nil {
|
||||
if err := p.serveDNS(ctx, listenerNum); err != nil {
|
||||
mainLog.Load().Fatal().Err(err).Msgf("unable to start dns proxy on listener.%s", listenerNum)
|
||||
}
|
||||
mainLog.Load().Debug().Msgf("end of serveDNS listener.%s: %s", listenerNum, addr)
|
||||
}(listenerNum)
|
||||
}
|
||||
go func() {
|
||||
@@ -481,23 +560,44 @@ func (p *prog) run(reload bool, reloadCh chan struct{}) {
|
||||
if !reload {
|
||||
// Stop writing log to unix socket.
|
||||
consoleWriter.Out = os.Stdout
|
||||
initLoggingWithBackup(false)
|
||||
logWriters := initLoggingWithBackup(false)
|
||||
if p.logConn != nil {
|
||||
_ = p.logConn.Close()
|
||||
}
|
||||
go p.apiConfigReload()
|
||||
p.postRun()
|
||||
p.initInternalLogging(logWriters)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// setupClientInfoDiscover performs necessary works for running client info discover.
|
||||
func (p *prog) setupClientInfoDiscover(selfIP string) {
|
||||
p.ciTable = clientinfo.NewTable(&cfg, selfIP, cdUID, p.ptrNameservers)
|
||||
if leaseFile := p.cfg.Service.DHCPLeaseFile; leaseFile != "" {
|
||||
mainLog.Load().Debug().Msgf("watching custom lease file: %s", leaseFile)
|
||||
format := ctrld.LeaseFileFormat(p.cfg.Service.DHCPLeaseFileFormat)
|
||||
p.ciTable.AddLeaseFile(leaseFile, format)
|
||||
}
|
||||
}
|
||||
|
||||
// runClientInfoDiscover runs the client info discover.
|
||||
func (p *prog) runClientInfoDiscover(ctx context.Context) {
|
||||
p.ciTable.Init()
|
||||
p.ciTable.RefreshLoop(ctx)
|
||||
}
|
||||
|
||||
// metricsEnabled reports whether prometheus exporter is enabled/disabled.
|
||||
func (p *prog) metricsEnabled() bool {
|
||||
return p.cfg.Service.MetricsQueryStats || p.cfg.Service.MetricsListener != ""
|
||||
}
|
||||
|
||||
func (p *prog) Stop(s service.Service) error {
|
||||
mainLog.Load().Info().Msg("Service stopped")
|
||||
p.stopDnsWatchers()
|
||||
mainLog.Load().Debug().Msg("dns watchers stopped")
|
||||
defer func() {
|
||||
mainLog.Load().Info().Msg("Service stopped")
|
||||
}()
|
||||
close(p.stopCh)
|
||||
if err := p.deAllocateIP(); err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("de-allocate ip failed")
|
||||
@@ -506,6 +606,15 @@ func (p *prog) Stop(s service.Service) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *prog) stopDnsWatchers() {
|
||||
// Ensure all DNS watchers goroutine are terminated,
|
||||
// so it won't mess up with other DNS changes.
|
||||
p.dnsWatcherClosedOnce.Do(func() {
|
||||
close(p.dnsWatcherStopCh)
|
||||
})
|
||||
p.dnsWg.Wait()
|
||||
}
|
||||
|
||||
func (p *prog) allocateIP(ip string) error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
@@ -533,34 +642,47 @@ func (p *prog) setDNS() {
|
||||
setDnsOK := false
|
||||
defer func() {
|
||||
p.csSetDnsOk = setDnsOK
|
||||
p.csSetDnsDone <- struct{}{}
|
||||
close(p.csSetDnsDone)
|
||||
}()
|
||||
|
||||
if cfg.Listener == nil {
|
||||
return
|
||||
}
|
||||
if iface == "" {
|
||||
if p.runningIface == "" {
|
||||
return
|
||||
}
|
||||
runningIface := iface
|
||||
|
||||
// allIfaces tracks whether we should set DNS for all physical interfaces.
|
||||
allIfaces := false
|
||||
if runningIface == "auto" {
|
||||
runningIface = defaultIfaceName()
|
||||
// If runningIface is "auto", it means user does not specify "--iface" flag.
|
||||
// In this case, ctrld has to set DNS for all physical interfaces, so
|
||||
// thing will still work when user switch from one to the other.
|
||||
allIfaces = requiredMultiNICsConfig()
|
||||
}
|
||||
allIfaces := p.requiredMultiNICsConfig
|
||||
lc := cfg.FirstListener()
|
||||
if lc == nil {
|
||||
return
|
||||
}
|
||||
logger := mainLog.Load().With().Str("iface", runningIface).Logger()
|
||||
netIface, err := netInterface(runningIface)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not get interface")
|
||||
logger := mainLog.Load().With().Str("iface", p.runningIface).Logger()
|
||||
|
||||
const maxDNSRetryAttempts = 3
|
||||
const retryDelay = 1 * time.Second
|
||||
var netIface *net.Interface
|
||||
var err error
|
||||
for attempt := 1; attempt <= maxDNSRetryAttempts; attempt++ {
|
||||
netIface, err = netInterface(p.runningIface)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if attempt < maxDNSRetryAttempts {
|
||||
// Try to find a different working interface
|
||||
newIface := findWorkingInterface(p.runningIface)
|
||||
if newIface != p.runningIface {
|
||||
p.runningIface = newIface
|
||||
logger = mainLog.Load().With().Str("iface", p.runningIface).Logger()
|
||||
logger.Info().Msg("switched to new interface")
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Warn().Err(err).Int("attempt", attempt).Msg("could not get interface, retrying...")
|
||||
time.Sleep(retryDelay)
|
||||
continue
|
||||
}
|
||||
logger.Error().Err(err).Msg("could not get interface after all attempts")
|
||||
return
|
||||
}
|
||||
if err := setupNetworkManager(); err != nil {
|
||||
@@ -588,12 +710,21 @@ func (p *prog) setDNS() {
|
||||
if needRFC1918Listeners(lc) {
|
||||
nameservers = append(nameservers, ctrld.Rfc1918Addresses()...)
|
||||
}
|
||||
if needLocalIPv6Listener() {
|
||||
nameservers = append(nameservers, "::1")
|
||||
}
|
||||
slices.Sort(nameservers)
|
||||
if err := setDNS(netIface, nameservers); err != nil {
|
||||
logger.Error().Err(err).Msgf("could not set DNS for interface")
|
||||
return
|
||||
}
|
||||
setDnsOK = true
|
||||
logger.Debug().Msg("setting DNS successfully")
|
||||
if allIfaces {
|
||||
withEachPhysicalInterfaces(netIface.Name, "set DNS", func(i *net.Interface) error {
|
||||
return setDnsIgnoreUnusableInterface(i, nameservers)
|
||||
})
|
||||
}
|
||||
if shouldWatchResolvconf() {
|
||||
servers := make([]netip.Addr, len(nameservers))
|
||||
for i := range nameservers {
|
||||
@@ -605,11 +736,6 @@ func (p *prog) setDNS() {
|
||||
p.watchResolvConf(netIface, servers, setResolvConf)
|
||||
}()
|
||||
}
|
||||
if allIfaces {
|
||||
withEachPhysicalInterfaces(netIface.Name, "set DNS", func(i *net.Interface) error {
|
||||
return setDnsIgnoreUnusableInterface(i, nameservers)
|
||||
})
|
||||
}
|
||||
if p.dnsWatchdogEnabled() {
|
||||
p.dnsWg.Add(1)
|
||||
go func() {
|
||||
@@ -643,61 +769,60 @@ func (p *prog) dnsWatchdog(iface *net.Interface, nameservers []string, allIfaces
|
||||
if !requiredMultiNICsConfig() {
|
||||
return
|
||||
}
|
||||
logger := mainLog.Load().With().Str("iface", iface.Name).Logger()
|
||||
logger.Debug().Msg("start DNS settings watchdog")
|
||||
|
||||
p.dnsWatchDogOnce.Do(func() {
|
||||
mainLog.Load().Debug().Msg("start DNS settings watchdog")
|
||||
ns := nameservers
|
||||
slices.Sort(ns)
|
||||
ticker := time.NewTicker(p.dnsWatchdogDuration())
|
||||
logger := mainLog.Load().With().Str("iface", iface.Name).Logger()
|
||||
for {
|
||||
select {
|
||||
case <-p.dnsWatcherStopCh:
|
||||
ns := nameservers
|
||||
slices.Sort(ns)
|
||||
ticker := time.NewTicker(p.dnsWatchdogDuration())
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-p.dnsWatcherStopCh:
|
||||
return
|
||||
case <-p.stopCh:
|
||||
mainLog.Load().Debug().Msg("stop dns watchdog")
|
||||
return
|
||||
case <-ticker.C:
|
||||
if p.recoveryRunning.Load() {
|
||||
return
|
||||
case <-p.stopCh:
|
||||
mainLog.Load().Debug().Msg("stop dns watchdog")
|
||||
return
|
||||
case <-ticker.C:
|
||||
if dnsChanged(iface, ns) {
|
||||
logger.Debug().Msg("DNS settings were changed, re-applying settings")
|
||||
if err := setDNS(iface, ns); err != nil {
|
||||
mainLog.Load().Error().Err(err).Str("iface", iface.Name).Msgf("could not re-apply DNS settings")
|
||||
}
|
||||
}
|
||||
if allIfaces {
|
||||
withEachPhysicalInterfaces(iface.Name, "re-applying DNS", func(i *net.Interface) error {
|
||||
if dnsChanged(i, ns) {
|
||||
if err := setDnsIgnoreUnusableInterface(i, nameservers); err != nil {
|
||||
mainLog.Load().Error().Err(err).Str("iface", i.Name).Msgf("could not re-apply DNS settings")
|
||||
} else {
|
||||
mainLog.Load().Debug().Msgf("re-applying DNS for interface %q successfully", i.Name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
if dnsChanged(iface, ns) {
|
||||
logger.Debug().Msg("DNS settings were changed, re-applying settings")
|
||||
if err := setDNS(iface, ns); err != nil {
|
||||
mainLog.Load().Error().Err(err).Str("iface", iface.Name).Msgf("could not re-apply DNS settings")
|
||||
}
|
||||
}
|
||||
if allIfaces {
|
||||
withEachPhysicalInterfaces(iface.Name, "", func(i *net.Interface) error {
|
||||
if dnsChanged(i, ns) {
|
||||
if err := setDnsIgnoreUnusableInterface(i, nameservers); err != nil {
|
||||
mainLog.Load().Error().Err(err).Str("iface", i.Name).Msgf("could not re-apply DNS settings")
|
||||
} else {
|
||||
mainLog.Load().Debug().Msgf("re-applying DNS for interface %q successfully", i.Name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (p *prog) resetDNS() {
|
||||
if iface == "" {
|
||||
if p.runningIface == "" {
|
||||
mainLog.Load().Debug().Msg("no running interface, skipping resetDNS")
|
||||
return
|
||||
}
|
||||
runningIface := iface
|
||||
allIfaces := false
|
||||
if runningIface == "auto" {
|
||||
runningIface = defaultIfaceName()
|
||||
// See corresponding comments in (*prog).setDNS function.
|
||||
allIfaces = requiredMultiNICsConfig()
|
||||
}
|
||||
logger := mainLog.Load().With().Str("iface", runningIface).Logger()
|
||||
netIface, err := netInterface(runningIface)
|
||||
// See corresponding comments in (*prog).setDNS function.
|
||||
allIfaces := p.requiredMultiNICsConfig
|
||||
logger := mainLog.Load().With().Str("iface", p.runningIface).Logger()
|
||||
netIface, err := netInterface(p.runningIface)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not get interface")
|
||||
return
|
||||
}
|
||||
|
||||
if err := restoreNetworkManager(); err != nil {
|
||||
logger.Error().Err(err).Msg("could not restore NetworkManager")
|
||||
return
|
||||
@@ -713,6 +838,159 @@ func (p *prog) resetDNS() {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *prog) logInterfacesState() {
|
||||
withEachPhysicalInterfaces("", "", func(i *net.Interface) error {
|
||||
addrs, err := i.Addrs()
|
||||
if err != nil {
|
||||
mainLog.Load().Warn().Str("interface", i.Name).Err(err).Msg("failed to get addresses")
|
||||
}
|
||||
nss, err := currentStaticDNS(i)
|
||||
if err != nil {
|
||||
mainLog.Load().Warn().Str("interface", i.Name).Err(err).Msg("failed to get DNS")
|
||||
}
|
||||
if len(nss) == 0 {
|
||||
nss = currentDNS(i)
|
||||
}
|
||||
mainLog.Load().Debug().
|
||||
Any("addrs", addrs).
|
||||
Strs("nameservers", nss).
|
||||
Int("index", i.Index).
|
||||
Msgf("interface state: %s", i.Name)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// findWorkingInterface looks for a network interface with a valid IP configuration
|
||||
func findWorkingInterface(currentIface string) string {
|
||||
// Helper to check if IP is valid (not link-local)
|
||||
isValidIP := func(ip net.IP) bool {
|
||||
return ip != nil &&
|
||||
!ip.IsLinkLocalUnicast() &&
|
||||
!ip.IsLinkLocalMulticast() &&
|
||||
!ip.IsLoopback() &&
|
||||
!ip.IsUnspecified()
|
||||
}
|
||||
|
||||
// Helper to check if interface has valid IP configuration
|
||||
hasValidIPConfig := func(iface *net.Interface) bool {
|
||||
if iface == nil || iface.Flags&net.FlagUp == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
mainLog.Load().Debug().
|
||||
Str("interface", iface.Name).
|
||||
Err(err).
|
||||
Msg("failed to get interface addresses")
|
||||
return false
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
// Check for IP network
|
||||
if ipNet, ok := addr.(*net.IPNet); ok {
|
||||
if isValidIP(ipNet.IP) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Get default route interface
|
||||
defaultRoute, err := netmon.DefaultRoute()
|
||||
if err != nil {
|
||||
mainLog.Load().Debug().
|
||||
Err(err).
|
||||
Msg("failed to get default route")
|
||||
} else {
|
||||
mainLog.Load().Debug().
|
||||
Str("default_route_iface", defaultRoute.InterfaceName).
|
||||
Msg("found default route")
|
||||
}
|
||||
|
||||
// Get all interfaces
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("failed to list network interfaces")
|
||||
return currentIface // Return current interface as fallback
|
||||
}
|
||||
|
||||
var firstWorkingIface string
|
||||
var currentIfaceValid bool
|
||||
|
||||
// Single pass through interfaces
|
||||
for _, iface := range ifaces {
|
||||
// Must be physical (has MAC address)
|
||||
if len(iface.HardwareAddr) == 0 {
|
||||
continue
|
||||
}
|
||||
// Skip interfaces that are:
|
||||
// - Loopback
|
||||
// - Not up
|
||||
// - Point-to-point (like VPN tunnels)
|
||||
if iface.Flags&net.FlagLoopback != 0 ||
|
||||
iface.Flags&net.FlagUp == 0 ||
|
||||
iface.Flags&net.FlagPointToPoint != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if !hasValidIPConfig(&iface) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Found working physical interface
|
||||
if err == nil && defaultRoute.InterfaceName == iface.Name {
|
||||
// Found interface with default route - use it immediately
|
||||
mainLog.Load().Info().
|
||||
Str("old_iface", currentIface).
|
||||
Str("new_iface", iface.Name).
|
||||
Msg("switching to interface with default route")
|
||||
return iface.Name
|
||||
}
|
||||
|
||||
// Keep track of first working interface as fallback
|
||||
if firstWorkingIface == "" {
|
||||
firstWorkingIface = iface.Name
|
||||
}
|
||||
|
||||
// Check if this is our current interface
|
||||
if iface.Name == currentIface {
|
||||
currentIfaceValid = true
|
||||
}
|
||||
}
|
||||
|
||||
// Return interfaces in order of preference:
|
||||
// 1. Current interface if it's still valid
|
||||
if currentIfaceValid {
|
||||
mainLog.Load().Debug().
|
||||
Str("interface", currentIface).
|
||||
Msg("keeping current interface")
|
||||
return currentIface
|
||||
}
|
||||
|
||||
// 2. First working interface found
|
||||
if firstWorkingIface != "" {
|
||||
mainLog.Load().Info().
|
||||
Str("old_iface", currentIface).
|
||||
Str("new_iface", firstWorkingIface).
|
||||
Msg("switching to first working physical interface")
|
||||
return firstWorkingIface
|
||||
}
|
||||
|
||||
// 3. Fall back to current interface if nothing else works
|
||||
mainLog.Load().Warn().
|
||||
Str("current_iface", currentIface).
|
||||
Msg("no working physical interface found, keeping current")
|
||||
return currentIface
|
||||
}
|
||||
|
||||
// recoverOnUpstreamFailure reports whether ctrld should recover from upstream failure.
|
||||
func (p *prog) recoverOnUpstreamFailure() bool {
|
||||
// Default is false on routers, since this recovery flow is only useful for devices that move between networks.
|
||||
return router.Name() == ""
|
||||
}
|
||||
|
||||
func randomLocalIP() string {
|
||||
n := rand.Intn(254-2) + 2
|
||||
return fmt.Sprintf("127.0.0.%d", n)
|
||||
@@ -830,7 +1108,7 @@ func ifaceFirstPrivateIP(iface *net.Interface) string {
|
||||
|
||||
// defaultRouteIP returns private IP string of the default route if present, prefer IPv4 over IPv6.
|
||||
func defaultRouteIP() string {
|
||||
dr, err := interfaces.DefaultRoute()
|
||||
dr, err := netmon.DefaultRoute()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
@@ -850,7 +1128,7 @@ func defaultRouteIP() string {
|
||||
// There could be multiple LAN interfaces with the same Mac address, so we find all private
|
||||
// IPs then using the smallest one.
|
||||
var addrs []netip.Addr
|
||||
interfaces.ForeachInterface(func(i interfaces.Interface, prefixes []netip.Prefix) {
|
||||
netmon.ForeachInterface(func(i netmon.Interface, prefixes []netip.Prefix) {
|
||||
if i.Name == drNetIface.Name {
|
||||
return
|
||||
}
|
||||
@@ -890,8 +1168,8 @@ func canBeLocalUpstream(addr string) bool {
|
||||
// log message when error happens.
|
||||
func withEachPhysicalInterfaces(excludeIfaceName, context string, f func(i *net.Interface) error) {
|
||||
validIfacesMap := validInterfacesMap()
|
||||
interfaces.ForeachInterface(func(i interfaces.Interface, prefixes []netip.Prefix) {
|
||||
// Skip loopback/virtual interface.
|
||||
netmon.ForeachInterface(func(i netmon.Interface, prefixes []netip.Prefix) {
|
||||
// Skip loopback/virtual/down interface.
|
||||
if i.IsLoopback() || len(i.HardwareAddr) == 0 {
|
||||
return
|
||||
}
|
||||
@@ -900,9 +1178,12 @@ func withEachPhysicalInterfaces(excludeIfaceName, context string, f func(i *net.
|
||||
return
|
||||
}
|
||||
netIface := i.Interface
|
||||
if err := patchNetIfaceName(netIface); err != nil {
|
||||
if patched, err := patchNetIfaceName(netIface); err != nil {
|
||||
mainLog.Load().Debug().Err(err).Msg("failed to patch net interface name")
|
||||
return
|
||||
} else if !patched {
|
||||
// The interface is not functional, skipping.
|
||||
return
|
||||
}
|
||||
// Skip excluded interface.
|
||||
if netIface.Name == excludeIfaceName {
|
||||
@@ -948,11 +1229,13 @@ func saveCurrentStaticDNS(iface *net.Interface) error {
|
||||
if err := os.Remove(file); err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
mainLog.Load().Warn().Err(err).Msg("could not remove old static DNS settings file")
|
||||
}
|
||||
mainLog.Load().Debug().Msgf("DNS settings for %s is static, saving ...", iface.Name)
|
||||
if err := os.WriteFile(file, []byte(strings.Join(ns, ",")), 0600); err != nil {
|
||||
nss := strings.Join(ns, ",")
|
||||
mainLog.Load().Debug().Msgf("DNS settings for %q is static: %v, saving ...", iface.Name, nss)
|
||||
if err := os.WriteFile(file, []byte(nss), 0600); err != nil {
|
||||
mainLog.Load().Err(err).Msgf("could not save DNS settings for iface: %s", iface.Name)
|
||||
return err
|
||||
}
|
||||
mainLog.Load().Debug().Msgf("save DNS settings for interface %q successfully", iface.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -967,7 +1250,16 @@ func savedStaticDnsSettingsFilePath(iface *net.Interface) string {
|
||||
func savedStaticNameservers(iface *net.Interface) []string {
|
||||
file := savedStaticDnsSettingsFilePath(iface)
|
||||
if data, _ := os.ReadFile(file); len(data) > 0 {
|
||||
return strings.Split(string(data), ",")
|
||||
saveValues := strings.Split(string(data), ",")
|
||||
returnValues := []string{}
|
||||
// check each one, if its in loopback range, remove it
|
||||
for _, v := range saveValues {
|
||||
if net.ParseIP(v).IsLoopback() {
|
||||
continue
|
||||
}
|
||||
returnValues = append(returnValues, v)
|
||||
}
|
||||
return returnValues
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -977,16 +1269,18 @@ func savedStaticNameservers(iface *net.Interface) []string {
|
||||
func dnsChanged(iface *net.Interface, nameservers []string) bool {
|
||||
curNameservers, _ := currentStaticDNS(iface)
|
||||
slices.Sort(curNameservers)
|
||||
return !slices.Equal(curNameservers, nameservers)
|
||||
if !slices.Equal(curNameservers, nameservers) {
|
||||
mainLog.Load().Debug().Msgf("interface %q current DNS settings: %v, expected: %v", iface.Name, curNameservers, nameservers)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// selfUninstallCheck checks if the error dues to controld.InvalidConfigCode, perform self-uninstall then.
|
||||
func selfUninstallCheck(uninstallErr error, p *prog, logger zerolog.Logger) {
|
||||
var uer *controld.UtilityErrorResponse
|
||||
var uer *controld.ErrorResponse
|
||||
if errors.As(uninstallErr, &uer) && uer.ErrorField.Code == controld.InvalidConfigCode {
|
||||
// Ensure all DNS watchers goroutine are terminated, so it won't mess up with self-uninstall.
|
||||
close(p.dnsWatcherStopCh)
|
||||
p.dnsWg.Wait()
|
||||
p.stopDnsWatchers()
|
||||
|
||||
// Perform self-uninstall now.
|
||||
selfUninstall(p, logger)
|
||||
|
||||
@@ -9,12 +9,14 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/kardianos/service"
|
||||
"tailscale.com/control/controlknobs"
|
||||
"tailscale.com/health"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld/internal/dns"
|
||||
)
|
||||
|
||||
func init() {
|
||||
if r, err := dns.NewOSConfigurator(func(format string, args ...any) {}, "lo"); err == nil {
|
||||
if r, err := dns.NewOSConfigurator(func(format string, args ...any) {}, &health.Tracker{}, &controlknobs.Knobs{}, "lo"); err == nil {
|
||||
useSystemdResolved = r.Mode() == "systemd-resolved"
|
||||
}
|
||||
// Disable quic-go's ECN support by default, see https://github.com/quic-go/quic-go/issues/3911
|
||||
|
||||
@@ -3,11 +3,38 @@ package cli
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
// parseResolvConfNameservers reads the resolv.conf file and returns the nameservers found.
|
||||
// Returns nil if no nameservers are found.
|
||||
func (p *prog) parseResolvConfNameservers(path string) ([]string, error) {
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Parse the file for "nameserver" lines
|
||||
var currentNS []string
|
||||
lines := strings.Split(string(content), "\n")
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "nameserver") {
|
||||
parts := strings.Fields(trimmed)
|
||||
if len(parts) >= 2 {
|
||||
currentNS = append(currentNS, parts[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return currentNS, nil
|
||||
}
|
||||
|
||||
// watchResolvConf watches any changes to /etc/resolv.conf file,
|
||||
// and reverting to the original config set by ctrld.
|
||||
func (p *prog) watchResolvConf(iface *net.Interface, ns []netip.Addr, setDnsFn func(iface *net.Interface, ns []netip.Addr) error) {
|
||||
@@ -40,6 +67,9 @@ func (p *prog) watchResolvConf(iface *net.Interface, ns []netip.Addr, setDnsFn f
|
||||
mainLog.Load().Debug().Msgf("stopping watcher for %s", resolvConfPath)
|
||||
return
|
||||
case event, ok := <-watcher.Events:
|
||||
if p.recoveryRunning.Load() {
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -47,17 +77,81 @@ func (p *prog) watchResolvConf(iface *net.Interface, ns []netip.Addr, setDnsFn f
|
||||
continue
|
||||
}
|
||||
if event.Has(fsnotify.Write) || event.Has(fsnotify.Create) {
|
||||
mainLog.Load().Debug().Msg("/etc/resolv.conf changes detected, reverting to ctrld setting")
|
||||
if err := watcher.Remove(watchDir); err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("failed to pause watcher")
|
||||
continue
|
||||
mainLog.Load().Debug().Msgf("/etc/resolv.conf changes detected, reading changes...")
|
||||
|
||||
// Convert expected nameservers to strings for comparison
|
||||
expectedNS := make([]string, len(ns))
|
||||
for i, addr := range ns {
|
||||
expectedNS[i] = addr.String()
|
||||
}
|
||||
if err := setDnsFn(iface, ns); err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("failed to revert /etc/resolv.conf changes")
|
||||
|
||||
var foundNS []string
|
||||
var err error
|
||||
|
||||
maxRetries := 1
|
||||
for retry := 0; retry < maxRetries; retry++ {
|
||||
foundNS, err = p.parseResolvConfNameservers(resolvConfPath)
|
||||
if err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("failed to read resolv.conf content")
|
||||
break
|
||||
}
|
||||
|
||||
// If we found nameservers, break out of retry loop
|
||||
if len(foundNS) > 0 {
|
||||
break
|
||||
}
|
||||
|
||||
// Only retry if we found no nameservers
|
||||
if retry < maxRetries-1 {
|
||||
mainLog.Load().Debug().Msgf("resolv.conf has no nameserver entries, retry %d/%d in 2 seconds", retry+1, maxRetries)
|
||||
select {
|
||||
case <-p.stopCh:
|
||||
return
|
||||
case <-p.dnsWatcherStopCh:
|
||||
return
|
||||
case <-time.After(2 * time.Second):
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
mainLog.Load().Debug().Msg("resolv.conf remained empty after all retries")
|
||||
}
|
||||
}
|
||||
if err := watcher.Add(watchDir); err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("failed to continue running watcher")
|
||||
return
|
||||
|
||||
// If we found nameservers, check if they match what we expect
|
||||
if len(foundNS) > 0 {
|
||||
// Check if the nameservers match exactly what we expect
|
||||
matches := len(foundNS) == len(expectedNS)
|
||||
if matches {
|
||||
for i := range foundNS {
|
||||
if foundNS[i] != expectedNS[i] {
|
||||
matches = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mainLog.Load().Debug().
|
||||
Strs("found", foundNS).
|
||||
Strs("expected", expectedNS).
|
||||
Bool("matches", matches).
|
||||
Msg("checking nameservers")
|
||||
|
||||
// Only revert if the nameservers don't match
|
||||
if !matches {
|
||||
if err := watcher.Remove(watchDir); err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("failed to pause watcher")
|
||||
continue
|
||||
}
|
||||
|
||||
if err := setDnsFn(iface, ns); err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("failed to revert /etc/resolv.conf changes")
|
||||
}
|
||||
|
||||
if err := watcher.Add(watchDir); err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("failed to continue running watcher")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case err, ok := <-watcher.Errors:
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"net"
|
||||
"net/netip"
|
||||
|
||||
"tailscale.com/control/controlknobs"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/util/dnsname"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld/internal/dns"
|
||||
@@ -13,7 +15,7 @@ import (
|
||||
|
||||
// setResolvConf sets the content of resolv.conf file using the given nameservers list.
|
||||
func setResolvConf(iface *net.Interface, ns []netip.Addr) error {
|
||||
r, err := dns.NewOSConfigurator(func(format string, args ...any) {}, "lo") // interface name does not matter.
|
||||
r, err := dns.NewOSConfigurator(func(format string, args ...any) {}, &health.Tracker{}, &controlknobs.Knobs{}, "lo") // interface name does not matter.
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -27,7 +29,7 @@ func setResolvConf(iface *net.Interface, ns []netip.Addr) error {
|
||||
|
||||
// shouldWatchResolvconf reports whether ctrld should watch changes to resolv.conf file with given OS configurator.
|
||||
func shouldWatchResolvconf() bool {
|
||||
r, err := dns.NewOSConfigurator(func(format string, args ...any) {}, "lo") // interface name does not matter.
|
||||
r, err := dns.NewOSConfigurator(func(format string, args ...any) {}, &health.Tracker{}, &controlknobs.Knobs{}, "lo") // interface name does not matter.
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ func selfUninstall(p *prog, logger zerolog.Logger) {
|
||||
}
|
||||
args := []string{"uninstall"}
|
||||
if !deactivationPinNotSet() {
|
||||
args = append(args, fmt.Sprintf("--pin=%d", cdDeactivationPin))
|
||||
args = append(args, fmt.Sprintf("--pin=%d", cdDeactivationPin.Load()))
|
||||
}
|
||||
cmd := exec.Command(bin, args...)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
|
||||
@@ -156,17 +156,18 @@ func (l *launchd) Status() (service.Status, error) {
|
||||
type task struct {
|
||||
f func() error
|
||||
abortOnError bool
|
||||
Name string
|
||||
}
|
||||
|
||||
func doTasks(tasks []task) bool {
|
||||
var prevErr error
|
||||
for _, task := range tasks {
|
||||
mainLog.Load().Debug().Msgf("Running task %s", task.Name)
|
||||
if err := task.f(); err != nil {
|
||||
if task.abortOnError {
|
||||
mainLog.Load().Error().Msg(errors.Join(prevErr, err).Error())
|
||||
mainLog.Load().Error().Msgf("error running task %s: %v", task.Name, err)
|
||||
return false
|
||||
}
|
||||
prevErr = err
|
||||
mainLog.Load().Debug().Msgf("error running task %s: %v", task.Name, err)
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -13,3 +13,8 @@ func hasElevatedPrivilege() (bool, error) {
|
||||
func openLogFile(path string, flags int) (*os.File, error) {
|
||||
return os.OpenFile(path, flags, os.FileMode(0o600))
|
||||
}
|
||||
|
||||
// hasLocalDnsServerRunning reports whether we are on Windows and having Dns server running.
|
||||
func hasLocalDnsServerRunning() bool { return false }
|
||||
|
||||
func ConfigureWindowsServiceFailureActions(serviceName string) error { return nil }
|
||||
|
||||
@@ -2,9 +2,14 @@ package cli
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/svc/mgr"
|
||||
)
|
||||
|
||||
func hasElevatedPrivilege() (bool, error) {
|
||||
@@ -28,6 +33,67 @@ func hasElevatedPrivilege() (bool, error) {
|
||||
return token.IsMember(sid)
|
||||
}
|
||||
|
||||
// ConfigureWindowsServiceFailureActions checks if the given service
|
||||
// has the correct failure actions configured, and updates them if not.
|
||||
func ConfigureWindowsServiceFailureActions(serviceName string) error {
|
||||
if runtime.GOOS != "windows" {
|
||||
return nil // no-op on non-Windows
|
||||
}
|
||||
|
||||
m, err := mgr.Connect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer m.Disconnect()
|
||||
|
||||
s, err := m.OpenService(serviceName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
// 1. Retrieve the current config
|
||||
cfg, err := s.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. Update the Description
|
||||
cfg.Description = "A highly configurable, multi-protocol DNS forwarding proxy"
|
||||
|
||||
// 3. Apply the updated config
|
||||
if err := s.UpdateConfig(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Then proceed with existing actions, e.g. setting failure actions
|
||||
actions := []mgr.RecoveryAction{
|
||||
{Type: mgr.ServiceRestart, Delay: time.Second * 5}, // 5 seconds
|
||||
{Type: mgr.ServiceRestart, Delay: time.Second * 5}, // 5 seconds
|
||||
{Type: mgr.ServiceRestart, Delay: time.Second * 5}, // 5 seconds
|
||||
}
|
||||
|
||||
// Set the recovery actions (3 restarts, reset period = 120).
|
||||
err = s.SetRecoveryActions(actions, 120)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ensure that failure actions are NOT triggered on user-initiated stops.
|
||||
var failureActionsFlag windows.SERVICE_FAILURE_ACTIONS_FLAG
|
||||
failureActionsFlag.FailureActionsOnNonCrashFailures = 0
|
||||
|
||||
if err := windows.ChangeServiceConfig2(
|
||||
s.Handle,
|
||||
windows.SERVICE_CONFIG_FAILURE_ACTIONS_FLAG,
|
||||
(*byte)(unsafe.Pointer(&failureActionsFlag)),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func openLogFile(path string, mode int) (*os.File, error) {
|
||||
if len(path) == 0 {
|
||||
return nil, &os.PathError{Path: path, Op: "open", Err: syscall.ERROR_FILE_NOT_FOUND}
|
||||
@@ -79,3 +145,23 @@ func openLogFile(path string, mode int) (*os.File, error) {
|
||||
|
||||
return os.NewFile(uintptr(handle), path), nil
|
||||
}
|
||||
|
||||
const processEntrySize = uint32(unsafe.Sizeof(windows.ProcessEntry32{}))
|
||||
|
||||
// hasLocalDnsServerRunning reports whether we are on Windows and having Dns server running.
|
||||
func hasLocalDnsServerRunning() bool {
|
||||
h, e := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0)
|
||||
if e != nil {
|
||||
return false
|
||||
}
|
||||
p := windows.ProcessEntry32{Size: processEntrySize}
|
||||
for {
|
||||
e := windows.Process32Next(h, &p)
|
||||
if e != nil {
|
||||
return false
|
||||
}
|
||||
if strings.ToLower(windows.UTF16ToString(p.ExeFile[:])) == "dns.exe" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
25
cmd/cli/service_windows_test.go
Normal file
25
cmd/cli/service_windows_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Test_hasLocalDnsServerRunning(t *testing.T) {
|
||||
start := time.Now()
|
||||
hasDns := hasLocalDnsServerRunning()
|
||||
t.Logf("Using Windows API takes: %d", time.Since(start).Milliseconds())
|
||||
|
||||
start = time.Now()
|
||||
hasDnsPowershell := hasLocalDnsServerRunningPowershell()
|
||||
t.Logf("Using Powershell takes: %d", time.Since(start).Milliseconds())
|
||||
|
||||
if hasDns != hasDnsPowershell {
|
||||
t.Fatalf("result mismatch, want: %v, got: %v", hasDnsPowershell, hasDns)
|
||||
}
|
||||
}
|
||||
|
||||
func hasLocalDnsServerRunningPowershell() bool {
|
||||
_, err := powershell("Get-Process -Name DNS")
|
||||
return err == nil
|
||||
}
|
||||
@@ -1,18 +1,15 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld"
|
||||
)
|
||||
|
||||
const (
|
||||
// maxFailureRequest is the maximum failed queries allowed before an upstream is marked as down.
|
||||
maxFailureRequest = 100
|
||||
maxFailureRequest = 50
|
||||
// checkUpstreamBackoffSleep is the time interval between each upstream checks.
|
||||
checkUpstreamBackoffSleep = 2 * time.Second
|
||||
)
|
||||
@@ -21,18 +18,24 @@ const (
|
||||
type upstreamMonitor struct {
|
||||
cfg *ctrld.Config
|
||||
|
||||
mu sync.Mutex
|
||||
mu sync.RWMutex
|
||||
checking map[string]bool
|
||||
down map[string]bool
|
||||
failureReq map[string]uint64
|
||||
recovered map[string]bool
|
||||
|
||||
// failureTimerActive tracks if a timer is already running for a given upstream.
|
||||
failureTimerActive map[string]bool
|
||||
}
|
||||
|
||||
func newUpstreamMonitor(cfg *ctrld.Config) *upstreamMonitor {
|
||||
um := &upstreamMonitor{
|
||||
cfg: cfg,
|
||||
checking: make(map[string]bool),
|
||||
down: make(map[string]bool),
|
||||
failureReq: make(map[string]uint64),
|
||||
cfg: cfg,
|
||||
checking: make(map[string]bool),
|
||||
down: make(map[string]bool),
|
||||
failureReq: make(map[string]uint64),
|
||||
recovered: make(map[string]bool),
|
||||
failureTimerActive: make(map[string]bool),
|
||||
}
|
||||
for n := range cfg.Upstream {
|
||||
upstream := upstreamPrefix + n
|
||||
@@ -42,14 +45,47 @@ func newUpstreamMonitor(cfg *ctrld.Config) *upstreamMonitor {
|
||||
return um
|
||||
}
|
||||
|
||||
// increaseFailureCount increase failed queries count for an upstream by 1.
|
||||
// increaseFailureCount increases failed queries count for an upstream by 1 and logs debug information.
|
||||
// It uses a timer to debounce failure detection, ensuring that an upstream is marked as down
|
||||
// within 10 seconds if failures persist, without spawning duplicate goroutines.
|
||||
func (um *upstreamMonitor) increaseFailureCount(upstream string) {
|
||||
um.mu.Lock()
|
||||
defer um.mu.Unlock()
|
||||
|
||||
if um.recovered[upstream] {
|
||||
mainLog.Load().Debug().Msgf("upstream %q is recovered, skipping failure count increase", upstream)
|
||||
return
|
||||
}
|
||||
|
||||
um.failureReq[upstream] += 1
|
||||
failedCount := um.failureReq[upstream]
|
||||
um.down[upstream] = failedCount >= maxFailureRequest
|
||||
|
||||
// Log the updated failure count.
|
||||
mainLog.Load().Debug().Msgf("upstream %q failure count updated to %d", upstream, failedCount)
|
||||
|
||||
// If this is the first failure and no timer is running, start a 10-second timer.
|
||||
if failedCount == 1 && !um.failureTimerActive[upstream] {
|
||||
um.failureTimerActive[upstream] = true
|
||||
go func(upstream string) {
|
||||
time.Sleep(10 * time.Second)
|
||||
um.mu.Lock()
|
||||
defer um.mu.Unlock()
|
||||
// If no success occurred during the 10-second window (i.e. counter remains > 0)
|
||||
// and the upstream is not in a recovered state, mark it as down.
|
||||
if um.failureReq[upstream] > 0 && !um.recovered[upstream] {
|
||||
um.down[upstream] = true
|
||||
mainLog.Load().Warn().Msgf("upstream %q marked as down after 10 seconds (failure count: %d)", upstream, um.failureReq[upstream])
|
||||
}
|
||||
// Reset the timer flag so that a new timer can be spawned if needed.
|
||||
um.failureTimerActive[upstream] = false
|
||||
}(upstream)
|
||||
}
|
||||
|
||||
// If the failure count quickly reaches the threshold, mark the upstream as down immediately.
|
||||
if failedCount >= maxFailureRequest {
|
||||
um.down[upstream] = true
|
||||
mainLog.Load().Warn().Msgf("upstream %q marked as down immediately (failure count: %d)", upstream, failedCount)
|
||||
}
|
||||
}
|
||||
|
||||
// isDown reports whether the given upstream is being marked as down.
|
||||
@@ -63,50 +99,28 @@ func (um *upstreamMonitor) isDown(upstream string) bool {
|
||||
// reset marks an upstream as up and set failed queries counter to zero.
|
||||
func (um *upstreamMonitor) reset(upstream string) {
|
||||
um.mu.Lock()
|
||||
defer um.mu.Unlock()
|
||||
|
||||
um.failureReq[upstream] = 0
|
||||
um.down[upstream] = false
|
||||
}
|
||||
|
||||
// checkUpstream checks the given upstream status, periodically sending query to upstream
|
||||
// until successfully. An upstream status/counter will be reset once it becomes reachable.
|
||||
func (um *upstreamMonitor) checkUpstream(upstream string, uc *ctrld.UpstreamConfig) {
|
||||
um.mu.Lock()
|
||||
isChecking := um.checking[upstream]
|
||||
if isChecking {
|
||||
um.mu.Unlock()
|
||||
return
|
||||
}
|
||||
um.checking[upstream] = true
|
||||
um.recovered[upstream] = true
|
||||
um.mu.Unlock()
|
||||
defer func() {
|
||||
go func() {
|
||||
// debounce the recovery to avoid incrementing failure counts already in flight
|
||||
time.Sleep(1 * time.Second)
|
||||
um.mu.Lock()
|
||||
um.checking[upstream] = false
|
||||
um.recovered[upstream] = false
|
||||
um.mu.Unlock()
|
||||
}()
|
||||
|
||||
resolver, err := ctrld.NewResolver(uc)
|
||||
if err != nil {
|
||||
mainLog.Load().Warn().Err(err).Msg("could not check upstream")
|
||||
return
|
||||
}
|
||||
msg := new(dns.Msg)
|
||||
msg.SetQuestion(".", dns.TypeNS)
|
||||
|
||||
check := func() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
uc.ReBootstrap()
|
||||
_, err := resolver.Resolve(ctx, msg)
|
||||
return err
|
||||
}
|
||||
for {
|
||||
if err := check(); err == nil {
|
||||
mainLog.Load().Debug().Msgf("upstream %q is online", uc.Endpoint)
|
||||
um.reset(upstream)
|
||||
return
|
||||
}
|
||||
time.Sleep(checkUpstreamBackoffSleep)
|
||||
}
|
||||
}
|
||||
|
||||
// countHealthy returns the number of upstreams in the provided map that are considered healthy.
|
||||
func (um *upstreamMonitor) countHealthy(upstreams []string) int {
|
||||
var count int
|
||||
um.mu.RLock()
|
||||
for _, upstream := range upstreams {
|
||||
if !um.down[upstream] {
|
||||
count++
|
||||
}
|
||||
}
|
||||
um.mu.RUnlock()
|
||||
return count
|
||||
}
|
||||
|
||||
115
config.go
115
config.go
@@ -7,6 +7,7 @@ import (
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net"
|
||||
@@ -22,6 +23,7 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/ameshkov/dnsstamps"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/spf13/viper"
|
||||
@@ -59,6 +61,11 @@ const (
|
||||
controlDComDomain = "controld.com"
|
||||
controlDNetDomain = "controld.net"
|
||||
controlDDevDomain = "controld.dev"
|
||||
|
||||
endpointPrefixHTTPS = "https://"
|
||||
endpointPrefixQUIC = "quic://"
|
||||
endpointPrefixH3 = "h3://"
|
||||
endpointPrefixSdns = "sdns://"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -198,7 +205,7 @@ type ServiceConfig struct {
|
||||
CacheFlushDomains []string `mapstructure:"cache_flush_domains" toml:"cache_flush_domains" validate:"max=256"`
|
||||
MaxConcurrentRequests *int `mapstructure:"max_concurrent_requests" toml:"max_concurrent_requests,omitempty" validate:"omitempty,gte=0"`
|
||||
DHCPLeaseFile string `mapstructure:"dhcp_lease_file_path" toml:"dhcp_lease_file_path" validate:"omitempty,file"`
|
||||
DHCPLeaseFileFormat string `mapstructure:"dhcp_lease_file_format" toml:"dhcp_lease_file_format" validate:"required_unless=DHCPLeaseFile '',omitempty,oneof=dnsmasq isc-dhcp"`
|
||||
DHCPLeaseFileFormat string `mapstructure:"dhcp_lease_file_format" toml:"dhcp_lease_file_format" validate:"required_unless=DHCPLeaseFile '',omitempty,oneof=dnsmasq isc-dhcp kea-dhcp4"`
|
||||
DiscoverMDNS *bool `mapstructure:"discover_mdns" toml:"discover_mdns,omitempty"`
|
||||
DiscoverARP *bool `mapstructure:"discover_arp" toml:"discover_arp,omitempty"`
|
||||
DiscoverDHCP *bool `mapstructure:"discover_dhcp" toml:"discover_dhcp,omitempty"`
|
||||
@@ -211,6 +218,8 @@ type ServiceConfig struct {
|
||||
DnsWatchdogEnabled *bool `mapstructure:"dns_watchdog_enabled" toml:"dns_watchdog_enabled,omitempty"`
|
||||
DnsWatchdogInvterval *time.Duration `mapstructure:"dns_watchdog_interval" toml:"dns_watchdog_interval,omitempty"`
|
||||
RefetchTime *int `mapstructure:"refetch_time" toml:"refetch_time,omitempty"`
|
||||
ForceRefetchWaitTime *int `mapstructure:"force_refetch_wait_time" toml:"force_refetch_wait_time,omitempty"`
|
||||
LeakOnUpstreamFailure *bool `mapstructure:"leak_on_upstream_failure" toml:"leak_on_upstream_failure,omitempty"`
|
||||
Daemon bool `mapstructure:"-" toml:"-"`
|
||||
AllocateIP bool `mapstructure:"-" toml:"-"`
|
||||
}
|
||||
@@ -225,7 +234,7 @@ 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"`
|
||||
Type string `mapstructure:"type" toml:"type,omitempty" validate:"oneof=doh doh3 dot doq os legacy sdns ''"`
|
||||
Endpoint string `mapstructure:"endpoint" toml:"endpoint,omitempty"`
|
||||
BootstrapIP string `mapstructure:"bootstrap_ip" toml:"bootstrap_ip,omitempty"`
|
||||
Domain string `mapstructure:"-" toml:"-"`
|
||||
@@ -299,10 +308,13 @@ type Rule map[string][]string
|
||||
|
||||
// Init initialized necessary values for an UpstreamConfig.
|
||||
func (uc *UpstreamConfig) Init() {
|
||||
if err := uc.initDnsStamps(); err != nil {
|
||||
ProxyLogger.Load().Fatal().Err(err).Msg("invalid DNS Stamps")
|
||||
}
|
||||
uc.initDoHScheme()
|
||||
uc.uid = upstreamUID()
|
||||
if u, err := url.Parse(uc.Endpoint); err == nil {
|
||||
uc.Domain = u.Host
|
||||
uc.Domain = u.Hostname()
|
||||
switch uc.Type {
|
||||
case ResolverTypeDOH, ResolverTypeDOH3:
|
||||
uc.u = u
|
||||
@@ -372,7 +384,7 @@ func (uc *UpstreamConfig) IsDiscoverable() bool {
|
||||
return *uc.Discoverable
|
||||
}
|
||||
switch uc.Type {
|
||||
case ResolverTypeOS, ResolverTypeLegacy, ResolverTypePrivate:
|
||||
case ResolverTypeOS, ResolverTypeLegacy, ResolverTypePrivate, ResolverTypeLocal:
|
||||
if ip, err := netip.ParseAddr(uc.Domain); err == nil {
|
||||
return ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || tsaddr.CGNATRange().Contains(ip)
|
||||
}
|
||||
@@ -446,7 +458,7 @@ func (uc *UpstreamConfig) ReBootstrap() {
|
||||
}
|
||||
_, _, _ = uc.g.Do("ReBootstrap", func() (any, error) {
|
||||
if uc.rebootstrap.CompareAndSwap(false, true) {
|
||||
ProxyLogger.Load().Debug().Msg("re-bootstrapping upstream ip")
|
||||
ProxyLogger.Load().Debug().Msgf("re-bootstrapping upstream ip for %v", uc)
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
@@ -676,16 +688,67 @@ func (uc *UpstreamConfig) netForDNSType(dnsType uint16) (string, string) {
|
||||
|
||||
// initDoHScheme initializes the endpoint scheme for DoH/DoH3 upstream if not present.
|
||||
func (uc *UpstreamConfig) initDoHScheme() {
|
||||
if strings.HasPrefix(uc.Endpoint, endpointPrefixH3) && uc.Type == "" {
|
||||
uc.Type = ResolverTypeDOH3
|
||||
}
|
||||
switch uc.Type {
|
||||
case ResolverTypeDOH, ResolverTypeDOH3:
|
||||
case ResolverTypeDOH:
|
||||
case ResolverTypeDOH3:
|
||||
if after, found := strings.CutPrefix(uc.Endpoint, endpointPrefixH3); found {
|
||||
uc.Endpoint = endpointPrefixHTTPS + after
|
||||
}
|
||||
default:
|
||||
return
|
||||
}
|
||||
if !strings.HasPrefix(uc.Endpoint, "https://") {
|
||||
uc.Endpoint = "https://" + uc.Endpoint
|
||||
if !strings.HasPrefix(uc.Endpoint, endpointPrefixHTTPS) {
|
||||
uc.Endpoint = endpointPrefixHTTPS + uc.Endpoint
|
||||
}
|
||||
}
|
||||
|
||||
// initDnsStamps initializes upstream config based on encoded DNS Stamps Endpoint.
|
||||
func (uc *UpstreamConfig) initDnsStamps() error {
|
||||
if strings.HasPrefix(uc.Endpoint, endpointPrefixSdns) && uc.Type == "" {
|
||||
uc.Type = ResolverTypeSDNS
|
||||
}
|
||||
if uc.Type != ResolverTypeSDNS {
|
||||
return nil
|
||||
}
|
||||
sdns, err := dnsstamps.NewServerStampFromString(uc.Endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ip, port, _ := net.SplitHostPort(sdns.ServerAddrStr)
|
||||
providerName, port2, _ := net.SplitHostPort(sdns.ProviderName)
|
||||
if port2 != "" {
|
||||
port = port2
|
||||
}
|
||||
if providerName == "" {
|
||||
providerName = sdns.ProviderName
|
||||
}
|
||||
switch sdns.Proto {
|
||||
case dnsstamps.StampProtoTypeDoH:
|
||||
uc.Type = ResolverTypeDOH
|
||||
host := sdns.ProviderName
|
||||
if port != "" && port != defaultPortFor(uc.Type) {
|
||||
host = net.JoinHostPort(providerName, port)
|
||||
}
|
||||
uc.Endpoint = "https://" + host + sdns.Path
|
||||
case dnsstamps.StampProtoTypeTLS:
|
||||
uc.Type = ResolverTypeDOT
|
||||
uc.Endpoint = net.JoinHostPort(providerName, port)
|
||||
case dnsstamps.StampProtoTypeDoQ:
|
||||
uc.Type = ResolverTypeDOQ
|
||||
uc.Endpoint = net.JoinHostPort(providerName, port)
|
||||
case dnsstamps.StampProtoTypePlain:
|
||||
uc.Type = ResolverTypeLegacy
|
||||
uc.Endpoint = sdns.ServerAddrStr
|
||||
default:
|
||||
return fmt.Errorf("unsupported stamp protocol %q", sdns.Proto)
|
||||
}
|
||||
uc.BootstrapIP = ip
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init initialized necessary values for an ListenerConfig.
|
||||
func (lc *ListenerConfig) Init() {
|
||||
if lc.Policy != nil {
|
||||
@@ -738,6 +801,23 @@ func upstreamConfigStructLevelValidation(sl validator.StructLevel) {
|
||||
return
|
||||
}
|
||||
|
||||
// Empty type is ok only for endpoints starts with "h3://" and "sdns://".
|
||||
if uc.Type == "" && !strings.HasPrefix(uc.Endpoint, endpointPrefixH3) && !strings.HasPrefix(uc.Endpoint, endpointPrefixSdns) {
|
||||
sl.ReportError(uc.Endpoint, "type", "type", "oneof", "doh doh3 dot doq os legacy sdns")
|
||||
return
|
||||
}
|
||||
|
||||
// initDoHScheme/initDnsStamps may change upstreams information,
|
||||
// so restoring changed values after validation to keep original one.
|
||||
defer func(ep, typ string) {
|
||||
uc.Endpoint = ep
|
||||
uc.Type = typ
|
||||
}(uc.Endpoint, uc.Type)
|
||||
|
||||
if err := uc.initDnsStamps(); err != nil {
|
||||
sl.ReportError(uc.Endpoint, "endpoint", "Endpoint", "http_url", "")
|
||||
return
|
||||
}
|
||||
uc.initDoHScheme()
|
||||
// DoH/DoH3 requires endpoint is an HTTP url.
|
||||
if uc.Type == ResolverTypeDOH || uc.Type == ResolverTypeDOH3 {
|
||||
@@ -767,13 +847,19 @@ func defaultPortFor(typ string) string {
|
||||
// - If endpoint is an IP address -> ResolverTypeLegacy
|
||||
// - If endpoint starts with "https://" -> ResolverTypeDOH
|
||||
// - If endpoint starts with "quic://" -> ResolverTypeDOQ
|
||||
// - If endpoint starts with "h3://" -> ResolverTypeDOH3
|
||||
// - If endpoint starts with "sdns://" -> ResolverTypeSDNS
|
||||
// - For anything else -> ResolverTypeDOT
|
||||
func ResolverTypeFromEndpoint(endpoint string) string {
|
||||
switch {
|
||||
case strings.HasPrefix(endpoint, "https://"):
|
||||
case strings.HasPrefix(endpoint, endpointPrefixHTTPS):
|
||||
return ResolverTypeDOH
|
||||
case strings.HasPrefix(endpoint, "quic://"):
|
||||
case strings.HasPrefix(endpoint, endpointPrefixQUIC):
|
||||
return ResolverTypeDOQ
|
||||
case strings.HasPrefix(endpoint, endpointPrefixH3):
|
||||
return ResolverTypeDOH3
|
||||
case strings.HasPrefix(endpoint, endpointPrefixSdns):
|
||||
return ResolverTypeSDNS
|
||||
}
|
||||
host := endpoint
|
||||
if strings.Contains(endpoint, ":") {
|
||||
@@ -800,3 +886,12 @@ func upstreamUID() string {
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a string representation of the UpstreamConfig for logging.
|
||||
func (uc *UpstreamConfig) String() string {
|
||||
if uc == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
return fmt.Sprintf("{name: %q, type: %q, endpoint: %q, bootstrap_ip: %q, domain: %q, ip_stack: %q}",
|
||||
uc.Name, uc.Type, uc.Endpoint, uc.BootstrapIP, uc.Domain, uc.IPStack)
|
||||
}
|
||||
|
||||
@@ -2,12 +2,16 @@ package ctrld
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUpstreamConfig_SetupBootstrapIP(t *testing.T) {
|
||||
l := zerolog.New(os.Stdout)
|
||||
ProxyLogger.Store(&l)
|
||||
uc := &UpstreamConfig{
|
||||
Name: "test",
|
||||
Type: ResolverTypeDOH,
|
||||
@@ -17,7 +21,7 @@ func TestUpstreamConfig_SetupBootstrapIP(t *testing.T) {
|
||||
uc.Init()
|
||||
uc.setupBootstrapIP(false)
|
||||
if len(uc.bootstrapIPs) == 0 {
|
||||
t.Log(nameservers())
|
||||
t.Log(defaultNameservers())
|
||||
t.Fatal("could not bootstrap ip without bootstrap DNS")
|
||||
}
|
||||
t.Log(uc)
|
||||
@@ -26,6 +30,7 @@ func TestUpstreamConfig_SetupBootstrapIP(t *testing.T) {
|
||||
func TestUpstreamConfig_Init(t *testing.T) {
|
||||
u1, _ := url.Parse("https://example.com")
|
||||
u2, _ := url.Parse("https://example.com?k=v")
|
||||
u3, _ := url.Parse("https://freedns.controld.com/p1")
|
||||
tests := []struct {
|
||||
name string
|
||||
uc *UpstreamConfig
|
||||
@@ -178,6 +183,152 @@ func TestUpstreamConfig_Init(t *testing.T) {
|
||||
u: u2,
|
||||
},
|
||||
},
|
||||
{
|
||||
"h3",
|
||||
&UpstreamConfig{
|
||||
Name: "doh3",
|
||||
Type: "doh3",
|
||||
Endpoint: "h3://example.com",
|
||||
BootstrapIP: "",
|
||||
Domain: "",
|
||||
Timeout: 0,
|
||||
},
|
||||
&UpstreamConfig{
|
||||
Name: "doh3",
|
||||
Type: "doh3",
|
||||
Endpoint: "https://example.com",
|
||||
BootstrapIP: "",
|
||||
Domain: "example.com",
|
||||
Timeout: 0,
|
||||
IPStack: IpStackBoth,
|
||||
u: u1,
|
||||
},
|
||||
},
|
||||
{
|
||||
"h3 without type",
|
||||
&UpstreamConfig{
|
||||
Name: "doh3",
|
||||
Endpoint: "h3://example.com",
|
||||
BootstrapIP: "",
|
||||
Domain: "",
|
||||
Timeout: 0,
|
||||
},
|
||||
&UpstreamConfig{
|
||||
Name: "doh3",
|
||||
Type: "doh3",
|
||||
Endpoint: "https://example.com",
|
||||
BootstrapIP: "",
|
||||
Domain: "example.com",
|
||||
Timeout: 0,
|
||||
IPStack: IpStackBoth,
|
||||
u: u1,
|
||||
},
|
||||
},
|
||||
{
|
||||
"sdns -> doh",
|
||||
&UpstreamConfig{
|
||||
Name: "sdns",
|
||||
Type: "sdns",
|
||||
Endpoint: "sdns://AgMAAAAAAAAACjc2Ljc2LjIuMTEAFGZyZWVkbnMuY29udHJvbGQuY29tAy9wMQ",
|
||||
BootstrapIP: "",
|
||||
Domain: "",
|
||||
Timeout: 0,
|
||||
IPStack: IpStackBoth,
|
||||
},
|
||||
&UpstreamConfig{
|
||||
Name: "sdns",
|
||||
Type: "doh",
|
||||
Endpoint: "https://freedns.controld.com/p1",
|
||||
BootstrapIP: "76.76.2.11",
|
||||
Domain: "freedns.controld.com",
|
||||
Timeout: 0,
|
||||
IPStack: IpStackBoth,
|
||||
u: u3,
|
||||
},
|
||||
},
|
||||
{
|
||||
"sdns -> dot",
|
||||
&UpstreamConfig{
|
||||
Name: "sdns",
|
||||
Type: "sdns",
|
||||
Endpoint: "sdns://AwcAAAAAAAAACjc2Ljc2LjIuMTEAFGZyZWVkbnMuY29udHJvbGQuY29t",
|
||||
BootstrapIP: "",
|
||||
Domain: "",
|
||||
Timeout: 0,
|
||||
IPStack: IpStackBoth,
|
||||
},
|
||||
&UpstreamConfig{
|
||||
Name: "sdns",
|
||||
Type: "dot",
|
||||
Endpoint: "freedns.controld.com:843",
|
||||
BootstrapIP: "76.76.2.11",
|
||||
Domain: "freedns.controld.com",
|
||||
Timeout: 0,
|
||||
IPStack: IpStackBoth,
|
||||
},
|
||||
},
|
||||
{
|
||||
"sdns -> doq",
|
||||
&UpstreamConfig{
|
||||
Name: "sdns",
|
||||
Type: "sdns",
|
||||
Endpoint: "sdns://BAcAAAAAAAAACjc2Ljc2LjIuMTEAFGZyZWVkbnMuY29udHJvbGQuY29t",
|
||||
BootstrapIP: "",
|
||||
Domain: "",
|
||||
Timeout: 0,
|
||||
IPStack: IpStackBoth,
|
||||
},
|
||||
&UpstreamConfig{
|
||||
Name: "sdns",
|
||||
Type: "doq",
|
||||
Endpoint: "freedns.controld.com:784",
|
||||
BootstrapIP: "76.76.2.11",
|
||||
Domain: "freedns.controld.com",
|
||||
Timeout: 0,
|
||||
IPStack: IpStackBoth,
|
||||
},
|
||||
},
|
||||
{
|
||||
"sdns -> legacy",
|
||||
&UpstreamConfig{
|
||||
Name: "sdns",
|
||||
Type: "sdns",
|
||||
Endpoint: "sdns://AAcAAAAAAAAACjc2Ljc2LjIuMTE",
|
||||
BootstrapIP: "",
|
||||
Domain: "",
|
||||
Timeout: 0,
|
||||
IPStack: IpStackBoth,
|
||||
},
|
||||
&UpstreamConfig{
|
||||
Name: "sdns",
|
||||
Type: "legacy",
|
||||
Endpoint: "76.76.2.11:53",
|
||||
BootstrapIP: "76.76.2.11",
|
||||
Domain: "76.76.2.11",
|
||||
Timeout: 0,
|
||||
IPStack: IpStackBoth,
|
||||
},
|
||||
},
|
||||
{
|
||||
"sdns without type",
|
||||
&UpstreamConfig{
|
||||
Name: "sdns",
|
||||
Endpoint: "sdns://AAcAAAAAAAAACjc2Ljc2LjIuMTE",
|
||||
BootstrapIP: "",
|
||||
Domain: "",
|
||||
Timeout: 0,
|
||||
IPStack: IpStackBoth,
|
||||
},
|
||||
&UpstreamConfig{
|
||||
Name: "sdns",
|
||||
Type: "legacy",
|
||||
Endpoint: "76.76.2.11:53",
|
||||
BootstrapIP: "76.76.2.11",
|
||||
Domain: "76.76.2.11",
|
||||
Timeout: 0,
|
||||
IPStack: IpStackBoth,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
|
||||
@@ -34,7 +34,7 @@ func (uc *UpstreamConfig) setupDOH3Transport() {
|
||||
}
|
||||
|
||||
func (uc *UpstreamConfig) newDOH3Transport(addrs []string) http.RoundTripper {
|
||||
rt := &http3.RoundTripper{}
|
||||
rt := &http3.Transport{}
|
||||
rt.TLSClientConfig = &tls.Config{RootCAs: uc.certPool}
|
||||
rt.Dial = func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
||||
_, port, _ := net.SplitHostPort(addr)
|
||||
@@ -64,7 +64,7 @@ func (uc *UpstreamConfig) newDOH3Transport(addrs []string) http.RoundTripper {
|
||||
ProxyLogger.Load().Debug().Msgf("sending doh3 request to: %s", conn.RemoteAddr())
|
||||
return conn, err
|
||||
}
|
||||
runtime.SetFinalizer(rt, func(rt *http3.RoundTripper) {
|
||||
runtime.SetFinalizer(rt, func(rt *http3.Transport) {
|
||||
rt.CloseIdleConnections()
|
||||
})
|
||||
return rt
|
||||
|
||||
@@ -107,7 +107,11 @@ func TestConfigValidation(t *testing.T) {
|
||||
{"invalid doh/doh3 endpoint", configWithInvalidDoHEndpoint(t), true},
|
||||
{"invalid client id pref", configWithInvalidClientIDPref(t), true},
|
||||
{"doh endpoint without scheme", dohUpstreamEndpointWithoutScheme(t), false},
|
||||
{"doh endpoint without type", dohUpstreamEndpointWithoutType(t), true},
|
||||
{"doh3 endpoint without type", doh3UpstreamEndpointWithoutType(t), false},
|
||||
{"sdns endpoint without type", sdnsUpstreamEndpointWithoutType(t), false},
|
||||
{"maximum number of flush cache domains", configWithInvalidFlushCacheDomain(t), true},
|
||||
{"kea dhcp4 format", configWithDhcp4KeaFormat(t), false},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
@@ -127,6 +131,21 @@ func TestConfigValidation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigValidationDoNotChangeEndpoint(t *testing.T) {
|
||||
cfg := configWithInvalidDoHEndpoint(t)
|
||||
endpointMap := map[string]struct{}{}
|
||||
for _, uc := range cfg.Upstream {
|
||||
endpointMap[uc.Endpoint] = struct{}{}
|
||||
}
|
||||
validate := validator.New()
|
||||
_ = ctrld.ValidateConfig(validate, cfg)
|
||||
for _, uc := range cfg.Upstream {
|
||||
if _, ok := endpointMap[uc.Endpoint]; !ok {
|
||||
t.Fatalf("expected endpoint '%s' to exist", uc.Endpoint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigDiscoverOverride(t *testing.T) {
|
||||
v := viper.NewWithOptions(viper.KeyDelimiter("::"))
|
||||
ctrld.InitConfig(v, "test_config_discover_override")
|
||||
@@ -179,6 +198,27 @@ func dohUpstreamEndpointWithoutScheme(t *testing.T) *ctrld.Config {
|
||||
return cfg
|
||||
}
|
||||
|
||||
func dohUpstreamEndpointWithoutType(t *testing.T) *ctrld.Config {
|
||||
cfg := defaultConfig(t)
|
||||
cfg.Upstream["0"].Endpoint = "https://freedns.controld.com/p1"
|
||||
cfg.Upstream["0"].Type = ""
|
||||
return cfg
|
||||
}
|
||||
|
||||
func doh3UpstreamEndpointWithoutType(t *testing.T) *ctrld.Config {
|
||||
cfg := defaultConfig(t)
|
||||
cfg.Upstream["0"].Endpoint = "h3://freedns.controld.com/p1"
|
||||
cfg.Upstream["0"].Type = ""
|
||||
return cfg
|
||||
}
|
||||
|
||||
func sdnsUpstreamEndpointWithoutType(t *testing.T) *ctrld.Config {
|
||||
cfg := defaultConfig(t)
|
||||
cfg.Upstream["0"].Endpoint = "sdns://AgMAAAAAAAAACjc2Ljc2LjIuMTEAFGZyZWVkbnMuY29udHJvbGQuY29tAy9wMQ"
|
||||
cfg.Upstream["0"].Type = ""
|
||||
return cfg
|
||||
}
|
||||
|
||||
func invalidUpstreamTimeout(t *testing.T) *ctrld.Config {
|
||||
cfg := defaultConfig(t)
|
||||
cfg.Upstream["0"].Timeout = -1
|
||||
@@ -268,6 +308,12 @@ func configWithInvalidLeaseFileFormat(t *testing.T) *ctrld.Config {
|
||||
return cfg
|
||||
}
|
||||
|
||||
func configWithDhcp4KeaFormat(t *testing.T) *ctrld.Config {
|
||||
cfg := defaultConfig(t)
|
||||
cfg.Service.DHCPLeaseFileFormat = "kea-dhcp4"
|
||||
return cfg
|
||||
}
|
||||
|
||||
func configWithInvalidDoHEndpoint(t *testing.T) *ctrld.Config {
|
||||
cfg := defaultConfig(t)
|
||||
cfg.Upstream["0"].Endpoint = "/1.1.1.1"
|
||||
|
||||
@@ -255,7 +255,7 @@ Specifying the `ip` and `port` of the Prometheus metrics server. The Prometheus
|
||||
### dns_watchdog_enabled
|
||||
Checking DNS changes to network interfaces and reverting to ctrld's own settings.
|
||||
|
||||
The DNS watchdog process only runs on Windows and MacOS.
|
||||
The DNS watchdog process only runs on Windows and MacOS, while in `--cd` mode.
|
||||
|
||||
- Type: boolean
|
||||
- Required: no
|
||||
@@ -281,6 +281,13 @@ The value must be a positive number, any invalid value will be ignored and defau
|
||||
- Required: no
|
||||
- Default: 3600
|
||||
|
||||
### leak_on_upstream_failure
|
||||
Once ctrld is "offline", mean ctrld could not connect to any upstream, next queries will be leaked to OS resolver.
|
||||
|
||||
- Type: boolean
|
||||
- Required: no
|
||||
- Default: true on Windows, MacOS and non-router Linux.
|
||||
|
||||
## Upstream
|
||||
The `[upstream]` section specifies the DNS upstream servers that `ctrld` will forward DNS requests to.
|
||||
|
||||
@@ -557,6 +564,12 @@ And within each policy, the rules are processed from top to bottom.
|
||||
- Required: no
|
||||
- Default: []
|
||||
|
||||
---
|
||||
|
||||
Note that the domain comparisons are done in case in-sensitive manner following [RFC 1034](https://datatracker.ietf.org/doc/html/rfc1034#section-3.1)
|
||||
|
||||
---
|
||||
|
||||
### macs:
|
||||
`macs` is the list of mac rules within the policy. Mac address value is case-insensitive.
|
||||
|
||||
|
||||
90
go.mod
90
go.mod
@@ -1,92 +1,102 @@
|
||||
module github.com/Control-D-Inc/ctrld
|
||||
|
||||
go 1.21
|
||||
go 1.23
|
||||
|
||||
toolchain go1.23.1
|
||||
|
||||
require (
|
||||
github.com/Masterminds/semver v1.5.0
|
||||
github.com/ameshkov/dnsstamps v1.0.3
|
||||
github.com/coreos/go-systemd/v22 v22.5.0
|
||||
github.com/cuonglm/osinfo v0.0.0-20230921071424-e0e1b1e0bbbf
|
||||
github.com/frankban/quicktest v1.14.5
|
||||
github.com/fsnotify/fsnotify v1.6.0
|
||||
github.com/docker/go-units v0.5.0
|
||||
github.com/frankban/quicktest v1.14.6
|
||||
github.com/fsnotify/fsnotify v1.7.0
|
||||
github.com/go-playground/validator/v10 v10.11.1
|
||||
github.com/godbus/dbus/v5 v5.1.0
|
||||
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.1
|
||||
github.com/illarion/gonotify v1.0.1
|
||||
github.com/insomniacslk/dhcp v0.0.0-20230407062729-974c6f05fe16
|
||||
github.com/illarion/gonotify/v2 v2.0.3
|
||||
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2
|
||||
github.com/jaytaylor/go-hostsfile v0.0.0-20220426042432-61485ac1fa6c
|
||||
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86
|
||||
github.com/kardianos/service v1.2.1
|
||||
github.com/mdlayher/ndp v1.0.1
|
||||
github.com/miekg/dns v1.1.55
|
||||
github.com/microsoft/wmi v0.24.5
|
||||
github.com/miekg/dns v1.1.58
|
||||
github.com/minio/selfupdate v0.6.0
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/pelletier/go-toml/v2 v2.0.8
|
||||
github.com/prometheus/client_golang v1.15.1
|
||||
github.com/prometheus/client_model v0.4.0
|
||||
github.com/prometheus/client_golang v1.19.1
|
||||
github.com/prometheus/client_model v0.5.0
|
||||
github.com/prometheus/prom2json v1.3.3
|
||||
github.com/quic-go/quic-go v0.42.0
|
||||
github.com/quic-go/quic-go v0.48.2
|
||||
github.com/rs/zerolog v1.28.0
|
||||
github.com/spf13/cobra v1.7.0
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.16.0
|
||||
github.com/stretchr/testify v1.8.3
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/vishvananda/netlink v1.2.1-beta.2
|
||||
golang.org/x/net v0.23.0
|
||||
golang.org/x/sync v0.2.0
|
||||
golang.org/x/sys v0.18.0
|
||||
golang.org/x/net v0.33.0
|
||||
golang.org/x/sync v0.10.0
|
||||
golang.org/x/sys v0.29.0
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3
|
||||
tailscale.com v1.44.0
|
||||
tailscale.com v1.74.0
|
||||
)
|
||||
|
||||
require (
|
||||
aead.dev/minisign v0.2.0 // indirect
|
||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect
|
||||
github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 // indirect
|
||||
github.com/go-ole/go-ole v1.3.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-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jsimonetti/rtnetlink v1.3.2 // indirect
|
||||
github.com/jsimonetti/rtnetlink v1.4.0 // indirect
|
||||
github.com/kr/pretty v0.3.1 // 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/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.18 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 // indirect
|
||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||
github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 // indirect
|
||||
github.com/mdlayher/socket v0.4.1 // indirect
|
||||
github.com/mdlayher/packet v1.1.2 // indirect
|
||||
github.com/mdlayher/socket v0.5.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.17 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/common v0.44.0 // indirect
|
||||
github.com/prometheus/procfs v0.9.0 // indirect
|
||||
github.com/quic-go/qpack v0.4.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/common v0.48.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/rogpeppe/go-internal v1.10.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||
github.com/spf13/afero v1.9.5 // indirect
|
||||
github.com/spf13/cast v1.5.1 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 // indirect
|
||||
github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e // indirect
|
||||
github.com/vishvananda/netns v0.0.4 // indirect
|
||||
go.uber.org/mock v0.4.0 // indirect
|
||||
go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect
|
||||
golang.org/x/crypto v0.21.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 // indirect
|
||||
golang.org/x/mod v0.11.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/tools v0.9.1 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
|
||||
golang.org/x/mod v0.19.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/tools v0.23.0 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
@@ -94,4 +104,4 @@ require (
|
||||
|
||||
replace github.com/mr-karan/doggo => github.com/Windscribe/doggo v0.0.0-20220919152748-2c118fc391f8
|
||||
|
||||
replace github.com/rs/zerolog => github.com/Windscribe/zerolog v0.0.0-20230503170159-e6aa153233be
|
||||
replace github.com/rs/zerolog => github.com/Windscribe/zerolog v0.0.0-20241206130353-cc6e8ef5397c
|
||||
|
||||
199
go.sum
199
go.sum
@@ -42,10 +42,12 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
||||
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||
github.com/Windscribe/zerolog v0.0.0-20230503170159-e6aa153233be h1:qBKVRi7Mom5heOkyZ+NCIu9HZBiNCsRqrRe5t9pooik=
|
||||
github.com/Windscribe/zerolog v0.0.0-20230503170159-e6aa153233be/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w=
|
||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA=
|
||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/Windscribe/zerolog v0.0.0-20241206130353-cc6e8ef5397c h1:UqFsxmwiCh/DBvwJB0m7KQ2QFDd6DdUkosznfMppdhE=
|
||||
github.com/Windscribe/zerolog v0.0.0-20241206130353-cc6e8ef5397c/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo=
|
||||
github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
@@ -54,36 +56,45 @@ github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cilium/ebpf v0.10.0 h1:nk5HPMeoBXtOzbkZBWym+ZWq1GIiHUsBFXxwewXAHLQ=
|
||||
github.com/cilium/ebpf v0.10.0/go.mod h1:DPiVdY/kT534dgc9ERmvP8mWA+9gvwgKfRvk4nNWnoE=
|
||||
github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk=
|
||||
github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/cuonglm/osinfo v0.0.0-20230921071424-e0e1b1e0bbbf h1:40DHYsri+d1bnroFDU2FQAeq68f3kAlOzlQ93kCf26Q=
|
||||
github.com/cuonglm/osinfo v0.0.0-20230921071424-e0e1b1e0bbbf/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=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk=
|
||||
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA=
|
||||
github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 h1:ymLjT4f35nQbASLnvxEde4XOBL+Sn7rFuV+FOJqkljg=
|
||||
github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0/go.mod h1:6daplAwHHGbUGib4990V3Il26O0OC4aRyvewaaAihaA=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
|
||||
@@ -95,8 +106,8 @@ github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg=
|
||||
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
@@ -122,9 +133,8 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
@@ -136,9 +146,9 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
@@ -152,10 +162,12 @@ 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/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
|
||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||
@@ -169,19 +181,19 @@ github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8
|
||||
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/illarion/gonotify v1.0.1 h1:F1d+0Fgbq/sDWjj/r66ekjDG+IDeecQKUFH4wNwsoio=
|
||||
github.com/illarion/gonotify v1.0.1/go.mod h1:zt5pmDofZpU1f8aqlK0+95eQhoEAn/d4G4B/FjVW4jE=
|
||||
github.com/illarion/gonotify/v2 v2.0.3 h1:B6+SKPo/0Sw8cRJh1aLzNEeNVFfzE3c6N+o+vyxM+9A=
|
||||
github.com/illarion/gonotify/v2 v2.0.3/go.mod h1:38oIJTgFqupkEydkkClkbL6i5lXV/bxdH9do5TALPEE=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20230407062729-974c6f05fe16 h1:+aAGyK41KRn8jbF2Q7PLL0Sxwg6dShGcQSeCC7nZQ8E=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20230407062729-974c6f05fe16/go.mod h1:IKrnDWs3/Mqq5n0lI+RxA2sB7MvN/vbMBP3ehXg65UI=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI=
|
||||
github.com/jaytaylor/go-hostsfile v0.0.0-20220426042432-61485ac1fa6c h1:kbTQ8oGf+BVFvt/fM+ECI+NbZDCqoi0vtZTfB2p2hrI=
|
||||
github.com/jaytaylor/go-hostsfile v0.0.0-20220426042432-61485ac1fa6c/go.mod h1:k6+89xKz7BSMJ+DzIerBdtpEUeTlBMugO/hcVSzahog=
|
||||
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 v1.3.2 h1:dcn0uWkfxycEEyNy0IGfx3GrhQ38LH7odjxAghimsVI=
|
||||
github.com/jsimonetti/rtnetlink v1.3.2/go.mod h1:BBu4jZCpTjP6Gk0/wfrO8qcqymnN3g0hoFqObRmUo6U=
|
||||
github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I=
|
||||
github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/kardianos/service v1.2.1 h1:AYndMsehS+ywIS6RB9KOlcXzteWUzxgMgBymJD7+BYk=
|
||||
@@ -201,31 +213,29 @@ 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/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
|
||||
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 h1:lez6TS6aAau+8wXUP3G9I3TGlmPFEq2CTxBaRqY6AGE=
|
||||
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y=
|
||||
github.com/mdlayher/ndp v1.0.1 h1:+yAD79/BWyFlvAoeG5ncPS0ItlHP/eVbH7bQ6/+LVA4=
|
||||
github.com/mdlayher/ndp v1.0.1/go.mod h1:rf3wKaWhAYJEXFKpgF8kQ2AxypxVbfNcZbqoAo6fVzk=
|
||||
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
|
||||
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
||||
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.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
|
||||
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
|
||||
github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
|
||||
github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
|
||||
github.com/mdlayher/packet v1.1.2 h1:3Up1NG6LZrsgDVn6X4L9Ge/iyRyxFEFD9o6Pr3Q1nQY=
|
||||
github.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU+x0kew4=
|
||||
github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI=
|
||||
github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI=
|
||||
github.com/microsoft/wmi v0.24.5 h1:NT+WqhjKbEcg3ldmDsRMarWgHGkpeW+gMopSCfON0kM=
|
||||
github.com/microsoft/wmi v0.24.5/go.mod h1:1zbdSF0A+5OwTUII5p3hN7/K6KF2m3o27pSG6Y51VU8=
|
||||
github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
|
||||
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
|
||||
github.com/minio/selfupdate v0.6.0 h1:i76PgT0K5xO9+hjzKcacQtO7+MjJ4JKA8Ak8XQ9DDwU=
|
||||
github.com/minio/selfupdate v0.6.0/go.mod h1:bO02GTIPCMQFTEvE5h4DjYB58bCoZ35XLeBf0buTDdM=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
@@ -239,28 +249,30 @@ github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+q
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||
github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc=
|
||||
github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
||||
github.com/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_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI=
|
||||
github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
|
||||
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
|
||||
github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
|
||||
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
|
||||
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
|
||||
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
|
||||
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
|
||||
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/prometheus/prom2json v1.3.3 h1:IYfSMiZ7sSOfliBoo89PcufjWO4eAR0gznGcETyaUgo=
|
||||
github.com/prometheus/prom2json v1.3.3/go.mod h1:Pv4yIPktEkK7btWsrUTWDDDrnpUrAELaOCj+oFwlgmc=
|
||||
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/quic-go v0.42.0 h1:uSfdap0eveIl8KXnipv9K7nlwZ5IqLlYOpJ58u5utpM=
|
||||
github.com/quic-go/quic-go v0.42.0/go.mod h1:132kz4kL3F9vxhW3CtQJLDVwcFe5wdWeJXXijhsO57M=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE=
|
||||
github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
@@ -268,16 +280,16 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
|
||||
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
|
||||
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
|
||||
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
|
||||
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
||||
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
@@ -286,8 +298,9 @@ github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc=
|
||||
github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
@@ -295,12 +308,13 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
|
||||
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||
github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 h1:YcojQL98T/OO+rybuzn2+5KrD5dBwXIvYBvQ2cD3Avg=
|
||||
github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
|
||||
github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e h1:BA9O3BmlTmpjbvajAwzWx4Wo2TRVdpPXZEeemGQcajw=
|
||||
github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
|
||||
github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs=
|
||||
github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
|
||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
||||
@@ -320,6 +334,8 @@ go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
||||
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||
go4.org/mem v0.0.0-20220726221520-4f986261bf13 h1:CbZeCBZ0aZj8EfVgnqQcYZgf0lpZ3H9rmp5nkDTAst8=
|
||||
go4.org/mem v0.0.0-20220726221520-4f986261bf13/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
@@ -330,8 +346,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm
|
||||
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
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=
|
||||
@@ -342,8 +358,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-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o=
|
||||
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
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=
|
||||
@@ -367,15 +383,14 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
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.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
|
||||
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
|
||||
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
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-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=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
@@ -402,8 +417,8 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
|
||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
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=
|
||||
@@ -423,16 +438,14 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
|
||||
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -467,16 +480,16 @@ golang.org/x/sys v0.0.0-20210228012217-479acdf4ea46/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
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-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=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -487,8 +500,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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
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=
|
||||
@@ -541,8 +554,8 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f
|
||||
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.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo=
|
||||
golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
|
||||
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
|
||||
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
|
||||
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=
|
||||
@@ -637,8 +650,6 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -663,5 +674,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.44.0 h1:MPos9n30kJvdyfL52045gVFyNg93K+bwgDsr8gqKq2o=
|
||||
tailscale.com v1.44.0/go.mod h1:+iYwTdeHyVJuNDu42Zafwihq1Uqfh+pW7pRaY1GD328=
|
||||
tailscale.com v1.74.0 h1:J+vRN9o3D4wCqZBiwvDg9kZpQag2mG4Xz5RXNpmV3KE=
|
||||
tailscale.com v1.74.0/go.mod h1:3iACpCONQ4lauDXvwfoGlwNCpfbVxjdc2j6G9EuFOW8=
|
||||
|
||||
@@ -77,6 +77,7 @@ type Table struct {
|
||||
hostnameResolvers []HostnameResolver
|
||||
refreshers []refresher
|
||||
initOnce sync.Once
|
||||
stopOnce sync.Once
|
||||
refreshInterval int
|
||||
|
||||
dhcp *dhcp
|
||||
@@ -90,7 +91,9 @@ type Table struct {
|
||||
vni *virtualNetworkIface
|
||||
svcCfg ctrld.ServiceConfig
|
||||
quitCh chan struct{}
|
||||
stopCh chan struct{}
|
||||
selfIP string
|
||||
selfIPLock sync.RWMutex
|
||||
cdUID string
|
||||
ptrNameservers []string
|
||||
}
|
||||
@@ -103,6 +106,7 @@ func NewTable(cfg *ctrld.Config, selfIP, cdUID string, ns []string) *Table {
|
||||
return &Table{
|
||||
svcCfg: cfg.Service,
|
||||
quitCh: make(chan struct{}),
|
||||
stopCh: make(chan struct{}),
|
||||
selfIP: selfIP,
|
||||
cdUID: cdUID,
|
||||
ptrNameservers: ns,
|
||||
@@ -120,24 +124,59 @@ func (t *Table) AddLeaseFile(name string, format ctrld.LeaseFileFormat) {
|
||||
// RefreshLoop runs all the refresher to update new client info data.
|
||||
func (t *Table) RefreshLoop(ctx context.Context) {
|
||||
timer := time.NewTicker(time.Second * time.Duration(t.refreshInterval))
|
||||
defer timer.Stop()
|
||||
defer func() {
|
||||
timer.Stop()
|
||||
close(t.quitCh)
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case <-timer.C:
|
||||
for _, r := range t.refreshers {
|
||||
_ = r.refresh()
|
||||
}
|
||||
t.Refresh()
|
||||
case <-t.stopCh:
|
||||
return
|
||||
case <-ctx.Done():
|
||||
close(t.quitCh)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Init initializes all client info discovers.
|
||||
func (t *Table) Init() {
|
||||
t.initOnce.Do(t.init)
|
||||
}
|
||||
|
||||
// Refresh forces all discovers to retrieve new data.
|
||||
func (t *Table) Refresh() {
|
||||
for _, r := range t.refreshers {
|
||||
_ = r.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
// Stop stops all the discovers.
|
||||
// It blocks until all the discovers done.
|
||||
func (t *Table) Stop() {
|
||||
t.stopOnce.Do(func() {
|
||||
close(t.stopCh)
|
||||
})
|
||||
<-t.quitCh
|
||||
}
|
||||
|
||||
// SelfIP returns the selfIP value of the Table in a thread-safe manner.
|
||||
func (t *Table) SelfIP() string {
|
||||
t.selfIPLock.RLock()
|
||||
defer t.selfIPLock.RUnlock()
|
||||
return t.selfIP
|
||||
}
|
||||
|
||||
// SetSelfIP sets the selfIP value of the Table in a thread-safe manner.
|
||||
func (t *Table) SetSelfIP(ip string) {
|
||||
t.selfIPLock.Lock()
|
||||
defer t.selfIPLock.Unlock()
|
||||
t.selfIP = ip
|
||||
t.dhcp.selfIP = t.selfIP
|
||||
t.dhcp.addSelf()
|
||||
}
|
||||
|
||||
func (t *Table) init() {
|
||||
// Custom client ID presents, use it as the only source.
|
||||
if _, clientID := controld.ParseRawUID(t.cdUID); clientID != "" {
|
||||
@@ -381,9 +420,7 @@ func (t *Table) lookupHostnameAll(ip, mac string) []*hostnameEntry {
|
||||
|
||||
// ListClients returns list of clients discovered by ctrld.
|
||||
func (t *Table) ListClients() []*Client {
|
||||
for _, r := range t.refreshers {
|
||||
_ = r.refresh()
|
||||
}
|
||||
t.Refresh()
|
||||
ipMap := make(map[string]*Client)
|
||||
il := []ipLister{t.dhcp, t.arp, t.ndp, t.ptr, t.mdns, t.vni}
|
||||
for _, ir := range il {
|
||||
|
||||
@@ -13,8 +13,9 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"tailscale.com/net/netmon"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/util/lineread"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld"
|
||||
@@ -356,7 +357,7 @@ func (d *dhcp) addSelf() {
|
||||
d.ip2name.Store(ipV4Loopback, hostname)
|
||||
d.ip2name.Store(ipv6Loopback, hostname)
|
||||
found := false
|
||||
interfaces.ForeachInterface(func(i interfaces.Interface, prefixes []netip.Prefix) {
|
||||
netmon.ForeachInterface(func(i netmon.Interface, prefixes []netip.Prefix) {
|
||||
mac := i.HardwareAddr.String()
|
||||
// Skip loopback interfaces, info was stored above.
|
||||
if mac == "" {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
@@ -24,8 +25,12 @@ import (
|
||||
const (
|
||||
apiDomainCom = "api.controld.com"
|
||||
apiDomainDev = "api.controld.dev"
|
||||
resolverDataURLCom = "https://api.controld.com/utility"
|
||||
resolverDataURLDev = "https://api.controld.dev/utility"
|
||||
apiURLCom = "https://api.controld.com"
|
||||
apiURLDev = "https://api.controld.dev"
|
||||
resolverDataURLCom = apiURLCom + "/utility"
|
||||
resolverDataURLDev = apiURLDev + "/utility"
|
||||
logURLCom = apiURLCom + "/logs"
|
||||
logURLDev = apiURLDev + "/logs"
|
||||
InvalidConfigCode = 40402
|
||||
)
|
||||
|
||||
@@ -48,14 +53,14 @@ type utilityResponse struct {
|
||||
} `json:"body"`
|
||||
}
|
||||
|
||||
type UtilityErrorResponse struct {
|
||||
type ErrorResponse struct {
|
||||
ErrorField struct {
|
||||
Message string `json:"message"`
|
||||
Code int `json:"code"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
func (u UtilityErrorResponse) Error() string {
|
||||
func (u ErrorResponse) Error() string {
|
||||
return u.ErrorField.Message
|
||||
}
|
||||
|
||||
@@ -64,11 +69,18 @@ type utilityRequest struct {
|
||||
ClientID string `json:"client_id,omitempty"`
|
||||
}
|
||||
|
||||
type utilityOrgRequest struct {
|
||||
// UtilityOrgRequest contains request data for calling Org API.
|
||||
type UtilityOrgRequest struct {
|
||||
ProvToken string `json:"prov_token"`
|
||||
Hostname string `json:"hostname"`
|
||||
}
|
||||
|
||||
// LogsRequest contains request data for sending runtime logs to API.
|
||||
type LogsRequest struct {
|
||||
UID string `json:"uid"`
|
||||
Data io.ReadCloser `json:"-"`
|
||||
}
|
||||
|
||||
// FetchResolverConfig fetch Control D config for given uid.
|
||||
func FetchResolverConfig(rawUID, version string, cdDev bool) (*ResolverConfig, error) {
|
||||
uid, clientID := ParseRawUID(rawUID)
|
||||
@@ -81,9 +93,15 @@ func FetchResolverConfig(rawUID, version string, cdDev bool) (*ResolverConfig, e
|
||||
}
|
||||
|
||||
// FetchResolverUID fetch resolver uid from provision token.
|
||||
func FetchResolverUID(pt, version string, cdDev bool) (*ResolverConfig, error) {
|
||||
hostname, _ := os.Hostname()
|
||||
body, _ := json.Marshal(utilityOrgRequest{ProvToken: pt, Hostname: hostname})
|
||||
func FetchResolverUID(req *UtilityOrgRequest, version string, cdDev bool) (*ResolverConfig, error) {
|
||||
if req == nil {
|
||||
return nil, errors.New("invalid request")
|
||||
}
|
||||
hostname := req.Hostname
|
||||
if hostname == "" {
|
||||
hostname, _ = os.Hostname()
|
||||
}
|
||||
body, _ := json.Marshal(UtilityOrgRequest{ProvToken: req.ProvToken, Hostname: hostname})
|
||||
return postUtilityAPI(version, cdDev, false, bytes.NewReader(body))
|
||||
}
|
||||
|
||||
@@ -115,6 +133,81 @@ func postUtilityAPI(version string, cdDev, lastUpdatedFailed bool, body io.Reade
|
||||
}
|
||||
req.URL.RawQuery = q.Encode()
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
transport := apiTransport(cdDev)
|
||||
client := http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
Transport: transport,
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("postUtilityAPI client.Do: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
d := json.NewDecoder(resp.Body)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
errResp := &ErrorResponse{}
|
||||
if err := d.Decode(errResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, errResp
|
||||
}
|
||||
|
||||
ur := &utilityResponse{}
|
||||
if err := d.Decode(ur); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ur.Body.Resolver, nil
|
||||
}
|
||||
|
||||
// SendLogs sends runtime log to ControlD API.
|
||||
func SendLogs(lr *LogsRequest, cdDev bool) error {
|
||||
defer lr.Data.Close()
|
||||
apiUrl := logURLCom
|
||||
if cdDev {
|
||||
apiUrl = logURLDev
|
||||
}
|
||||
req, err := http.NewRequest("POST", apiUrl, lr.Data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("http.NewRequest: %w", err)
|
||||
}
|
||||
q := req.URL.Query()
|
||||
q.Set("uid", lr.UID)
|
||||
req.URL.RawQuery = q.Encode()
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
transport := apiTransport(cdDev)
|
||||
client := http.Client{
|
||||
Timeout: 300 * time.Second,
|
||||
Transport: transport,
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SendLogs client.Do: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
d := json.NewDecoder(resp.Body)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
errResp := &ErrorResponse{}
|
||||
if err := d.Decode(errResp); err != nil {
|
||||
return err
|
||||
}
|
||||
return errResp
|
||||
}
|
||||
_, _ = io.Copy(io.Discard, resp.Body)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseRawUID parse the input raw UID, returning real UID and ClientID.
|
||||
// The raw UID can have 2 forms:
|
||||
//
|
||||
// - <uid>
|
||||
// - <uid>/<client_id>
|
||||
func ParseRawUID(rawUID string) (string, string) {
|
||||
uid, clientID, _ := strings.Cut(rawUID, "/")
|
||||
return uid, clientID
|
||||
}
|
||||
|
||||
// apiTransport returns an HTTP transport for connecting to ControlD API endpoint.
|
||||
func apiTransport(cdDev bool) *http.Transport {
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
apiDomain := apiDomainCom
|
||||
@@ -135,41 +228,8 @@ func postUtilityAPI(version string, cdDev, lastUpdatedFailed bool, body io.Reade
|
||||
d := &ctrldnet.ParallelDialer{}
|
||||
return d.DialContext(ctx, network, addrs)
|
||||
}
|
||||
|
||||
if router.Name() == ddwrt.Name || runtime.GOOS == "android" {
|
||||
transport.TLSClientConfig = &tls.Config{RootCAs: certs.CACertPool()}
|
||||
}
|
||||
client := http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
Transport: transport,
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("client.Do: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
d := json.NewDecoder(resp.Body)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
errResp := &UtilityErrorResponse{}
|
||||
if err := d.Decode(errResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, errResp
|
||||
}
|
||||
|
||||
ur := &utilityResponse{}
|
||||
if err := d.Decode(ur); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ur.Body.Resolver, nil
|
||||
}
|
||||
|
||||
// ParseRawUID parse the input raw UID, returning real UID and ClientID.
|
||||
// The raw UID can have 2 forms:
|
||||
//
|
||||
// - <uid>
|
||||
// - <uid>/<client_id>
|
||||
func ParseRawUID(rawUID string) (string, string) {
|
||||
uid, clientID, _ := strings.Cut(rawUID, "/")
|
||||
return uid, clientID
|
||||
return transport
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
This is a fork of https://pkg.go.dev/tailscale.com@v1.34.2/net/dns with modification
|
||||
This is a fork of https://pkg.go.dev/tailscale.com@v1.74.0/net/dns with modification
|
||||
to fit ctrld use case.
|
||||
@@ -1,12 +1,12 @@
|
||||
// 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.
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux || freebsd || openbsd
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
@@ -33,7 +33,7 @@ var workaroundScript []byte
|
||||
// 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"
|
||||
const resolvconfConfigName = "tun-ctrld.inet"
|
||||
|
||||
// resolvconfLibcHookPath is the directory containing libc update
|
||||
// scripts, which are run by Debian resolvconf when /etc/resolv.conf
|
||||
@@ -53,8 +53,6 @@ type resolvconfManager struct {
|
||||
scriptInstalled bool // libc update script has been installed
|
||||
}
|
||||
|
||||
var _ OSConfigurator = (*resolvconfManager)(nil)
|
||||
|
||||
func newDebianResolvconfManager(logf logger.Logf) (*resolvconfManager, error) {
|
||||
ret := &resolvconfManager{
|
||||
logf: logf,
|
||||
@@ -135,6 +133,43 @@ func (m *resolvconfManager) SetDNS(config OSConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *resolvconfManager) SupportsSplitDNS() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *resolvconfManager) GetBaseConfig() (OSConfig, error) {
|
||||
var bs bytes.Buffer
|
||||
|
||||
cmd := exec.Command(m.listRecordsPath)
|
||||
// list-records assumes it's being run with CWD set to the
|
||||
// interfaces runtime dir, and returns nonsense otherwise.
|
||||
cmd.Dir = m.interfacesDir
|
||||
cmd.Stdout = &bs
|
||||
if err := cmd.Run(); err != nil {
|
||||
return OSConfig{}, err
|
||||
}
|
||||
|
||||
var conf bytes.Buffer
|
||||
sc := bufio.NewScanner(&bs)
|
||||
for sc.Scan() {
|
||||
if sc.Text() == resolvconfConfigName {
|
||||
continue
|
||||
}
|
||||
bs, err := os.ReadFile(filepath.Join(m.interfacesDir, sc.Text()))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// Probably raced with a deletion, that's okay.
|
||||
continue
|
||||
}
|
||||
return OSConfig{}, err
|
||||
}
|
||||
conf.Write(bs)
|
||||
conf.WriteByte('\n')
|
||||
}
|
||||
|
||||
return readResolv(&conf)
|
||||
}
|
||||
|
||||
func (m *resolvconfManager) Close() error {
|
||||
if err := m.deleteCtrldConfig(); err != nil {
|
||||
return err
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
// 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.
|
||||
|
||||
//lint:file-ignore U1000 Ignore, this file is forked from upstream code.
|
||||
//lint:file-ignore ST1005 Ignore, this file is forked from upstream code.
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package dns
|
||||
|
||||
@@ -20,11 +16,13 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/version/distro"
|
||||
@@ -32,11 +30,6 @@ import (
|
||||
"github.com/Control-D-Inc/ctrld/internal/dns/resolvconffile"
|
||||
)
|
||||
|
||||
const (
|
||||
backupConf = "/etc/resolv.pre-ctrld-backup.conf"
|
||||
resolvConf = "/etc/resolv.conf"
|
||||
)
|
||||
|
||||
// writeResolvConf writes DNS configuration in resolv.conf format to the given writer.
|
||||
func writeResolvConf(w io.Writer, servers []netip.Addr, domains []dnsname.FQDN) error {
|
||||
c := &resolvconffile.Config{
|
||||
@@ -60,6 +53,8 @@ func readResolv(r io.Reader) (OSConfig, error) {
|
||||
// resolvOwner returns the apparent owner of the resolv.conf
|
||||
// configuration in bs - one of "resolvconf", "systemd-resolved" or
|
||||
// "NetworkManager", or "" if no known owner was found.
|
||||
//
|
||||
//lint:ignore U1000 used in linux and freebsd code
|
||||
func resolvOwner(bs []byte) string {
|
||||
likely := ""
|
||||
b := bytes.NewBuffer(bs)
|
||||
@@ -123,8 +118,9 @@ func restartResolved() error {
|
||||
// The caller must call Down before program shutdown
|
||||
// or as cleanup if the program terminates unexpectedly.
|
||||
type directManager struct {
|
||||
logf logger.Logf
|
||||
fs wholeFileFS
|
||||
logf logger.Logf
|
||||
health *health.Tracker
|
||||
fs wholeFileFS
|
||||
// renameBroken is set if fs.Rename to or from /etc/resolv.conf
|
||||
// fails. This can happen in some container runtimes, where
|
||||
// /etc/resolv.conf is bind-mounted from outside the container,
|
||||
@@ -140,19 +136,22 @@ type directManager struct {
|
||||
ctx context.Context // valid until Close
|
||||
ctxClose context.CancelFunc // closes ctx
|
||||
|
||||
mu sync.Mutex
|
||||
wantResolvConf []byte // if non-nil, what we expect /etc/resolv.conf to contain
|
||||
mu sync.Mutex
|
||||
wantResolvConf []byte // if non-nil, what we expect /etc/resolv.conf to contain
|
||||
//lint:ignore U1000 used in direct_linux.go
|
||||
lastWarnContents []byte // last resolv.conf contents that we warned about
|
||||
}
|
||||
|
||||
func newDirectManager(logf logger.Logf) *directManager {
|
||||
return newDirectManagerOnFS(logf, directFS{})
|
||||
//lint:ignore U1000 used in manager_{freebsd,openbsd}.go
|
||||
func newDirectManager(logf logger.Logf, health *health.Tracker) *directManager {
|
||||
return newDirectManagerOnFS(logf, health, directFS{})
|
||||
}
|
||||
|
||||
func newDirectManagerOnFS(logf logger.Logf, fs wholeFileFS) *directManager {
|
||||
func newDirectManagerOnFS(logf logger.Logf, health *health.Tracker, fs wholeFileFS) *directManager {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
m := &directManager{
|
||||
logf: logf,
|
||||
health: health,
|
||||
fs: fs,
|
||||
ctx: ctx,
|
||||
ctxClose: cancel,
|
||||
@@ -193,13 +192,13 @@ func (m *directManager) ownedByCtrld() (bool, error) {
|
||||
}
|
||||
|
||||
// backupConfig creates or updates a backup of /etc/resolv.conf, if
|
||||
// resolv.conf does not currently contain a Tailscale-managed config.
|
||||
// resolv.conf does not currently contain a ctrld-managed config.
|
||||
func (m *directManager) backupConfig() error {
|
||||
if _, err := m.fs.Stat(resolvConf); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// No resolv.conf, nothing to back up. Also get rid of any
|
||||
// existing backup file, to avoid restoring something old.
|
||||
_ = m.fs.Remove(backupConf)
|
||||
m.fs.Remove(backupConf)
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
@@ -237,7 +236,7 @@ func (m *directManager) restoreBackup() (restored bool, err error) {
|
||||
if resolvConfExists && !owned {
|
||||
// There's already a non-ctrld config in place, get rid of
|
||||
// our backup.
|
||||
_ = m.fs.Remove(backupConf)
|
||||
m.fs.Remove(backupConf)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -278,6 +277,14 @@ func (m *directManager) rename(old, new string) error {
|
||||
return fmt.Errorf("writing to %q in rename of %q: %w", new, old, err)
|
||||
}
|
||||
|
||||
// Explicitly set the permissions on the new file. This ensures that
|
||||
// if we have a umask set which prevents creating world-readable files,
|
||||
// the file will still have the correct permissions once it's renamed
|
||||
// into place. See #12609.
|
||||
if err := m.fs.Chmod(new, 0644); err != nil {
|
||||
return fmt.Errorf("chmod %q in rename of %q: %w", new, old, err)
|
||||
}
|
||||
|
||||
if err := m.fs.Remove(old); err != nil {
|
||||
err2 := m.fs.Truncate(old)
|
||||
if err2 != nil {
|
||||
@@ -298,53 +305,6 @@ func (m *directManager) setWant(want []byte) {
|
||||
m.wantResolvConf = want
|
||||
}
|
||||
|
||||
var warnTrample = health.NewWarnable()
|
||||
|
||||
// checkForFileTrample checks whether /etc/resolv.conf has been trampled
|
||||
// by another program on the system. (e.g. a DHCP client)
|
||||
func (m *directManager) checkForFileTrample() {
|
||||
m.mu.Lock()
|
||||
want := m.wantResolvConf
|
||||
lastWarn := m.lastWarnContents
|
||||
m.mu.Unlock()
|
||||
|
||||
if want == nil {
|
||||
return
|
||||
}
|
||||
|
||||
cur, err := m.fs.ReadFile(resolvConf)
|
||||
if err != nil {
|
||||
m.logf("trample: read error: %v", err)
|
||||
return
|
||||
}
|
||||
if bytes.Equal(cur, want) {
|
||||
warnTrample.Set(nil)
|
||||
if lastWarn != nil {
|
||||
m.mu.Lock()
|
||||
m.lastWarnContents = nil
|
||||
m.mu.Unlock()
|
||||
m.logf("trample: resolv.conf again matches expected content")
|
||||
}
|
||||
return
|
||||
}
|
||||
if bytes.Equal(cur, lastWarn) {
|
||||
// We already logged about this, so not worth doing it again.
|
||||
return
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
m.lastWarnContents = cur
|
||||
m.mu.Unlock()
|
||||
|
||||
show := cur
|
||||
if len(show) > 1024 {
|
||||
show = show[:1024]
|
||||
}
|
||||
m.logf("trample: resolv.conf changed from what we expected. did some other program interfere? current contents: %q", show)
|
||||
//lint:ignore ST1005 This error is for human.
|
||||
warnTrample.Set(errors.New("Linux DNS config not ideal. /etc/resolv.conf overwritten. See https://tailscale.com/s/dns-fight"))
|
||||
}
|
||||
|
||||
func (m *directManager) SetDNS(config OSConfig) (err error) {
|
||||
defer func() {
|
||||
if err != nil && errors.Is(err, fs.ErrPermission) && runtime.GOOS == "linux" &&
|
||||
@@ -370,7 +330,7 @@ func (m *directManager) SetDNS(config OSConfig) (err error) {
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
_ = writeResolvConf(buf, config.Nameservers, config.SearchDomains)
|
||||
writeResolvConf(buf, config.Nameservers, config.SearchDomains)
|
||||
if err := m.atomicWriteFile(m.fs, resolvConf, buf.Bytes(), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -411,12 +371,57 @@ func (m *directManager) SetDNS(config OSConfig) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *directManager) SupportsSplitDNS() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *directManager) GetBaseConfig() (OSConfig, error) {
|
||||
owned, err := m.ownedByCtrld()
|
||||
if err != nil {
|
||||
return OSConfig{}, err
|
||||
}
|
||||
fileToRead := resolvConf
|
||||
if owned {
|
||||
fileToRead = backupConf
|
||||
}
|
||||
|
||||
oscfg, err := m.readResolvFile(fileToRead)
|
||||
if err != nil {
|
||||
return OSConfig{}, err
|
||||
}
|
||||
|
||||
// On some systems, the backup configuration file is actually a
|
||||
// symbolic link to something owned by another DNS service (commonly,
|
||||
// resolved). Thus, it can be updated out from underneath us to contain
|
||||
// the Tailscale service IP, which results in an infinite loop of us
|
||||
// trying to send traffic to resolved, which sends back to us, and so
|
||||
// on. To solve this, drop the Tailscale service IP from the base
|
||||
// configuration; we do this in all situations since there's
|
||||
// essentially no world where we want to forward to ourselves.
|
||||
//
|
||||
// See: https://github.com/tailscale/tailscale/issues/7816
|
||||
var removed bool
|
||||
oscfg.Nameservers = slices.DeleteFunc(oscfg.Nameservers, func(ip netip.Addr) bool {
|
||||
if ip == tsaddr.TailscaleServiceIP() || ip == tsaddr.TailscaleServiceIPv6() {
|
||||
removed = true
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
if removed {
|
||||
m.logf("[v1] dropped Tailscale IP from base config that was a symlink")
|
||||
}
|
||||
return oscfg, nil
|
||||
}
|
||||
|
||||
func (m *directManager) Close() error {
|
||||
// We used to keep a file for the ctrld config and symlinked
|
||||
m.ctxClose()
|
||||
|
||||
// We used to keep a file for the tailscale config and symlinked
|
||||
// to it, but then we stopped because /etc/resolv.conf being a
|
||||
// symlink to surprising places breaks snaps and other sandboxing
|
||||
// things. Clean it up if it's still there.
|
||||
_ = m.fs.Remove("/etc/resolv.ctrld.conf")
|
||||
m.fs.Remove("/etc/resolv.ctrld.conf")
|
||||
|
||||
if _, err := m.fs.Stat(backupConf); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
@@ -436,9 +441,9 @@ func (m *directManager) Close() error {
|
||||
resolvConfExists := !os.IsNotExist(err)
|
||||
|
||||
if resolvConfExists && !owned {
|
||||
// There's already a non-ctrld config in place, get rid of
|
||||
// There's already a non-tailscale config in place, get rid of
|
||||
// our backup.
|
||||
_ = m.fs.Remove(backupConf)
|
||||
m.fs.Remove(backupConf)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -475,6 +480,14 @@ func (m *directManager) atomicWriteFile(fs wholeFileFS, filename string, data []
|
||||
if err := fs.WriteFile(tmpName, data, perm); err != nil {
|
||||
return fmt.Errorf("atomicWriteFile: %w", err)
|
||||
}
|
||||
// Explicitly set the permissions on the temporary file before renaming
|
||||
// it. This ensures that if we have a umask set which prevents creating
|
||||
// world-readable files, the file will still have the correct
|
||||
// permissions once it's renamed into place. See #12609.
|
||||
if err := fs.Chmod(tmpName, perm); err != nil {
|
||||
return fmt.Errorf("atomicWriteFile: Chmod: %w", err)
|
||||
}
|
||||
|
||||
return m.rename(tmpName, filename)
|
||||
}
|
||||
|
||||
@@ -483,10 +496,11 @@ func (m *directManager) atomicWriteFile(fs wholeFileFS, filename string, data []
|
||||
//
|
||||
// All name parameters are absolute paths.
|
||||
type wholeFileFS interface {
|
||||
Stat(name string) (isRegular bool, err error)
|
||||
Rename(oldName, newName string) error
|
||||
Remove(name string) error
|
||||
Chmod(name string, mode os.FileMode) error
|
||||
ReadFile(name string) ([]byte, error)
|
||||
Remove(name string) error
|
||||
Rename(oldName, newName string) error
|
||||
Stat(name string) (isRegular bool, err error)
|
||||
Truncate(name string) error
|
||||
WriteFile(name string, contents []byte, perm os.FileMode) error
|
||||
}
|
||||
@@ -510,6 +524,10 @@ func (fs directFS) Stat(name string) (isRegular bool, err error) {
|
||||
return fi.Mode().IsRegular(), nil
|
||||
}
|
||||
|
||||
func (fs directFS) Chmod(name string, mode os.FileMode) error {
|
||||
return os.Chmod(fs.path(name), mode)
|
||||
}
|
||||
|
||||
func (fs directFS) Rename(oldName, newName string) error {
|
||||
return os.Rename(fs.path(oldName), fs.path(newName))
|
||||
}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
// Copyright (c) 2022 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.
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
"github.com/illarion/gonotify"
|
||||
"github.com/illarion/gonotify/v2"
|
||||
"tailscale.com/health"
|
||||
)
|
||||
|
||||
func (m *directManager) runFileWatcher() {
|
||||
in, err := gonotify.NewInotify()
|
||||
ctx, cancel := context.WithCancel(m.ctx)
|
||||
defer cancel()
|
||||
in, err := gonotify.NewInotify(ctx)
|
||||
if err != nil {
|
||||
// Oh well, we tried. This is all best effort for now, to
|
||||
// surface warnings to users.
|
||||
m.logf("dns: inotify new: %v", err)
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithCancel(m.ctx)
|
||||
defer cancel()
|
||||
go m.closeInotifyOnDone(ctx, in)
|
||||
|
||||
const events = gonotify.IN_ATTRIB |
|
||||
gonotify.IN_CLOSE_WRITE |
|
||||
@@ -56,7 +56,53 @@ func (m *directManager) runFileWatcher() {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *directManager) closeInotifyOnDone(ctx context.Context, in *gonotify.Inotify) {
|
||||
<-ctx.Done()
|
||||
_ = in.Close()
|
||||
var resolvTrampleWarnable = health.Register(&health.Warnable{
|
||||
Code: "ctrld-resolv-conf-overwritten",
|
||||
Severity: health.SeverityMedium,
|
||||
Title: "Linux DNS configuration issue",
|
||||
Text: health.StaticMessage("Linux DNS config not ideal. /etc/resolv.conf overwritten. See https://tailscale.com/s/dns-fight"),
|
||||
})
|
||||
|
||||
// checkForFileTrample checks whether /etc/resolv.conf has been trampled
|
||||
// by another program on the system. (e.g. a DHCP client)
|
||||
func (m *directManager) checkForFileTrample() {
|
||||
m.mu.Lock()
|
||||
want := m.wantResolvConf
|
||||
lastWarn := m.lastWarnContents
|
||||
m.mu.Unlock()
|
||||
|
||||
if want == nil {
|
||||
return
|
||||
}
|
||||
|
||||
cur, err := m.fs.ReadFile(resolvConf)
|
||||
if err != nil {
|
||||
m.logf("trample: read error: %v", err)
|
||||
return
|
||||
}
|
||||
if bytes.Equal(cur, want) {
|
||||
m.health.SetHealthy(resolvTrampleWarnable)
|
||||
if lastWarn != nil {
|
||||
m.mu.Lock()
|
||||
m.lastWarnContents = nil
|
||||
m.mu.Unlock()
|
||||
m.logf("trample: resolv.conf again matches expected content")
|
||||
}
|
||||
return
|
||||
}
|
||||
if bytes.Equal(cur, lastWarn) {
|
||||
// We already logged about this, so not worth doing it again.
|
||||
return
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
m.lastWarnContents = cur
|
||||
m.mu.Unlock()
|
||||
|
||||
show := cur
|
||||
if len(show) > 1024 {
|
||||
show = show[:1024]
|
||||
}
|
||||
m.logf("trample: resolv.conf changed from what we expected. did some other program interfere? current contents: %q", show)
|
||||
m.health.SetUnhealthy(resolvTrampleWarnable, nil)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Copyright (c) 2022 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.
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !linux
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// 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.
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
@@ -79,7 +79,10 @@ func testDirect(t *testing.T, fs wholeFileFS) {
|
||||
}
|
||||
}
|
||||
|
||||
m := directManager{logf: t.Logf, fs: fs}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
m := directManager{logf: t.Logf, fs: fs, ctx: ctx, ctxClose: cancel}
|
||||
if err := m.SetDNS(OSConfig{
|
||||
Nameservers: []netip.Addr{netip.MustParseAddr("8.8.8.8"), netip.MustParseAddr("8.8.4.4")},
|
||||
SearchDomains: []dnsname.FQDN{"controld.com."},
|
||||
@@ -121,7 +124,7 @@ type brokenRemoveFS struct {
|
||||
directFS
|
||||
}
|
||||
|
||||
func (b brokenRemoveFS) Rename(_, _ string) error {
|
||||
func (b brokenRemoveFS) Rename(old, new string) error {
|
||||
return errors.New("nyaaah I'm a silly container!")
|
||||
}
|
||||
|
||||
@@ -178,12 +181,12 @@ func TestReadResolve(t *testing.T) {
|
||||
SearchDomains: []dnsname.FQDN{"controld.com."},
|
||||
},
|
||||
},
|
||||
{in: `search controld.com # typo`,
|
||||
{in: `search controld.com # comment`,
|
||||
want: OSConfig{
|
||||
SearchDomains: []dnsname.FQDN{"controld.com."},
|
||||
},
|
||||
},
|
||||
{in: `searchcontrold.com`, wantErr: true},
|
||||
{in: `searchctrld.com`, wantErr: true},
|
||||
{in: `search`, wantErr: true},
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// 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.
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package dns
|
||||
|
||||
@@ -8,13 +7,18 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"tailscale.com/control/controlknobs"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func NewOSConfigurator(logf logger.Logf, _ string) (OSConfigurator, error) {
|
||||
// NewOSConfigurator creates a new OS configurator.
|
||||
//
|
||||
// The health tracker may be nil; the knobs may be nil and are ignored on this platform.
|
||||
func NewOSConfigurator(logf logger.Logf, health *health.Tracker, _ *controlknobs.Knobs, _ string) (OSConfigurator, error) {
|
||||
bs, err := os.ReadFile("/etc/resolv.conf")
|
||||
if os.IsNotExist(err) {
|
||||
return newDirectManager(logf), nil
|
||||
return newDirectManager(logf, health), nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading /etc/resolv.conf: %w", err)
|
||||
@@ -24,16 +28,16 @@ func NewOSConfigurator(logf logger.Logf, _ string) (OSConfigurator, error) {
|
||||
case "resolvconf":
|
||||
switch resolvconfStyle() {
|
||||
case "":
|
||||
return newDirectManager(logf), nil
|
||||
return newDirectManager(logf, health), nil
|
||||
case "debian":
|
||||
return newDebianResolvconfManager(logf)
|
||||
case "openresolv":
|
||||
return newOpenresolvManager()
|
||||
return newOpenresolvManager(logf)
|
||||
default:
|
||||
logf("[unexpected] got unknown flavor of resolvconf %q, falling back to direct manager", resolvconfStyle())
|
||||
return newDirectManager(logf), nil
|
||||
return newDirectManager(logf, health), nil
|
||||
}
|
||||
default:
|
||||
return newDirectManager(logf), nil
|
||||
return newDirectManager(logf, health), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
// 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.
|
||||
|
||||
//lint:file-ignore U1000 Ignore this file, it's a copy.
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package dns
|
||||
|
||||
@@ -17,6 +14,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"tailscale.com/control/controlknobs"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/netaddr"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -38,7 +36,10 @@ func (kv kv) String() string {
|
||||
|
||||
var publishOnce sync.Once
|
||||
|
||||
func NewOSConfigurator(logf logger.Logf, interfaceName string) (ret OSConfigurator, err error) {
|
||||
// NewOSConfigurator created a new OS configurator.
|
||||
//
|
||||
// The health tracker may be nil; the knobs may be nil and are ignored on this platform.
|
||||
func NewOSConfigurator(logf logger.Logf, health *health.Tracker, _ *controlknobs.Knobs, interfaceName string) (ret OSConfigurator, err error) {
|
||||
env := newOSConfigEnv{
|
||||
fs: directFS{},
|
||||
dbusPing: dbusPing,
|
||||
@@ -47,7 +48,7 @@ func NewOSConfigurator(logf logger.Logf, interfaceName string) (ret OSConfigurat
|
||||
nmVersionBetween: nmVersionBetween,
|
||||
resolvconfStyle: resolvconfStyle,
|
||||
}
|
||||
mode, err := dnsMode(logf, env)
|
||||
mode, err := dnsMode(logf, health, env)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -59,18 +60,18 @@ func NewOSConfigurator(logf logger.Logf, interfaceName string) (ret OSConfigurat
|
||||
logf("dns: using %q mode", mode)
|
||||
switch mode {
|
||||
case "direct":
|
||||
return newDirectManagerOnFS(logf, env.fs), nil
|
||||
return newDirectManagerOnFS(logf, health, env.fs), nil
|
||||
case "systemd-resolved":
|
||||
return newResolvedManager(logf, interfaceName)
|
||||
return newResolvedManager(logf, health, interfaceName)
|
||||
case "network-manager":
|
||||
return newNMManager(interfaceName)
|
||||
case "debian-resolvconf":
|
||||
return newDebianResolvconfManager(logf)
|
||||
case "openresolv":
|
||||
return newOpenresolvManager()
|
||||
return newOpenresolvManager(logf)
|
||||
default:
|
||||
logf("[unexpected] detected unknown DNS mode %q, using direct manager as last resort", mode)
|
||||
return newDirectManagerOnFS(logf, env.fs), nil
|
||||
return newDirectManagerOnFS(logf, health, env.fs), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +85,7 @@ type newOSConfigEnv struct {
|
||||
resolvconfStyle func() string
|
||||
}
|
||||
|
||||
func dnsMode(logf logger.Logf, env newOSConfigEnv) (ret string, err error) {
|
||||
func dnsMode(logf logger.Logf, health *health.Tracker, env newOSConfigEnv) (ret string, err error) {
|
||||
var debug []kv
|
||||
dbg := func(k, v string) {
|
||||
debug = append(debug, kv{k, v})
|
||||
@@ -145,7 +146,7 @@ func dnsMode(logf logger.Logf, env newOSConfigEnv) (ret string, err error) {
|
||||
// header, but doesn't actually point to resolved. We mustn't
|
||||
// try to program resolved in that case.
|
||||
// https://github.com/tailscale/tailscale/issues/2136
|
||||
if err := resolvedIsActuallyResolver(bs); err != nil {
|
||||
if err := resolvedIsActuallyResolver(logf, env, dbg, bs); err != nil {
|
||||
logf("dns: resolvedIsActuallyResolver error: %v", err)
|
||||
dbg("resolved", "not-in-use")
|
||||
return "direct", nil
|
||||
@@ -231,7 +232,7 @@ func dnsMode(logf logger.Logf, env newOSConfigEnv) (ret string, err error) {
|
||||
dbg("rc", "nm")
|
||||
// Sometimes, NetworkManager owns the configuration but points
|
||||
// it at systemd-resolved.
|
||||
if err := resolvedIsActuallyResolver(bs); err != nil {
|
||||
if err := resolvedIsActuallyResolver(logf, env, dbg, bs); err != nil {
|
||||
logf("dns: resolvedIsActuallyResolver error: %v", err)
|
||||
dbg("resolved", "not-in-use")
|
||||
// You'd think we would use newNMManager here. However, as
|
||||
@@ -271,6 +272,14 @@ func dnsMode(logf logger.Logf, env newOSConfigEnv) (ret string, err error) {
|
||||
dbg("nm-safe", "yes")
|
||||
return "network-manager", nil
|
||||
}
|
||||
if err := env.nmIsUsingResolved(); err != nil {
|
||||
// If systemd-resolved is not running at all, then we don't have any
|
||||
// other choice: we take direct control of DNS.
|
||||
dbg("nm-resolved", "no")
|
||||
return "direct", nil
|
||||
}
|
||||
|
||||
//lint:ignore SA1019 upstream code still use it.
|
||||
health.SetDNSManagerHealth(errors.New("systemd-resolved and NetworkManager are wired together incorrectly; MagicDNS will probably not work. For more info, see https://tailscale.com/s/resolved-nm"))
|
||||
dbg("nm-safe", "no")
|
||||
return "systemd-resolved", nil
|
||||
@@ -324,14 +333,23 @@ func nmIsUsingResolved() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolvedIsActuallyResolver reports whether the given resolv.conf
|
||||
// bytes describe a configuration where systemd-resolved (127.0.0.53)
|
||||
// is the only configured nameserver.
|
||||
// resolvedIsActuallyResolver reports whether the system is using
|
||||
// systemd-resolved as the resolver. There are two different ways to
|
||||
// use systemd-resolved:
|
||||
// - libnss_resolve, which requires adding `resolve` to the "hosts:"
|
||||
// line in /etc/nsswitch.conf
|
||||
// - setting the only nameserver configured in `resolv.conf` to
|
||||
// systemd-resolved IP (127.0.0.53)
|
||||
//
|
||||
// Returns an error if the configuration is something other than
|
||||
// exclusively systemd-resolved, or nil if the config is only
|
||||
// systemd-resolved.
|
||||
func resolvedIsActuallyResolver(bs []byte) error {
|
||||
func resolvedIsActuallyResolver(logf logger.Logf, env newOSConfigEnv, dbg func(k, v string), bs []byte) error {
|
||||
if err := isLibnssResolveUsed(env); err == nil {
|
||||
dbg("resolved", "nss")
|
||||
return nil
|
||||
}
|
||||
|
||||
cfg, err := readResolv(bytes.NewBuffer(bs))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -348,9 +366,34 @@ func resolvedIsActuallyResolver(bs []byte) error {
|
||||
return fmt.Errorf("resolv.conf doesn't point to systemd-resolved; points to %v", cfg.Nameservers)
|
||||
}
|
||||
}
|
||||
dbg("resolved", "file")
|
||||
return nil
|
||||
}
|
||||
|
||||
// isLibnssResolveUsed reports whether libnss_resolve is used
|
||||
// for resolving names. Returns nil if it is, and an error otherwise.
|
||||
func isLibnssResolveUsed(env newOSConfigEnv) error {
|
||||
bs, err := env.fs.ReadFile("/etc/nsswitch.conf")
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading /etc/resolv.conf: %w", err)
|
||||
}
|
||||
for _, line := range strings.Split(string(bs), "\n") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 2 || fields[0] != "hosts:" {
|
||||
continue
|
||||
}
|
||||
for _, module := range fields[1:] {
|
||||
if module == "dns" {
|
||||
return fmt.Errorf("dns with a higher priority than libnss_resolve")
|
||||
}
|
||||
if module == "resolve" {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("libnss_resolve not used")
|
||||
}
|
||||
|
||||
func dbusPing(name, objectPath string) error {
|
||||
conn, err := dbus.SystemBus()
|
||||
if err != nil {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// 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.
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package dns
|
||||
|
||||
@@ -71,7 +70,7 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
{
|
||||
name: "resolved_alone_without_ping",
|
||||
env: env(resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53")),
|
||||
wantLog: "dns: ResolvConfMode error: dbus property not found\ndns: [rc=resolved nm=no resolv-conf-mode=error ret=systemd-resolved]",
|
||||
wantLog: "dns: ResolvConfMode error: dbus property not found\ndns: [rc=resolved resolved=file nm=no resolv-conf-mode=error ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
@@ -79,16 +78,46 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
env: env(
|
||||
resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53"),
|
||||
resolvedRunning()),
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved nm=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
name: "resolved_and_nsswitch_resolve",
|
||||
env: env(
|
||||
resolvDotConf("# Managed by systemd-resolved", "nameserver 1.1.1.1"),
|
||||
resolvedRunning(),
|
||||
nsswitchDotConf("hosts: files resolve [!UNAVAIL=return] dns"),
|
||||
),
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved resolved=nss nm=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
name: "resolved_and_nsswitch_dns",
|
||||
env: env(
|
||||
resolvDotConf("# Managed by systemd-resolved", "nameserver 1.1.1.1"),
|
||||
resolvedRunning(),
|
||||
nsswitchDotConf("hosts: files dns resolve [!UNAVAIL=return]"),
|
||||
),
|
||||
wantLog: "dns: resolvedIsActuallyResolver error: resolv.conf doesn't point to systemd-resolved; points to [1.1.1.1]\ndns: [resolved-ping=yes rc=resolved resolved=not-in-use ret=direct]",
|
||||
want: "direct",
|
||||
},
|
||||
{
|
||||
name: "resolved_and_nsswitch_none",
|
||||
env: env(
|
||||
resolvDotConf("# Managed by systemd-resolved", "nameserver 1.1.1.1"),
|
||||
resolvedRunning(),
|
||||
nsswitchDotConf("hosts:"),
|
||||
),
|
||||
wantLog: "dns: resolvedIsActuallyResolver error: resolv.conf doesn't point to systemd-resolved; points to [1.1.1.1]\ndns: [resolved-ping=yes rc=resolved resolved=not-in-use ret=direct]",
|
||||
want: "direct",
|
||||
},
|
||||
{
|
||||
name: "resolved_and_networkmanager_not_using_resolved",
|
||||
env: env(
|
||||
resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53"),
|
||||
resolvedRunning(),
|
||||
nmRunning("1.2.3", false)),
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved nm=yes nm-resolved=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=yes nm-resolved=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
@@ -97,7 +126,7 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53"),
|
||||
resolvedRunning(),
|
||||
nmRunning("1.26.2", true)),
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved nm=yes nm-resolved=yes nm-safe=yes ret=network-manager]",
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=yes nm-resolved=yes nm-safe=yes ret=network-manager]",
|
||||
want: "network-manager",
|
||||
},
|
||||
{
|
||||
@@ -106,7 +135,7 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53"),
|
||||
resolvedRunning(),
|
||||
nmRunning("1.27.0", true)),
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved nm=yes nm-resolved=yes nm-safe=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=yes nm-resolved=yes nm-safe=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
@@ -115,7 +144,7 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53"),
|
||||
resolvedRunning(),
|
||||
nmRunning("1.22.0", true)),
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved nm=yes nm-resolved=yes nm-safe=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=yes nm-resolved=yes nm-safe=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
// Regression tests for extreme corner cases below.
|
||||
@@ -141,7 +170,7 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
"nameserver 127.0.0.53",
|
||||
"nameserver 127.0.0.53"),
|
||||
resolvedRunning()),
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved nm=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
@@ -156,7 +185,7 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
"# run \"systemd-resolve --status\" to see details about the actual nameservers.",
|
||||
"nameserver 127.0.0.53"),
|
||||
resolvedRunning()),
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved nm=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
@@ -171,7 +200,7 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
"# 127.0.0.53 is the systemd-resolved stub resolver.",
|
||||
"# run \"systemd-resolve --status\" to see details about the actual nameservers.",
|
||||
"nameserver 127.0.0.53")),
|
||||
wantLog: "dns: ResolvConfMode error: dbus property not found\ndns: [rc=resolved nm=no resolv-conf-mode=error ret=systemd-resolved]",
|
||||
wantLog: "dns: ResolvConfMode error: dbus property not found\ndns: [rc=resolved resolved=file nm=no resolv-conf-mode=error ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
@@ -183,7 +212,7 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
"options edns0 trust-ad"),
|
||||
resolvedRunning(),
|
||||
nmRunning("1.32.12", true)),
|
||||
wantLog: "dns: [resolved-ping=yes rc=nm nm-resolved=yes nm-safe=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
wantLog: "dns: [resolved-ping=yes rc=nm resolved=file nm-resolved=yes nm-safe=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
@@ -194,7 +223,7 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
"nameserver 127.0.0.53",
|
||||
"options edns0 trust-ad"),
|
||||
nmRunning("1.32.12", true)),
|
||||
wantLog: "dns: ResolvConfMode error: dbus property not found\ndns: [rc=nm nm-resolved=yes nm-safe=no resolv-conf-mode=error ret=systemd-resolved]",
|
||||
wantLog: "dns: ResolvConfMode error: dbus property not found\ndns: [rc=nm resolved=file nm-resolved=yes nm-safe=no resolv-conf-mode=error ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
@@ -206,7 +235,7 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
"options edns0 trust-ad"),
|
||||
resolvedRunning(),
|
||||
nmRunning("1.26.3", true)),
|
||||
wantLog: "dns: [resolved-ping=yes rc=nm nm-resolved=yes nm-safe=yes ret=network-manager]",
|
||||
wantLog: "dns: [resolved-ping=yes rc=nm resolved=file nm-resolved=yes nm-safe=yes ret=network-manager]",
|
||||
want: "network-manager",
|
||||
},
|
||||
{
|
||||
@@ -217,7 +246,7 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
"nameserver 127.0.0.53",
|
||||
"options edns0 trust-ad"),
|
||||
resolvedRunning()),
|
||||
wantLog: "dns: [resolved-ping=yes rc=nm nm-resolved=yes nm=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
wantLog: "dns: [resolved-ping=yes rc=nm resolved=file nm-resolved=yes nm=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
@@ -228,7 +257,7 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
"search lan",
|
||||
"nameserver 127.0.0.53"),
|
||||
resolvedRunning()),
|
||||
wantLog: "dns: [resolved-ping=yes rc=nm nm-resolved=yes nm=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
wantLog: "dns: [resolved-ping=yes rc=nm resolved=file nm-resolved=yes nm=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
@@ -238,14 +267,26 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53"),
|
||||
resolvedDbusProperty(),
|
||||
)),
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved nm=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
// regression test for https://github.com/tailscale/tailscale/issues/9687
|
||||
name: "networkmanager_endeavouros",
|
||||
env: env(resolvDotConf(
|
||||
"# Generated by NetworkManager",
|
||||
"search example.com localdomain",
|
||||
"nameserver 10.0.0.1"),
|
||||
nmRunning("1.44.2", false)),
|
||||
wantLog: "dns: resolvedIsActuallyResolver error: resolv.conf doesn't point to systemd-resolved; points to [10.0.0.1]\n" +
|
||||
"dns: [rc=nm resolved=not-in-use ret=direct]",
|
||||
want: "direct",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var logBuf tstest.MemLogger
|
||||
got, err := dnsMode(logBuf.Logf, tt.env)
|
||||
got, err := dnsMode(logBuf.Logf, nil, tt.env)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -272,8 +313,9 @@ func (m memFS) Stat(name string) (isRegular bool, err error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (m memFS) Rename(_, _ string) error { panic("TODO") }
|
||||
func (m memFS) Remove(_ string) error { panic("TODO") }
|
||||
func (m memFS) Chmod(name string, mode os.FileMode) error { panic("TODO") }
|
||||
func (m memFS) Rename(oldName, newName string) error { panic("TODO") }
|
||||
func (m memFS) Remove(name string) error { panic("TODO") }
|
||||
func (m memFS) ReadFile(name string) ([]byte, error) {
|
||||
v, ok := m[name]
|
||||
if !ok {
|
||||
@@ -297,7 +339,7 @@ func (m memFS) Truncate(name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m memFS) WriteFile(name string, contents []byte, _ os.FileMode) error {
|
||||
func (m memFS) WriteFile(name string, contents []byte, perm os.FileMode) error {
|
||||
m[name] = string(contents)
|
||||
return nil
|
||||
}
|
||||
@@ -381,6 +423,12 @@ func resolvDotConf(ss ...string) envOption {
|
||||
})
|
||||
}
|
||||
|
||||
func nsswitchDotConf(ss ...string) envOption {
|
||||
return envOpt(func(b *envBuilder) {
|
||||
b.fs["/etc/nsswitch.conf"] = strings.Join(ss, "\n")
|
||||
})
|
||||
}
|
||||
|
||||
// resolvedRunning returns an option that makes resolved reply to a dbusPing
|
||||
// and the ResolvConfMode property.
|
||||
func resolvedRunning() envOption {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// 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.
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
|
||||
@@ -11,6 +10,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
@@ -24,6 +24,13 @@ const (
|
||||
lowerPriority = int32(200) // lower than all builtin auto priorities
|
||||
)
|
||||
|
||||
// reconfigTimeout is the time interval within which Manager.{Up,Down} should complete.
|
||||
//
|
||||
// This is particularly useful because certain conditions can cause indefinite hangs
|
||||
// (such as improper dbus auth followed by contextless dbus.Object.Call).
|
||||
// Such operations should be wrapped in a timeout context.
|
||||
const reconfigTimeout = time.Second
|
||||
|
||||
// nmManager uses the NetworkManager DBus API.
|
||||
type nmManager struct {
|
||||
interfaceName string
|
||||
@@ -31,8 +38,6 @@ type nmManager struct {
|
||||
dnsManager dbus.BusObject
|
||||
}
|
||||
|
||||
var _ OSConfigurator = (*nmManager)(nil)
|
||||
|
||||
func newNMManager(interfaceName string) (*nmManager, error) {
|
||||
conn, err := dbus.SystemBus()
|
||||
if err != nil {
|
||||
@@ -141,18 +146,17 @@ func (m *nmManager) trySet(ctx context.Context, config OSConfig) error {
|
||||
// tell it explicitly to keep it. Read out the current interface
|
||||
// settings and mirror them out to NetworkManager.
|
||||
var addrs6 []map[string]any
|
||||
if netIface, err := net.InterfaceByName(m.interfaceName); err == nil {
|
||||
if addrs, err := netIface.Addrs(); err == nil {
|
||||
for _, a := range addrs {
|
||||
if ipnet, ok := a.(*net.IPNet); ok {
|
||||
nip, ok := netip.AddrFromSlice(ipnet.IP)
|
||||
nip = nip.Unmap()
|
||||
if ok && nip.Is6() {
|
||||
addrs6 = append(addrs6, map[string]any{
|
||||
"address": nip.String(),
|
||||
"prefix": uint32(128),
|
||||
})
|
||||
}
|
||||
if tsIf, err := net.InterfaceByName(m.interfaceName); err == nil {
|
||||
addrs, _ := tsIf.Addrs()
|
||||
for _, a := range addrs {
|
||||
if ipnet, ok := a.(*net.IPNet); ok {
|
||||
nip, ok := netip.AddrFromSlice(ipnet.IP)
|
||||
nip = nip.Unmap()
|
||||
if ok && nip.Is6() {
|
||||
addrs6 = append(addrs6, map[string]any{
|
||||
"address": nip.String(),
|
||||
"prefix": uint32(128),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -260,6 +264,125 @@ func (m *nmManager) trySet(ctx context.Context, config OSConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *nmManager) SupportsSplitDNS() bool {
|
||||
var mode string
|
||||
v, err := m.dnsManager.GetProperty("org.freedesktop.NetworkManager.DnsManager.Mode")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
mode, ok := v.Value().(string)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// Per NM's documentation, it only does split-DNS when it's
|
||||
// programming dnsmasq or systemd-resolved. All other modes are
|
||||
// primary-only.
|
||||
return mode == "dnsmasq" || mode == "systemd-resolved"
|
||||
}
|
||||
|
||||
func (m *nmManager) GetBaseConfig() (OSConfig, error) {
|
||||
conn, err := dbus.SystemBus()
|
||||
if err != nil {
|
||||
return OSConfig{}, err
|
||||
}
|
||||
|
||||
nm := conn.Object("org.freedesktop.NetworkManager", dbus.ObjectPath("/org/freedesktop/NetworkManager/DnsManager"))
|
||||
v, err := nm.GetProperty("org.freedesktop.NetworkManager.DnsManager.Configuration")
|
||||
if err != nil {
|
||||
return OSConfig{}, err
|
||||
}
|
||||
cfgs, ok := v.Value().([]map[string]dbus.Variant)
|
||||
if !ok {
|
||||
return OSConfig{}, fmt.Errorf("unexpected NM config type %T", v.Value())
|
||||
}
|
||||
|
||||
if len(cfgs) == 0 {
|
||||
return OSConfig{}, nil
|
||||
}
|
||||
|
||||
type dnsPrio struct {
|
||||
resolvers []netip.Addr
|
||||
domains []string
|
||||
priority int32
|
||||
}
|
||||
order := make([]dnsPrio, 0, len(cfgs)-1)
|
||||
|
||||
for _, cfg := range cfgs {
|
||||
if name, ok := cfg["interface"]; ok {
|
||||
if s, ok := name.Value().(string); ok && s == m.interfaceName {
|
||||
// Config for the tailscale interface, skip.
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
var p dnsPrio
|
||||
|
||||
if v, ok := cfg["nameservers"]; ok {
|
||||
if ips, ok := v.Value().([]string); ok {
|
||||
for _, s := range ips {
|
||||
ip, err := netip.ParseAddr(s)
|
||||
if err != nil {
|
||||
// hmm, what do? Shouldn't really happen.
|
||||
continue
|
||||
}
|
||||
p.resolvers = append(p.resolvers, ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
if v, ok := cfg["domains"]; ok {
|
||||
if domains, ok := v.Value().([]string); ok {
|
||||
p.domains = domains
|
||||
}
|
||||
}
|
||||
if v, ok := cfg["priority"]; ok {
|
||||
if prio, ok := v.Value().(int32); ok {
|
||||
p.priority = prio
|
||||
}
|
||||
}
|
||||
|
||||
order = append(order, p)
|
||||
}
|
||||
|
||||
sort.Slice(order, func(i, j int) bool {
|
||||
return order[i].priority < order[j].priority
|
||||
})
|
||||
|
||||
var (
|
||||
ret OSConfig
|
||||
seenResolvers = map[netip.Addr]bool{}
|
||||
seenSearch = map[string]bool{}
|
||||
)
|
||||
|
||||
for _, cfg := range order {
|
||||
for _, resolver := range cfg.resolvers {
|
||||
if seenResolvers[resolver] {
|
||||
continue
|
||||
}
|
||||
ret.Nameservers = append(ret.Nameservers, resolver)
|
||||
seenResolvers[resolver] = true
|
||||
}
|
||||
for _, dom := range cfg.domains {
|
||||
if seenSearch[dom] {
|
||||
continue
|
||||
}
|
||||
fqdn, err := dnsname.ToFQDN(dom)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
ret.SearchDomains = append(ret.SearchDomains, fqdn)
|
||||
seenSearch[dom] = true
|
||||
}
|
||||
if cfg.priority < 0 {
|
||||
// exclusive configurations preempt all other
|
||||
// configurations, so we're done.
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (m *nmManager) Close() error {
|
||||
// No need to do anything on close, NetworkManager will delete our
|
||||
// settings when the tailscale interface goes away.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// 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.
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux || freebsd || openbsd
|
||||
|
||||
@@ -10,22 +9,41 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// openresolvManager manages DNS configuration using the openresolv
|
||||
// implementation of the `resolvconf` program.
|
||||
type openresolvManager struct{}
|
||||
type openresolvManager struct {
|
||||
logf logger.Logf
|
||||
}
|
||||
|
||||
var _ OSConfigurator = (*openresolvManager)(nil)
|
||||
func newOpenresolvManager(logf logger.Logf) (openresolvManager, error) {
|
||||
return openresolvManager{logf}, nil
|
||||
}
|
||||
|
||||
func newOpenresolvManager() (openresolvManager, error) {
|
||||
return openresolvManager{}, nil
|
||||
func (m openresolvManager) logCmdErr(cmd *exec.Cmd, err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
commandStr := fmt.Sprintf("path=%q args=%q", cmd.Path, cmd.Args)
|
||||
exerr, ok := err.(*exec.ExitError)
|
||||
if !ok {
|
||||
m.logf("error running command %s: %v", commandStr, err)
|
||||
return
|
||||
}
|
||||
|
||||
m.logf("error running command %s stderr=%q exitCode=%d: %v", commandStr, exerr.Stderr, exerr.ExitCode(), err)
|
||||
}
|
||||
|
||||
func (m openresolvManager) deleteTailscaleConfig() error {
|
||||
cmd := exec.Command("resolvconf", "-f", "-d", "ctrld")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
m.logCmdErr(cmd, err)
|
||||
return fmt.Errorf("running %s: %s", cmd, out)
|
||||
}
|
||||
return nil
|
||||
@@ -43,11 +61,55 @@ func (m openresolvManager) SetDNS(config OSConfig) error {
|
||||
cmd.Stdin = &stdin
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
m.logCmdErr(cmd, err)
|
||||
return fmt.Errorf("running %s: %s", cmd, out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m openresolvManager) SupportsSplitDNS() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (m openresolvManager) GetBaseConfig() (OSConfig, error) {
|
||||
// List the names of all config snippets openresolv is aware
|
||||
// of. Snippets get listed in priority order (most to least),
|
||||
// which we'll exploit later.
|
||||
bs, err := exec.Command("resolvconf", "-i").CombinedOutput()
|
||||
if err != nil {
|
||||
return OSConfig{}, err
|
||||
}
|
||||
|
||||
// Remove the "tailscale" snippet from the list.
|
||||
args := []string{"-l"}
|
||||
for _, f := range strings.Split(strings.TrimSpace(string(bs)), " ") {
|
||||
if f == "tailscale" {
|
||||
continue
|
||||
}
|
||||
args = append(args, f)
|
||||
}
|
||||
|
||||
// List all resolvconf snippets except our own, and parse that as
|
||||
// a resolv.conf. This effectively generates a blended config of
|
||||
// "everyone except tailscale", which is what would be in use if
|
||||
// tailscale hadn't set exclusive mode.
|
||||
//
|
||||
// Note that this is not _entirely_ true. To be perfectly correct,
|
||||
// we should be looking for other interfaces marked exclusive that
|
||||
// predated tailscale, and stick to only those. However, in
|
||||
// practice, openresolv uses are generally quite limited, and boil
|
||||
// down to 1-2 DHCP leases, for which the correct outcome is a
|
||||
// blended config like the one we produce here.
|
||||
var buf bytes.Buffer
|
||||
cmd := exec.Command("resolvconf", args...)
|
||||
cmd.Stdout = &buf
|
||||
if err := cmd.Run(); err != nil {
|
||||
m.logCmdErr(cmd, err)
|
||||
return OSConfig{}, err
|
||||
}
|
||||
return readResolv(&buf)
|
||||
}
|
||||
|
||||
func (m openresolvManager) Close() error {
|
||||
return m.deleteTailscaleConfig()
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
// 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.
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
"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.
|
||||
@@ -23,8 +23,21 @@ type OSConfigurator interface {
|
||||
// SetDNS must not be called after Close.
|
||||
// SetDNS takes ownership of cfg.
|
||||
SetDNS(cfg OSConfig) error
|
||||
// SupportsSplitDNS reports whether the configurator is capable of
|
||||
// installing a resolver only for specific DNS suffixes. If false,
|
||||
// the configurator can only set a global resolver.
|
||||
SupportsSplitDNS() bool
|
||||
// GetBaseConfig returns the OS's "base" configuration, i.e. the
|
||||
// resolver settings the OS would use without Tailscale
|
||||
// contributing any configuration.
|
||||
// GetBaseConfig must return the tailscale-free base config even
|
||||
// after SetDNS has been called to set a Tailscale configuration.
|
||||
// Only works when SupportsSplitDNS=false.
|
||||
|
||||
// Close removes ctrld-related DNS configuration from the OS.
|
||||
// Implementations that don't support getting the base config must
|
||||
// return ErrGetBaseConfigNotSupported.
|
||||
GetBaseConfig() (OSConfig, error)
|
||||
// Close removes Tailscale-related DNS configuration from the OS.
|
||||
Close() error
|
||||
|
||||
Mode() string
|
||||
@@ -50,14 +63,59 @@ type OSConfig struct {
|
||||
SearchDomains []dnsname.FQDN
|
||||
// MatchDomains are the DNS suffixes for which Nameservers should
|
||||
// be used. If empty, Nameservers is installed as the "primary" resolver.
|
||||
// A non-empty MatchDomains requests a "split DNS" configuration
|
||||
// from the OS, which will only work with OSConfigurators that
|
||||
// report SupportsSplitDNS()=true.
|
||||
MatchDomains []dnsname.FQDN
|
||||
}
|
||||
|
||||
func (o *OSConfig) WriteToBufioWriter(w *bufio.Writer) {
|
||||
if o == nil {
|
||||
w.WriteString("<nil>")
|
||||
return
|
||||
}
|
||||
w.WriteString("{")
|
||||
if len(o.Hosts) > 0 {
|
||||
fmt.Fprintf(w, "Hosts:%v ", o.Hosts)
|
||||
}
|
||||
if len(o.Nameservers) > 0 {
|
||||
fmt.Fprintf(w, "Nameservers:%v ", o.Nameservers)
|
||||
}
|
||||
if len(o.SearchDomains) > 0 {
|
||||
fmt.Fprintf(w, "SearchDomains:%v ", o.SearchDomains)
|
||||
}
|
||||
if len(o.MatchDomains) > 0 {
|
||||
w.WriteString("MatchDomains:[")
|
||||
sp := ""
|
||||
var numARPA int
|
||||
for _, s := range o.MatchDomains {
|
||||
if strings.HasSuffix(string(s), ".arpa.") {
|
||||
numARPA++
|
||||
continue
|
||||
}
|
||||
w.WriteString(sp)
|
||||
w.WriteString(string(s))
|
||||
sp = " "
|
||||
}
|
||||
w.WriteString("]")
|
||||
if numARPA > 0 {
|
||||
fmt.Fprintf(w, "+%darpa", numARPA)
|
||||
}
|
||||
}
|
||||
w.WriteString("}")
|
||||
}
|
||||
|
||||
func (o OSConfig) IsZero() bool {
|
||||
return len(o.Nameservers) == 0 && len(o.SearchDomains) == 0 && len(o.MatchDomains) == 0
|
||||
return len(o.Hosts) == 0 &&
|
||||
len(o.Nameservers) == 0 &&
|
||||
len(o.SearchDomains) == 0 &&
|
||||
len(o.MatchDomains) == 0
|
||||
}
|
||||
|
||||
func (a OSConfig) Equal(b OSConfig) bool {
|
||||
if len(a.Hosts) != len(b.Hosts) {
|
||||
return false
|
||||
}
|
||||
if len(a.Nameservers) != len(b.Nameservers) {
|
||||
return false
|
||||
}
|
||||
@@ -68,6 +126,15 @@ func (a OSConfig) Equal(b OSConfig) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range a.Hosts {
|
||||
ha, hb := a.Hosts[i], b.Hosts[i]
|
||||
if ha.Addr != hb.Addr {
|
||||
return false
|
||||
}
|
||||
if !slices.Equal(ha.Hosts, hb.Hosts) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for i := range a.Nameservers {
|
||||
if a.Nameservers[i] != b.Nameservers[i] {
|
||||
return false
|
||||
@@ -93,34 +160,39 @@ func (a OSConfig) Equal(b OSConfig) bool {
|
||||
// Fixes https://github.com/tailscale/tailscale/issues/5669
|
||||
func (a OSConfig) Format(f fmt.State, verb rune) {
|
||||
logger.ArgWriter(func(w *bufio.Writer) {
|
||||
_, _ = w.WriteString(`{Nameservers:[`)
|
||||
w.WriteString(`{Nameservers:[`)
|
||||
for i, ns := range a.Nameservers {
|
||||
if i != 0 {
|
||||
_, _ = w.WriteString(" ")
|
||||
w.WriteString(" ")
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, "%+v", ns)
|
||||
fmt.Fprintf(w, "%+v", ns)
|
||||
}
|
||||
_, _ = w.WriteString(`] SearchDomains:[`)
|
||||
w.WriteString(`] SearchDomains:[`)
|
||||
for i, domain := range a.SearchDomains {
|
||||
if i != 0 {
|
||||
_, _ = w.WriteString(" ")
|
||||
w.WriteString(" ")
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, "%+v", domain)
|
||||
fmt.Fprintf(w, "%+v", domain)
|
||||
}
|
||||
_, _ = w.WriteString(`] MatchDomains:[`)
|
||||
w.WriteString(`] MatchDomains:[`)
|
||||
for i, domain := range a.MatchDomains {
|
||||
if i != 0 {
|
||||
_, _ = w.WriteString(" ")
|
||||
w.WriteString(" ")
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, "%+v", domain)
|
||||
fmt.Fprintf(w, "%+v", domain)
|
||||
}
|
||||
_, _ = w.WriteString(`] Hosts:[`)
|
||||
w.WriteString(`] Hosts:[`)
|
||||
for i, host := range a.Hosts {
|
||||
if i != 0 {
|
||||
_, _ = w.WriteString(" ")
|
||||
w.WriteString(" ")
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, "%+v", host)
|
||||
fmt.Fprintf(w, "%+v", host)
|
||||
}
|
||||
_, _ = w.WriteString(`]}`)
|
||||
w.WriteString(`]}`)
|
||||
}).Format(f, verb)
|
||||
}
|
||||
|
||||
// ErrGetBaseConfigNotSupported is the error
|
||||
// OSConfigurator.GetBaseConfig returns when the OSConfigurator
|
||||
// doesn't support reading the underlying configuration out of the OS.
|
||||
var ErrGetBaseConfigNotSupported = errors.New("getting OS base config is not supported")
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
// Copyright (c) 2022 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.
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
@@ -42,3 +43,13 @@ func TestOSConfigPrintable(t *testing.T) {
|
||||
t.Errorf("format mismatch:\n got: %s\n want: %s", s, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsZero(t *testing.T) {
|
||||
tstest.CheckIsZero[OSConfig](t, map[reflect.Type]any{
|
||||
reflect.TypeFor[dnsname.FQDN](): dnsname.FQDN("foo.bar."),
|
||||
reflect.TypeFor[*HostEntry](): &HostEntry{
|
||||
Addr: netip.AddrFrom4([4]byte{100, 1, 2, 3}),
|
||||
Hosts: []string{"foo", "bar"},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#!/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.
|
||||
# Copyright (c) Ctrld Inc & AUTHORS
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
#
|
||||
# This script is a workaround for a vpn-unfriendly behavior of the
|
||||
# original resolvconf by Thomas Hood. Unlike the `openresolv`
|
||||
@@ -29,7 +28,7 @@ if [ -n "$CTRLD_RESOLVCONF_HOOK_LOOP" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ ! -f ctrld.inet ]; then
|
||||
if [ ! -f tun-ctrld.inet ]; then
|
||||
# Ctrld isn't trying to manage DNS, do nothing.
|
||||
exit 0
|
||||
fi
|
||||
@@ -60,4 +59,4 @@ 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
|
||||
fi
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// 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.
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux || freebsd || openbsd
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
@@ -14,13 +14,17 @@ func resolvconfStyle() string {
|
||||
if _, err := exec.LookPath("resolvconf"); err != nil {
|
||||
return ""
|
||||
}
|
||||
if _, err := exec.Command("resolvconf", "--version").CombinedOutput(); err != nil {
|
||||
output, err := exec.Command("resolvconf", "--version").CombinedOutput()
|
||||
if err != nil {
|
||||
// Debian resolvconf doesn't understand --version, and
|
||||
// exits with a specific error code.
|
||||
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 99 {
|
||||
return "debian"
|
||||
}
|
||||
}
|
||||
if bytes.HasPrefix(output, []byte("Debian resolvconf")) {
|
||||
return "debian"
|
||||
}
|
||||
// Treat everything else as openresolv, by far the more popular implementation.
|
||||
return "openresolv"
|
||||
}
|
||||
|
||||
11
internal/dns/resolvconfpath_default.go
Normal file
11
internal/dns/resolvconfpath_default.go
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !gokrazy
|
||||
|
||||
package dns
|
||||
|
||||
const (
|
||||
resolvConf = "/etc/resolv.conf"
|
||||
backupConf = "/etc/resolv.pre-ctrld-backup.conf"
|
||||
)
|
||||
11
internal/dns/resolvconfpath_gokrazy.go
Normal file
11
internal/dns/resolvconfpath_gokrazy.go
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build gokrazy
|
||||
|
||||
package dns
|
||||
|
||||
const (
|
||||
resolvConf = "/tmp/resolv.conf"
|
||||
backupConf = "/tmp/resolv.pre-ctrld-backup.conf"
|
||||
)
|
||||
@@ -1,6 +1,5 @@
|
||||
// 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.
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
|
||||
@@ -21,8 +20,6 @@ import (
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
const reconfigTimeout = time.Second
|
||||
|
||||
// DBus entities we talk to.
|
||||
//
|
||||
// DBus is an RPC bus. In particular, the bus we're talking to is the
|
||||
@@ -97,16 +94,14 @@ type resolvedManager struct {
|
||||
ctx context.Context
|
||||
cancel func() // terminate the context, for close
|
||||
|
||||
logf logger.Logf
|
||||
ifidx int
|
||||
logf logger.Logf
|
||||
health *health.Tracker
|
||||
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) {
|
||||
func newResolvedManager(logf logger.Logf, health *health.Tracker, interfaceName string) (*resolvedManager, error) {
|
||||
iface, err := net.InterfaceByName(interfaceName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -119,11 +114,11 @@ func newResolvedManager(logf logger.Logf, interfaceName string) (*resolvedManage
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
|
||||
logf: logf,
|
||||
ifidx: iface.Index,
|
||||
logf: logf,
|
||||
health: health,
|
||||
ifidx: iface.Index,
|
||||
|
||||
configCR: make(chan changeRequest),
|
||||
revertCh: make(chan struct{}),
|
||||
}
|
||||
|
||||
go mgr.run(ctx)
|
||||
@@ -132,8 +127,10 @@ func newResolvedManager(logf logger.Logf, interfaceName string) (*resolvedManage
|
||||
}
|
||||
|
||||
func (m *resolvedManager) SetDNS(config OSConfig) error {
|
||||
// NOTE: don't close this channel, since it's possible that the SetDNS
|
||||
// call will time out and return before the run loop answers, at which
|
||||
// point it will send on the now-closed channel.
|
||||
errc := make(chan error, 1)
|
||||
defer close(errc)
|
||||
|
||||
select {
|
||||
case <-m.ctx.Done():
|
||||
@@ -221,14 +218,12 @@ 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.
|
||||
bo.BackOff(ctx, nil)
|
||||
health.SetDNSOSHealth(nil)
|
||||
//lint:ignore SA1019 upstream code still use it.
|
||||
m.health.SetDNSOSHealth(nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -243,15 +238,13 @@ 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, 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.
|
||||
@@ -308,7 +301,8 @@ func (m *resolvedManager) run(ctx context.Context) {
|
||||
// Set health while holding the lock, because this will
|
||||
// graciously serialize the resync's health outcome with a
|
||||
// concurrent SetDNS call.
|
||||
health.SetDNSOSHealth(err)
|
||||
//lint:ignore SA1019 upstream code still use it.
|
||||
m.health.SetDNSOSHealth(err)
|
||||
if err != nil {
|
||||
m.logf("failed to configure systemd-resolved: %v", err)
|
||||
}
|
||||
@@ -426,18 +420,22 @@ func (m *resolvedManager) setConfigOverDBus(ctx context.Context, rManager dbus.B
|
||||
m.logf("[v1] failed to disable DoT: %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)
|
||||
}
|
||||
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) SupportsSplitDNS() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *resolvedManager) GetBaseConfig() (OSConfig, error) {
|
||||
return OSConfig{}, ErrGetBaseConfigNotSupported
|
||||
}
|
||||
|
||||
func (m *resolvedManager) Close() error {
|
||||
m.cancel() // stops the 'run' method goroutine
|
||||
<-m.revertCh
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package ctrld
|
||||
|
||||
import "net"
|
||||
|
||||
type dnsFn func() []string
|
||||
|
||||
// nameservers returns DNS nameservers from system settings.
|
||||
func nameservers() []string {
|
||||
var dns []string
|
||||
seen := make(map[string]bool)
|
||||
@@ -21,7 +20,7 @@ func nameservers() []string {
|
||||
continue
|
||||
}
|
||||
seen[ns] = true
|
||||
dns = append(dns, net.JoinHostPort(ns, "53"))
|
||||
dns = append(dns, ns)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
//go:build darwin || dragonfly || freebsd || netbsd || openbsd
|
||||
//go:build 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}
|
||||
return []dnsFn{dnsFromRIB}
|
||||
}
|
||||
|
||||
func dnsFromRIB() []string {
|
||||
@@ -49,18 +46,6 @@ func dnsFromRIB() []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:
|
||||
|
||||
217
nameservers_darwin.go
Normal file
217
nameservers_darwin.go
Normal file
@@ -0,0 +1,217 @@
|
||||
//go:build darwin
|
||||
|
||||
package ctrld
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tailscale.com/net/netmon"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld/internal/resolvconffile"
|
||||
)
|
||||
|
||||
func dnsFns() []dnsFn {
|
||||
return []dnsFn{dnsFromResolvConf, getDNSFromScutil, getAllDHCPNameservers}
|
||||
}
|
||||
|
||||
// dnsFromResolvConf reads nameservers from /etc/resolv.conf
|
||||
func dnsFromResolvConf() []string {
|
||||
const (
|
||||
maxRetries = 10
|
||||
retryInterval = 100 * time.Millisecond
|
||||
)
|
||||
|
||||
regularIPs, loopbackIPs, _ := netmon.LocalAddresses()
|
||||
|
||||
var dns []string
|
||||
for attempt := 0; attempt < maxRetries; attempt++ {
|
||||
if attempt > 0 {
|
||||
time.Sleep(retryInterval)
|
||||
}
|
||||
|
||||
nss := resolvconffile.NameServers("")
|
||||
var localDNS []string
|
||||
seen := make(map[string]bool)
|
||||
|
||||
for _, ns := range nss {
|
||||
if ip := net.ParseIP(ns); ip != nil {
|
||||
// skip loopback IPs
|
||||
for _, v := range slices.Concat(regularIPs, loopbackIPs) {
|
||||
ipStr := v.String()
|
||||
if ip.String() == ipStr {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if !seen[ip.String()] {
|
||||
seen[ip.String()] = true
|
||||
localDNS = append(localDNS, ip.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we successfully read the file and found nameservers, return them
|
||||
if len(localDNS) > 0 {
|
||||
return localDNS
|
||||
}
|
||||
}
|
||||
|
||||
return dns
|
||||
}
|
||||
|
||||
func getDNSFromScutil() []string {
|
||||
logger := *ProxyLogger.Load()
|
||||
|
||||
const (
|
||||
maxRetries = 10
|
||||
retryInterval = 100 * time.Millisecond
|
||||
)
|
||||
|
||||
regularIPs, loopbackIPs, _ := netmon.LocalAddresses()
|
||||
|
||||
var nameservers []string
|
||||
for attempt := 0; attempt < maxRetries; attempt++ {
|
||||
if attempt > 0 {
|
||||
time.Sleep(retryInterval)
|
||||
}
|
||||
|
||||
cmd := exec.Command("scutil", "--dns")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
Log(context.Background(), logger.Error(), "failed to execute scutil --dns (attempt %d/%d): %v", attempt+1, maxRetries, err)
|
||||
continue
|
||||
}
|
||||
|
||||
var localDNS []string
|
||||
seen := make(map[string]bool)
|
||||
|
||||
scanner := bufio.NewScanner(bytes.NewReader(output))
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if strings.HasPrefix(line, "nameserver[") {
|
||||
parts := strings.Split(line, ":")
|
||||
if len(parts) == 2 {
|
||||
ns := strings.TrimSpace(parts[1])
|
||||
if ip := net.ParseIP(ns); ip != nil {
|
||||
// skip loopback IPs
|
||||
isLocal := false
|
||||
for _, v := range slices.Concat(regularIPs, loopbackIPs) {
|
||||
ipStr := v.String()
|
||||
if ip.String() == ipStr {
|
||||
isLocal = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isLocal && !seen[ip.String()] {
|
||||
seen[ip.String()] = true
|
||||
localDNS = append(localDNS, ip.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
Log(context.Background(), logger.Error(), "error scanning scutil output (attempt %d/%d): %v", attempt+1, maxRetries, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// If we successfully read the output and found nameservers, return them
|
||||
if len(localDNS) > 0 {
|
||||
return localDNS
|
||||
}
|
||||
}
|
||||
|
||||
return nameservers
|
||||
}
|
||||
|
||||
func getDHCPNameservers(iface string) ([]string, error) {
|
||||
// Run the ipconfig command for the given interface.
|
||||
cmd := exec.Command("ipconfig", "getpacket", iface)
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error running ipconfig: %v", err)
|
||||
}
|
||||
|
||||
// Look for a line like:
|
||||
// domain_name_servers = 192.168.1.1 8.8.8.8;
|
||||
re := regexp.MustCompile(`domain_name_servers\s*=\s*(.*);`)
|
||||
matches := re.FindStringSubmatch(string(output))
|
||||
if len(matches) < 2 {
|
||||
return nil, fmt.Errorf("no DHCP nameservers found")
|
||||
}
|
||||
|
||||
// Split the nameservers by whitespace.
|
||||
nameservers := strings.Fields(matches[1])
|
||||
return nameservers, nil
|
||||
}
|
||||
|
||||
func getAllDHCPNameservers() []string {
|
||||
interfaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
regularIPs, loopbackIPs, _ := netmon.LocalAddresses()
|
||||
|
||||
var allNameservers []string
|
||||
seen := make(map[string]bool)
|
||||
|
||||
for _, iface := range interfaces {
|
||||
// Skip interfaces that are:
|
||||
// - down
|
||||
// - loopback
|
||||
// - not physical (virtual)
|
||||
// - point-to-point (like VPN interfaces)
|
||||
// - without MAC address (non-physical)
|
||||
if iface.Flags&net.FlagUp == 0 ||
|
||||
iface.Flags&net.FlagLoopback != 0 ||
|
||||
iface.Flags&net.FlagPointToPoint != 0 ||
|
||||
(iface.Flags&net.FlagBroadcast == 0 &&
|
||||
iface.Flags&net.FlagMulticast == 0) ||
|
||||
len(iface.HardwareAddr) == 0 ||
|
||||
strings.HasPrefix(iface.Name, "utun") ||
|
||||
strings.HasPrefix(iface.Name, "llw") ||
|
||||
strings.HasPrefix(iface.Name, "awdl") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Verify it's a valid MAC address (should be 6 bytes for IEEE 802 MAC-48)
|
||||
if len(iface.HardwareAddr) != 6 {
|
||||
continue
|
||||
}
|
||||
|
||||
nameservers, err := getDHCPNameservers(iface.Name)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Add unique nameservers to the result, skipping local IPs
|
||||
for _, ns := range nameservers {
|
||||
if ip := net.ParseIP(ns); ip != nil {
|
||||
// skip loopback and local IPs
|
||||
isLocal := false
|
||||
for _, v := range slices.Concat(regularIPs, loopbackIPs) {
|
||||
if ip.String() == v.String() {
|
||||
isLocal = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isLocal && !seen[ns] {
|
||||
seen[ns] = true
|
||||
allNameservers = append(allNameservers, ns)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allNameservers
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/hex"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld/internal/dns/resolvconffile"
|
||||
)
|
||||
@@ -28,6 +29,7 @@ func dns4() []string {
|
||||
|
||||
var dns []string
|
||||
seen := make(map[string]bool)
|
||||
vis := virtualInterfaces()
|
||||
s := bufio.NewScanner(f)
|
||||
first := true
|
||||
for s.Scan() {
|
||||
@@ -39,7 +41,10 @@ func dns4() []string {
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip virtual interfaces.
|
||||
if vis.contains(string(bytes.TrimSpace(fields[0]))) {
|
||||
continue
|
||||
}
|
||||
gw := make([]byte, net.IPv4len)
|
||||
// Third fields is gateway.
|
||||
if _, err := hex.Decode(gw, fields[2]); err != nil {
|
||||
@@ -63,12 +68,17 @@ func dns6() []string {
|
||||
defer f.Close()
|
||||
|
||||
var dns []string
|
||||
vis := virtualInterfaces()
|
||||
s := bufio.NewScanner(f)
|
||||
for s.Scan() {
|
||||
fields := bytes.Fields(s.Bytes())
|
||||
if len(fields) < 4 {
|
||||
continue
|
||||
}
|
||||
// Skip virtual interfaces.
|
||||
if vis.contains(string(bytes.TrimSpace(fields[len(fields)-1]))) {
|
||||
continue
|
||||
}
|
||||
|
||||
gw := make([]byte, net.IPv6len)
|
||||
// Fifth fields is gateway.
|
||||
@@ -95,3 +105,26 @@ func dnsFromSystemdResolver() []string {
|
||||
}
|
||||
return ns
|
||||
}
|
||||
|
||||
type set map[string]struct{}
|
||||
|
||||
func (s *set) add(e string) {
|
||||
(*s)[e] = struct{}{}
|
||||
}
|
||||
|
||||
func (s *set) contains(e string) bool {
|
||||
_, ok := (*s)[e]
|
||||
return ok
|
||||
}
|
||||
|
||||
// virtualInterfaces returns a set of virtual interfaces on current machine.
|
||||
func virtualInterfaces() set {
|
||||
s := make(set)
|
||||
entries, _ := os.ReadDir("/sys/devices/virtual/net")
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
s.add(strings.TrimSpace(entry.Name()))
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
10
nameservers_linux_test.go
Normal file
10
nameservers_linux_test.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package ctrld
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_virtualInterfaces(t *testing.T) {
|
||||
vis := virtualInterfaces()
|
||||
t.Log(vis)
|
||||
}
|
||||
@@ -1,44 +1,473 @@
|
||||
package ctrld
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/microsoft/wmi/pkg/base/host"
|
||||
"github.com/microsoft/wmi/pkg/base/instance"
|
||||
"github.com/microsoft/wmi/pkg/base/query"
|
||||
"github.com/microsoft/wmi/pkg/constant"
|
||||
"github.com/microsoft/wmi/pkg/hardware/network/netadapter"
|
||||
"github.com/rs/zerolog"
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||
)
|
||||
|
||||
const (
|
||||
maxDNSAdapterRetries = 5
|
||||
retryDelayDNSAdapter = 1 * time.Second
|
||||
defaultDNSAdapterTimeout = 10 * time.Second
|
||||
minDNSServers = 1 // Minimum number of DNS servers we want to find
|
||||
NetSetupUnknown uint32 = 0
|
||||
NetSetupWorkgroup uint32 = 1
|
||||
NetSetupDomain uint32 = 2
|
||||
NetSetupCloudDomain uint32 = 3
|
||||
DS_FORCE_REDISCOVERY = 0x00000001
|
||||
DS_DIRECTORY_SERVICE_REQUIRED = 0x00000010
|
||||
DS_BACKGROUND_ONLY = 0x00000100
|
||||
DS_IP_REQUIRED = 0x00000200
|
||||
DS_IS_DNS_NAME = 0x00020000
|
||||
DS_RETURN_DNS_NAME = 0x40000000
|
||||
)
|
||||
|
||||
type DomainControllerInfo struct {
|
||||
DomainControllerName *uint16
|
||||
DomainControllerAddress *uint16
|
||||
DomainControllerAddressType uint32
|
||||
DomainGuid windows.GUID
|
||||
DomainName *uint16
|
||||
DnsForestName *uint16
|
||||
Flags uint32
|
||||
DcSiteName *uint16
|
||||
ClientSiteName *uint16
|
||||
}
|
||||
|
||||
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
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultDNSAdapterTimeout)
|
||||
defer cancel()
|
||||
|
||||
var ns []string
|
||||
var err error
|
||||
|
||||
logger := zerolog.New(io.Discard)
|
||||
if ProxyLogger.Load() != nil {
|
||||
logger = *ProxyLogger.Load()
|
||||
}
|
||||
|
||||
for i := 0; i < maxDNSAdapterRetries; i++ {
|
||||
if ctx.Err() != nil {
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"dnsFromAdapter lookup cancelled or timed out, attempt %d", i)
|
||||
return nil
|
||||
}
|
||||
|
||||
ns, err = getDNSServers(ctx)
|
||||
if err == nil && len(ns) >= minDNSServers {
|
||||
if i > 0 {
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"Successfully got DNS servers after %d attempts, found %d servers",
|
||||
i+1, len(ns))
|
||||
}
|
||||
return ns
|
||||
}
|
||||
|
||||
// if osResolver is not initialized, this is likely a command line run
|
||||
// and ctrld is already on the interface, abort retries
|
||||
if or == nil {
|
||||
return ns
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"Failed to get DNS servers, attempt %d: %v", i+1, err)
|
||||
} else {
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"Got insufficient DNS servers, retrying, found %d servers", len(ns))
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-time.After(retryDelayDNSAdapter):
|
||||
}
|
||||
}
|
||||
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"Failed to get sufficient DNS servers after all attempts, max_retries=%d", maxDNSAdapterRetries)
|
||||
return ns
|
||||
}
|
||||
|
||||
func getDNSServers(ctx context.Context) ([]string, error) {
|
||||
logger := zerolog.New(io.Discard)
|
||||
if ProxyLogger.Load() != nil {
|
||||
logger = *ProxyLogger.Load()
|
||||
}
|
||||
// Check context before making the call
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
// Get DNS servers from adapters (existing method)
|
||||
flags := winipcfg.GAAFlagIncludeGateways |
|
||||
winipcfg.GAAFlagIncludePrefix
|
||||
|
||||
aas, err := winipcfg.GetAdaptersAddresses(syscall.AF_UNSPEC, flags)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting adapters: %w", err)
|
||||
}
|
||||
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"Found network adapters, count=%d", len(aas))
|
||||
|
||||
// Try to get domain controller info if domain-joined
|
||||
var dcServers []string
|
||||
isDomain := checkDomainJoined()
|
||||
if isDomain {
|
||||
domainName, err := getLocalADDomain()
|
||||
if err != nil {
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"Failed to get local AD domain: %v", err)
|
||||
} else {
|
||||
// Load netapi32.dll
|
||||
netapi32 := windows.NewLazySystemDLL("netapi32.dll")
|
||||
dsDcName := netapi32.NewProc("DsGetDcNameW")
|
||||
|
||||
var info *DomainControllerInfo
|
||||
flags := uint32(DS_RETURN_DNS_NAME | DS_IP_REQUIRED | DS_IS_DNS_NAME)
|
||||
|
||||
domainUTF16, err := windows.UTF16PtrFromString(domainName)
|
||||
if err != nil {
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"Failed to convert domain name to UTF16: %v", err)
|
||||
} else {
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"Attempting to get DC for domain: %s with flags: 0x%x", domainName, flags)
|
||||
|
||||
// Call DsGetDcNameW with domain name
|
||||
ret, _, err := dsDcName.Call(
|
||||
0, // ComputerName - can be NULL
|
||||
uintptr(unsafe.Pointer(domainUTF16)), // DomainName
|
||||
0, // DomainGuid - not needed
|
||||
0, // SiteName - not needed
|
||||
uintptr(flags), // Flags
|
||||
uintptr(unsafe.Pointer(&info))) // DomainControllerInfo - output
|
||||
|
||||
if ret != 0 {
|
||||
switch ret {
|
||||
case 1355: // ERROR_NO_SUCH_DOMAIN
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"Domain not found: %s (%d)", domainName, ret)
|
||||
case 1311: // ERROR_NO_LOGON_SERVERS
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"No logon servers available for domain: %s (%d)", domainName, ret)
|
||||
case 1004: // ERROR_DC_NOT_FOUND
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"Domain controller not found for domain: %s (%d)", domainName, ret)
|
||||
case 1722: // RPC_S_SERVER_UNAVAILABLE
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"RPC server unavailable for domain: %s (%d)", domainName, ret)
|
||||
default:
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"Failed to get domain controller info for domain %s: %d, %v", domainName, ret, err)
|
||||
}
|
||||
} else if info != nil {
|
||||
defer windows.NetApiBufferFree((*byte)(unsafe.Pointer(info)))
|
||||
|
||||
if info.DomainControllerAddress != nil {
|
||||
dcAddr := windows.UTF16PtrToString(info.DomainControllerAddress)
|
||||
dcAddr = strings.TrimPrefix(dcAddr, "\\\\")
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"Found domain controller address: %s", dcAddr)
|
||||
|
||||
if ip := net.ParseIP(dcAddr); ip != nil {
|
||||
dcServers = append(dcServers, ip.String())
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"Added domain controller DNS servers: %v", dcServers)
|
||||
}
|
||||
} else {
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"No domain controller address found")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Continue with existing adapter DNS collection
|
||||
ns := make([]string, 0, len(aas)*2)
|
||||
seen := make(map[string]bool)
|
||||
addressMap := make(map[string]struct{})
|
||||
|
||||
// Collect all local IPs
|
||||
for _, aa := range aas {
|
||||
if aa.OperStatus != winipcfg.IfOperStatusUp {
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"Skipping adapter %s - not up, status: %d", aa.FriendlyName(), aa.OperStatus)
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip if software loopback or other non-physical types
|
||||
// This is to avoid the "Loopback Pseudo-Interface 1" issue we see on windows
|
||||
if aa.IfType == winipcfg.IfTypeSoftwareLoopback {
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"Skipping %s (software loopback)", aa.FriendlyName())
|
||||
continue
|
||||
}
|
||||
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"Processing adapter %s", aa.FriendlyName())
|
||||
|
||||
for a := aa.FirstUnicastAddress; a != nil; a = a.Next {
|
||||
addressMap[a.Address.IP().String()] = struct{}{}
|
||||
ip := a.Address.IP().String()
|
||||
addressMap[ip] = struct{}{}
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"Added local IP %s from adapter %s", ip, aa.FriendlyName())
|
||||
}
|
||||
}
|
||||
|
||||
validInterfacesMap := validInterfaces()
|
||||
|
||||
// Collect DNS servers
|
||||
for _, aa := range aas {
|
||||
if aa.OperStatus != winipcfg.IfOperStatusUp {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip if software loopback or other non-physical types
|
||||
// This is to avoid the "Loopback Pseudo-Interface 1" issue we see on windows
|
||||
if aa.IfType == winipcfg.IfTypeSoftwareLoopback {
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"Skipping %s (software loopback)", aa.FriendlyName())
|
||||
continue
|
||||
}
|
||||
|
||||
// if not in the validInterfacesMap, skip
|
||||
if _, ok := validInterfacesMap[aa.FriendlyName()]; !ok {
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"Skipping %s (not in validInterfacesMap)", aa.FriendlyName())
|
||||
continue
|
||||
}
|
||||
|
||||
for dns := aa.FirstDNSServerAddress; dns != nil; dns = dns.Next {
|
||||
ip := dns.Address.IP()
|
||||
if ip == nil || ip.IsLoopback() || seen[ip.String()] {
|
||||
if ip == nil {
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"Skipping nil IP from adapter %s", aa.FriendlyName())
|
||||
continue
|
||||
}
|
||||
if _, ok := addressMap[ip.String()]; ok {
|
||||
|
||||
ipStr := ip.String()
|
||||
l := logger.Debug().
|
||||
Str("ip", ipStr).
|
||||
Str("adapter", aa.FriendlyName())
|
||||
|
||||
if ip.IsLoopback() {
|
||||
l.Msg("Skipping loopback IP")
|
||||
continue
|
||||
}
|
||||
seen[ip.String()] = true
|
||||
ns = append(ns, ip.String())
|
||||
if seen[ipStr] {
|
||||
l.Msg("Skipping duplicate IP")
|
||||
continue
|
||||
}
|
||||
if _, ok := addressMap[ipStr]; ok {
|
||||
l.Msg("Skipping local interface IP")
|
||||
continue
|
||||
}
|
||||
|
||||
seen[ipStr] = true
|
||||
ns = append(ns, ipStr)
|
||||
l.Msg("Added DNS server")
|
||||
}
|
||||
}
|
||||
return ns
|
||||
|
||||
// Add DC servers if they're not already in the list
|
||||
for _, dcServer := range dcServers {
|
||||
if !seen[dcServer] {
|
||||
seen[dcServer] = true
|
||||
ns = append(ns, dcServer)
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"Added additional domain controller DNS server: %s", dcServer)
|
||||
}
|
||||
}
|
||||
|
||||
if len(ns) == 0 {
|
||||
return nil, fmt.Errorf("no valid DNS servers found")
|
||||
}
|
||||
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"DNS server discovery completed, count=%d, servers=%v (including %d DC servers)",
|
||||
len(ns), ns, len(dcServers))
|
||||
return ns, nil
|
||||
}
|
||||
|
||||
func nameserversFromResolvconf() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkDomainJoined checks if the machine is joined to an Active Directory domain
|
||||
// Returns whether it's domain joined and the domain name if available
|
||||
func checkDomainJoined() bool {
|
||||
logger := zerolog.New(io.Discard)
|
||||
if ProxyLogger.Load() != nil {
|
||||
logger = *ProxyLogger.Load()
|
||||
}
|
||||
var domain *uint16
|
||||
var status uint32
|
||||
|
||||
err := windows.NetGetJoinInformation(nil, &domain, &status)
|
||||
if err != nil {
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"Failed to get domain join status: %v", err)
|
||||
return false
|
||||
}
|
||||
defer windows.NetApiBufferFree((*byte)(unsafe.Pointer(domain)))
|
||||
|
||||
domainName := windows.UTF16PtrToString(domain)
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"Domain join status: domain=%s status=%d (Unknown=0, Workgroup=1, Domain=2, CloudDomain=3)",
|
||||
domainName, status)
|
||||
|
||||
// Consider domain or cloud domain as domain-joined
|
||||
isDomain := status == NetSetupDomain || status == NetSetupCloudDomain
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"Is domain joined? status=%d, traditional=%v, cloud=%v, result=%v",
|
||||
status,
|
||||
status == NetSetupDomain,
|
||||
status == NetSetupCloudDomain,
|
||||
isDomain)
|
||||
|
||||
return isDomain
|
||||
}
|
||||
|
||||
// getLocalADDomain uses Microsoft's WMI wrappers (github.com/microsoft/wmi/pkg/*)
|
||||
// to query the Domain field from Win32_ComputerSystem instead of a direct go-ole call.
|
||||
func getLocalADDomain() (string, error) {
|
||||
log.SetOutput(io.Discard)
|
||||
defer log.SetOutput(os.Stderr)
|
||||
// 1) Check environment variable
|
||||
envDomain := os.Getenv("USERDNSDOMAIN")
|
||||
if envDomain != "" {
|
||||
return strings.TrimSpace(envDomain), nil
|
||||
}
|
||||
|
||||
// 2) Query WMI via the microsoft/wmi library
|
||||
whost := host.NewWmiLocalHost()
|
||||
q := query.NewWmiQuery("Win32_ComputerSystem")
|
||||
instances, err := instance.GetWmiInstancesFromHost(whost, string(constant.CimV2), q)
|
||||
if instances != nil {
|
||||
defer instances.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("WMI query failed: %v", err)
|
||||
}
|
||||
|
||||
// If no results, return an error
|
||||
if len(instances) == 0 {
|
||||
return "", fmt.Errorf("no rows returned from Win32_ComputerSystem")
|
||||
}
|
||||
|
||||
// We only care about the first row
|
||||
domainVal, err := instances[0].GetProperty("Domain")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("machine does not appear to have a domain set: %v", err)
|
||||
}
|
||||
|
||||
domainName := strings.TrimSpace(fmt.Sprintf("%v", domainVal))
|
||||
if domainName == "" {
|
||||
return "", fmt.Errorf("machine does not appear to have a domain set")
|
||||
}
|
||||
return domainName, nil
|
||||
}
|
||||
|
||||
// validInterfaces returns a list of all physical interfaces.
|
||||
// this is a duplicate of what is in net_windows.go, we should
|
||||
// clean this up so there is only one version
|
||||
func validInterfaces() map[string]struct{} {
|
||||
log.SetOutput(io.Discard)
|
||||
defer log.SetOutput(os.Stderr)
|
||||
|
||||
//load the logger
|
||||
logger := zerolog.New(io.Discard)
|
||||
if ProxyLogger.Load() != nil {
|
||||
logger = *ProxyLogger.Load()
|
||||
}
|
||||
|
||||
whost := host.NewWmiLocalHost()
|
||||
q := query.NewWmiQuery("MSFT_NetAdapter")
|
||||
instances, err := instance.GetWmiInstancesFromHost(whost, string(constant.StadardCimV2), q)
|
||||
if instances != nil {
|
||||
defer instances.Close()
|
||||
}
|
||||
if err != nil {
|
||||
Log(context.Background(), logger.Warn(),
|
||||
"failed to get wmi network adapter: %v", err)
|
||||
return nil
|
||||
}
|
||||
var adapters []string
|
||||
for _, i := range instances {
|
||||
adapter, err := netadapter.NewNetworkAdapter(i)
|
||||
if err != nil {
|
||||
Log(context.Background(), logger.Warn(),
|
||||
"failed to get network adapter: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
name, err := adapter.GetPropertyName()
|
||||
if err != nil {
|
||||
Log(context.Background(), logger.Warn(),
|
||||
"failed to get interface name: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// From: https://learn.microsoft.com/en-us/previous-versions/windows/desktop/legacy/hh968170(v=vs.85)
|
||||
//
|
||||
// "Indicates if a connector is present on the network adapter. This value is set to TRUE
|
||||
// if this is a physical adapter or FALSE if this is not a physical adapter."
|
||||
physical, err := adapter.GetPropertyConnectorPresent()
|
||||
if err != nil {
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"failed to get network adapter connector present property: %v", err)
|
||||
continue
|
||||
}
|
||||
if !physical {
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"skipping non-physical adapter: %s", name)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if it's a hardware interface. Checking only for connector present is not enough
|
||||
// because some interfaces are not physical but have a connector.
|
||||
hardware, err := adapter.GetPropertyHardwareInterface()
|
||||
if err != nil {
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"failed to get network adapter hardware interface property: %v", err)
|
||||
continue
|
||||
}
|
||||
if !hardware {
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"skipping non-hardware interface: %s", name)
|
||||
continue
|
||||
}
|
||||
|
||||
adapters = append(adapters, name)
|
||||
}
|
||||
|
||||
m := make(map[string]struct{})
|
||||
for _, ifaceName := range adapters {
|
||||
m[ifaceName] = struct{}{}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
366
resolver.go
366
resolver.go
@@ -4,14 +4,19 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"runtime"
|
||||
"slices"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"tailscale.com/net/interfaces"
|
||||
"github.com/rs/zerolog"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/tsaddr"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -27,8 +32,13 @@ const (
|
||||
ResolverTypeOS = "os"
|
||||
// ResolverTypeLegacy specifies legacy resolver.
|
||||
ResolverTypeLegacy = "legacy"
|
||||
// ResolverTypePrivate is like ResolverTypeOS, but use for local resolver only.
|
||||
// ResolverTypePrivate is like ResolverTypeOS, but use for private resolver only.
|
||||
ResolverTypePrivate = "private"
|
||||
// ResolverTypeLocal is like ResolverTypeOS, but use for local resolver only.
|
||||
ResolverTypeLocal = "local"
|
||||
// ResolverTypeSDNS specifies resolver with information encoded using DNS Stamps.
|
||||
// See: https://dnscrypt.info/stamps-specifications/
|
||||
ResolverTypeSDNS = "sdns"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -38,13 +48,81 @@ const (
|
||||
|
||||
var controldPublicDnsWithPort = net.JoinHostPort(controldPublicDns, "53")
|
||||
|
||||
// or is the Resolver used for ResolverTypeOS.
|
||||
var or = &osResolver{nameservers: defaultNameservers()}
|
||||
var localResolver = newLocalResolver()
|
||||
|
||||
// defaultNameservers returns OS nameservers plus ControlD public DNS.
|
||||
var (
|
||||
resolverMutex sync.Mutex
|
||||
or *osResolver
|
||||
defaultLocalIPv4 atomic.Value // holds net.IP (IPv4)
|
||||
defaultLocalIPv6 atomic.Value // holds net.IP (IPv6)
|
||||
)
|
||||
|
||||
func newLocalResolver() Resolver {
|
||||
var nss []string
|
||||
for _, addr := range Rfc1918Addresses() {
|
||||
nss = append(nss, net.JoinHostPort(addr, "53"))
|
||||
}
|
||||
return NewResolverWithNameserver(nss)
|
||||
}
|
||||
|
||||
// LanQueryCtxKey is the context.Context key to indicate that the request is for LAN network.
|
||||
type LanQueryCtxKey struct{}
|
||||
|
||||
// LanQueryCtx returns a context.Context with LanQueryCtxKey set.
|
||||
func LanQueryCtx(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, LanQueryCtxKey{}, true)
|
||||
}
|
||||
|
||||
// defaultNameservers is like nameservers with each element formed "ip:53".
|
||||
func defaultNameservers() []string {
|
||||
ns := nameservers()
|
||||
return ns
|
||||
nss := make([]string, len(ns))
|
||||
for i := range ns {
|
||||
nss[i] = net.JoinHostPort(ns[i], "53")
|
||||
}
|
||||
return nss
|
||||
}
|
||||
|
||||
// availableNameservers returns list of current available DNS servers of the system.
|
||||
func availableNameservers() []string {
|
||||
var nss []string
|
||||
// Ignore local addresses to prevent loop.
|
||||
regularIPs, loopbackIPs, _ := netmon.LocalAddresses()
|
||||
machineIPsMap := make(map[string]struct{}, len(regularIPs))
|
||||
|
||||
//load the logger
|
||||
logger := zerolog.New(io.Discard)
|
||||
if ProxyLogger.Load() != nil {
|
||||
logger = *ProxyLogger.Load()
|
||||
}
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"Got local addresses - regular IPs: %v, loopback IPs: %v", regularIPs, loopbackIPs)
|
||||
|
||||
for _, v := range slices.Concat(regularIPs, loopbackIPs) {
|
||||
ipStr := v.String()
|
||||
machineIPsMap[ipStr] = struct{}{}
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"Added local IP to OS resolverexclusion map: %s", ipStr)
|
||||
}
|
||||
|
||||
systemNameservers := nameservers()
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"Got system nameservers: %v", systemNameservers)
|
||||
|
||||
for _, ns := range systemNameservers {
|
||||
if _, ok := machineIPsMap[ns]; ok {
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"Skipping local nameserver: %s", ns)
|
||||
continue
|
||||
}
|
||||
nss = append(nss, ns)
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"Added non-local nameserver: %s", ns)
|
||||
}
|
||||
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"Final available nameservers: %v", nss)
|
||||
return nss
|
||||
}
|
||||
|
||||
// InitializeOsResolver initializes OS resolver using the current system DNS settings.
|
||||
@@ -52,26 +130,47 @@ func defaultNameservers() []string {
|
||||
//
|
||||
// It's the caller's responsibility to ensure the system DNS is in a clean state before
|
||||
// calling this function.
|
||||
func InitializeOsResolver() []string {
|
||||
or.nameservers = or.nameservers[:0]
|
||||
for _, ns := range defaultNameservers() {
|
||||
if testNameserver(ns) {
|
||||
or.nameservers = append(or.nameservers, ns)
|
||||
}
|
||||
func InitializeOsResolver(guardAgainstNoNameservers bool) []string {
|
||||
nameservers := availableNameservers()
|
||||
// if no nameservers, return empty slice so we dont remove all nameservers
|
||||
if len(nameservers) == 0 && guardAgainstNoNameservers {
|
||||
return []string{}
|
||||
}
|
||||
or.nameservers = append(or.nameservers, controldPublicDnsWithPort)
|
||||
return or.nameservers
|
||||
ns := initializeOsResolver(nameservers)
|
||||
resolverMutex.Lock()
|
||||
defer resolverMutex.Unlock()
|
||||
or = newResolverWithNameserver(ns)
|
||||
return ns
|
||||
}
|
||||
|
||||
// testPlainDnsNameserver sends a test query to DNS nameserver to check if the server is available.
|
||||
func testNameserver(addr string) bool {
|
||||
msg := new(dns.Msg)
|
||||
msg.SetQuestion(".", dns.TypeNS)
|
||||
client := new(dns.Client)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
_, _, err := client.ExchangeContext(ctx, msg, addr)
|
||||
return err == nil
|
||||
// initializeOsResolver performs logic for choosing OS resolver nameserver.
|
||||
// The logic:
|
||||
//
|
||||
// - First available LAN servers are saved and store.
|
||||
// - Later calls, if no LAN servers available, the saved servers above will be used.
|
||||
func initializeOsResolver(servers []string) []string {
|
||||
|
||||
var lanNss, publicNss []string
|
||||
|
||||
// First categorize servers
|
||||
for _, ns := range servers {
|
||||
addr, err := netip.ParseAddr(ns)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
server := net.JoinHostPort(ns, "53")
|
||||
if isLanAddr(addr) {
|
||||
lanNss = append(lanNss, server)
|
||||
} else {
|
||||
publicNss = append(publicNss, server)
|
||||
}
|
||||
}
|
||||
|
||||
if len(publicNss) == 0 {
|
||||
publicNss = []string{controldPublicDnsWithPort}
|
||||
}
|
||||
|
||||
return slices.Concat(lanNss, publicNss)
|
||||
}
|
||||
|
||||
// Resolver is the interface that wraps the basic DNS operations.
|
||||
@@ -94,75 +193,195 @@ func NewResolver(uc *UpstreamConfig) (Resolver, error) {
|
||||
case ResolverTypeDOQ:
|
||||
return &doqResolver{uc: uc}, nil
|
||||
case ResolverTypeOS:
|
||||
if or == nil {
|
||||
or = newResolverWithNameserver(defaultNameservers())
|
||||
}
|
||||
return or, nil
|
||||
case ResolverTypeLegacy:
|
||||
return &legacyResolver{uc: uc}, nil
|
||||
case ResolverTypePrivate:
|
||||
return NewPrivateResolver(), nil
|
||||
case ResolverTypeLocal:
|
||||
return localResolver, nil
|
||||
}
|
||||
return nil, fmt.Errorf("%w: %s", errUnknownResolver, typ)
|
||||
}
|
||||
|
||||
type osResolver struct {
|
||||
nameservers []string
|
||||
lanServers atomic.Pointer[[]string]
|
||||
publicServers atomic.Pointer[[]string]
|
||||
}
|
||||
|
||||
type osResolverResult struct {
|
||||
answer *dns.Msg
|
||||
err error
|
||||
isControlDPublicDNS bool
|
||||
answer *dns.Msg
|
||||
err error
|
||||
server string
|
||||
lan bool
|
||||
}
|
||||
|
||||
type publicResponse struct {
|
||||
answer *dns.Msg
|
||||
server string
|
||||
}
|
||||
|
||||
// SetDefaultLocalIPv4 updates the stored local IPv4.
|
||||
func SetDefaultLocalIPv4(ip net.IP) {
|
||||
Log(context.Background(), ProxyLogger.Load().Debug(), "SetDefaultLocalIPv4: %s", ip)
|
||||
defaultLocalIPv4.Store(ip)
|
||||
}
|
||||
|
||||
// SetDefaultLocalIPv6 updates the stored local IPv6.
|
||||
func SetDefaultLocalIPv6(ip net.IP) {
|
||||
Log(context.Background(), ProxyLogger.Load().Debug(), "SetDefaultLocalIPv6: %s", ip)
|
||||
defaultLocalIPv6.Store(ip)
|
||||
}
|
||||
|
||||
// GetDefaultLocalIPv4 returns the stored local IPv4 or nil if none.
|
||||
func GetDefaultLocalIPv4() net.IP {
|
||||
if v := defaultLocalIPv4.Load(); v != nil {
|
||||
return v.(net.IP)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDefaultLocalIPv6 returns the stored local IPv6 or nil if none.
|
||||
func GetDefaultLocalIPv6() net.IP {
|
||||
if v := defaultLocalIPv6.Load(); v != nil {
|
||||
return v.(net.IP)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// customDNSExchange wraps the DNS exchange to use our debug dialer.
|
||||
// It uses dns.ExchangeWithConn so that our custom dialer is used directly.
|
||||
func customDNSExchange(ctx context.Context, msg *dns.Msg, server string, desiredLocalIP net.IP) (*dns.Msg, time.Duration, error) {
|
||||
baseDialer := &net.Dialer{
|
||||
Timeout: 3 * time.Second,
|
||||
Resolver: &net.Resolver{PreferGo: true},
|
||||
}
|
||||
if desiredLocalIP != nil {
|
||||
baseDialer.LocalAddr = &net.UDPAddr{IP: desiredLocalIP, Port: 0}
|
||||
}
|
||||
dnsClient := &dns.Client{Net: "udp"}
|
||||
dnsClient.Dialer = baseDialer
|
||||
return dnsClient.ExchangeContext(ctx, msg, server)
|
||||
}
|
||||
|
||||
// Resolve resolves DNS queries using pre-configured nameservers.
|
||||
// Query is sent to all nameservers concurrently, and the first
|
||||
// success response will be returned.
|
||||
func (o *osResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) {
|
||||
numServers := len(o.nameservers)
|
||||
publicServers := *o.publicServers.Load()
|
||||
var nss []string
|
||||
if p := o.lanServers.Load(); p != nil {
|
||||
nss = append(nss, (*p)...)
|
||||
}
|
||||
numServers := len(nss) + len(publicServers)
|
||||
// If this is a LAN query, skip public DNS.
|
||||
lan, ok := ctx.Value(LanQueryCtxKey{}).(bool)
|
||||
if ok && lan {
|
||||
numServers -= len(publicServers)
|
||||
}
|
||||
if numServers == 0 {
|
||||
return nil, errors.New("no nameservers available")
|
||||
}
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
dnsClient := &dns.Client{Net: "udp"}
|
||||
ch := make(chan *osResolverResult, numServers)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(o.nameservers))
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(numServers)
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(ch)
|
||||
}()
|
||||
for _, server := range o.nameservers {
|
||||
go func(server string) {
|
||||
defer wg.Done()
|
||||
answer, _, err := dnsClient.ExchangeContext(ctx, msg.Copy(), server)
|
||||
ch <- &osResolverResult{answer: answer, err: err, isControlDPublicDNS: server == controldPublicDnsWithPort}
|
||||
}(server)
|
||||
|
||||
do := func(servers []string, isLan bool) {
|
||||
for _, server := range servers {
|
||||
go func(server string) {
|
||||
defer wg.Done()
|
||||
var answer *dns.Msg
|
||||
var err error
|
||||
var localOSResolverIP net.IP
|
||||
if runtime.GOOS == "darwin" {
|
||||
host, _, err := net.SplitHostPort(server)
|
||||
if err == nil {
|
||||
ip := net.ParseIP(host)
|
||||
if ip != nil && ip.To4() == nil {
|
||||
// IPv6 nameserver; use default IPv6 address (if set)
|
||||
localOSResolverIP = GetDefaultLocalIPv6()
|
||||
} else {
|
||||
localOSResolverIP = GetDefaultLocalIPv4()
|
||||
}
|
||||
}
|
||||
}
|
||||
answer, _, err = customDNSExchange(ctx, msg.Copy(), server, localOSResolverIP)
|
||||
ch <- &osResolverResult{answer: answer, err: err, server: server, lan: isLan}
|
||||
}(server)
|
||||
}
|
||||
}
|
||||
do(nss, true)
|
||||
if !lan {
|
||||
do(publicServers, false)
|
||||
}
|
||||
|
||||
logAnswer := func(server string) {
|
||||
host, _, err := net.SplitHostPort(server)
|
||||
if err != nil {
|
||||
// If splitting fails, fallback to the original server string
|
||||
host = server
|
||||
}
|
||||
Log(ctx, ProxyLogger.Load().Debug(), "got answer from nameserver: %s", host)
|
||||
}
|
||||
var (
|
||||
nonSuccessAnswer *dns.Msg
|
||||
nonSuccessServer string
|
||||
controldSuccessAnswer *dns.Msg
|
||||
publicResponses []publicResponse
|
||||
)
|
||||
errs := make([]error, 0, numServers)
|
||||
for res := range ch {
|
||||
switch {
|
||||
case res.answer != nil && res.answer.Rcode == dns.RcodeSuccess:
|
||||
if res.isControlDPublicDNS {
|
||||
controldSuccessAnswer = res.answer // only use ControlD answer as last one.
|
||||
} else {
|
||||
switch {
|
||||
case res.lan:
|
||||
// Always prefer LAN responses immediately
|
||||
Log(ctx, ProxyLogger.Load().Debug(), "using LAN answer from: %s", res.server)
|
||||
cancel()
|
||||
logAnswer(res.server)
|
||||
return res.answer, nil
|
||||
case res.server == controldPublicDnsWithPort:
|
||||
controldSuccessAnswer = res.answer
|
||||
case !res.lan:
|
||||
publicResponses = append(publicResponses, publicResponse{
|
||||
answer: res.answer,
|
||||
server: res.server,
|
||||
})
|
||||
}
|
||||
case res.answer != nil:
|
||||
nonSuccessAnswer = res.answer
|
||||
nonSuccessServer = res.server
|
||||
Log(ctx, ProxyLogger.Load().Debug(), "got non-success answer from: %s with code: %d",
|
||||
res.server, res.answer.Rcode)
|
||||
}
|
||||
errs = append(errs, res.err)
|
||||
}
|
||||
for _, answer := range []*dns.Msg{controldSuccessAnswer, nonSuccessAnswer} {
|
||||
if answer != nil {
|
||||
return answer, nil
|
||||
}
|
||||
|
||||
if len(publicResponses) > 0 {
|
||||
resp := publicResponses[0]
|
||||
Log(ctx, ProxyLogger.Load().Debug(), "got public answer from: %s", resp.server)
|
||||
logAnswer(resp.server)
|
||||
return resp.answer, nil
|
||||
}
|
||||
if controldSuccessAnswer != nil {
|
||||
Log(ctx, ProxyLogger.Load().Debug(), "got ControlD answer from: %s", controldPublicDnsWithPort)
|
||||
logAnswer(controldPublicDnsWithPort)
|
||||
return controldSuccessAnswer, nil
|
||||
}
|
||||
if nonSuccessAnswer != nil {
|
||||
Log(ctx, ProxyLogger.Load().Debug(), "got non-success answer from: %s", nonSuccessServer)
|
||||
logAnswer(nonSuccessServer)
|
||||
return nonSuccessAnswer, nil
|
||||
}
|
||||
return nil, errors.Join(errs...)
|
||||
}
|
||||
@@ -209,11 +428,16 @@ func LookupIP(domain string) []string {
|
||||
}
|
||||
|
||||
func lookupIP(domain string, timeout int, withBootstrapDNS bool) (ips []string) {
|
||||
resolver := &osResolver{nameservers: nameservers()}
|
||||
if withBootstrapDNS {
|
||||
resolver.nameservers = append([]string{net.JoinHostPort(controldBootstrapDns, "53")}, resolver.nameservers...)
|
||||
if or == nil {
|
||||
or = newResolverWithNameserver(defaultNameservers())
|
||||
}
|
||||
ProxyLogger.Load().Debug().Msgf("resolving %q using bootstrap DNS %q", domain, resolver.nameservers)
|
||||
nss := *or.lanServers.Load()
|
||||
nss = append(nss, *or.publicServers.Load()...)
|
||||
if withBootstrapDNS {
|
||||
nss = append([]string{net.JoinHostPort(controldBootstrapDns, "53")}, nss...)
|
||||
}
|
||||
resolver := newResolverWithNameserver(nss)
|
||||
ProxyLogger.Load().Debug().Msgf("resolving %q using bootstrap DNS %q", domain, nss)
|
||||
timeoutMs := 2000
|
||||
if timeout > 0 && timeout < timeoutMs {
|
||||
timeoutMs = timeout
|
||||
@@ -286,12 +510,12 @@ func lookupIP(domain string, timeout int, withBootstrapDNS bool) (ips []string)
|
||||
// - Gateway IP address (depends on OS).
|
||||
// - Input servers.
|
||||
func NewBootstrapResolver(servers ...string) Resolver {
|
||||
resolver := &osResolver{nameservers: nameservers()}
|
||||
resolver.nameservers = append([]string{controldPublicDnsWithPort}, resolver.nameservers...)
|
||||
nss := defaultNameservers()
|
||||
nss = append([]string{controldPublicDnsWithPort}, nss...)
|
||||
for _, ns := range servers {
|
||||
resolver.nameservers = append([]string{net.JoinHostPort(ns, "53")}, resolver.nameservers...)
|
||||
nss = append([]string{net.JoinHostPort(ns, "53")}, nss...)
|
||||
}
|
||||
return resolver
|
||||
return NewResolverWithNameserver(nss)
|
||||
}
|
||||
|
||||
// NewPrivateResolver returns an OS resolver, which includes only private DNS servers,
|
||||
@@ -302,7 +526,7 @@ func NewBootstrapResolver(servers ...string) Resolver {
|
||||
//
|
||||
// This is useful for doing PTR lookup in LAN network.
|
||||
func NewPrivateResolver() Resolver {
|
||||
nss := nameservers()
|
||||
nss := defaultNameservers()
|
||||
resolveConfNss := nameserversFromResolvconf()
|
||||
localRfc1918Addrs := Rfc1918Addresses()
|
||||
n := 0
|
||||
@@ -328,10 +552,10 @@ func NewPrivateResolver() Resolver {
|
||||
}
|
||||
}
|
||||
nss = nss[:n]
|
||||
return NewResolverWithNameserver(nss)
|
||||
return newResolverWithNameserver(nss)
|
||||
}
|
||||
|
||||
// NewResolverWithNameserver returns an OS resolver which uses the given nameservers
|
||||
// NewResolverWithNameserver returns a Resolver which uses the given nameservers
|
||||
// for resolving DNS queries. If nameservers is empty, a dummy resolver will be returned.
|
||||
//
|
||||
// Each nameserver must be form "host:port". It's the caller responsibility to ensure all
|
||||
@@ -340,13 +564,33 @@ func NewResolverWithNameserver(nameservers []string) Resolver {
|
||||
if len(nameservers) == 0 {
|
||||
return &dummyResolver{}
|
||||
}
|
||||
return &osResolver{nameservers: nameservers}
|
||||
return newResolverWithNameserver(nameservers)
|
||||
}
|
||||
|
||||
// newResolverWithNameserver returns an OS resolver from given nameservers list.
|
||||
// The caller must ensure each server in list is formed "ip:53".
|
||||
func newResolverWithNameserver(nameservers []string) *osResolver {
|
||||
r := &osResolver{}
|
||||
var publicNss []string
|
||||
var lanNss []string
|
||||
for _, ns := range slices.Sorted(slices.Values(nameservers)) {
|
||||
ip, _, _ := net.SplitHostPort(ns)
|
||||
addr, _ := netip.ParseAddr(ip)
|
||||
if isLanAddr(addr) {
|
||||
lanNss = append(lanNss, ns)
|
||||
} else {
|
||||
publicNss = append(publicNss, ns)
|
||||
}
|
||||
}
|
||||
r.lanServers.Store(&lanNss)
|
||||
r.publicServers.Store(&publicNss)
|
||||
return r
|
||||
}
|
||||
|
||||
// Rfc1918Addresses returns the list of local interfaces private IP addresses
|
||||
func Rfc1918Addresses() []string {
|
||||
var res []string
|
||||
interfaces.ForeachInterface(func(i interfaces.Interface, prefixes []netip.Prefix) {
|
||||
netmon.ForeachInterface(func(i netmon.Interface, prefixes []netip.Prefix) {
|
||||
addrs, _ := i.Addrs()
|
||||
for _, addr := range addrs {
|
||||
ipNet, ok := addr.(*net.IPNet)
|
||||
@@ -370,3 +614,11 @@ func newDialer(dnsAddress string) *net.Dialer {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// isLanAddr reports whether addr is considered a LAN ip address.
|
||||
func isLanAddr(addr netip.Addr) bool {
|
||||
return addr.IsPrivate() ||
|
||||
addr.IsLoopback() ||
|
||||
addr.IsLinkLocalUnicast() ||
|
||||
tsaddr.CGNATRange().Contains(addr)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@ func Test_osResolver_Resolve(t *testing.T) {
|
||||
|
||||
go func() {
|
||||
defer cancel()
|
||||
resolver := &osResolver{nameservers: []string{"127.0.0.127:5353"}}
|
||||
resolver := &osResolver{}
|
||||
resolver.publicServers.Store(&[]string{"127.0.0.127:5353"})
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion("controld.com.", dns.TypeA)
|
||||
m.RecursionDesired = true
|
||||
@@ -30,26 +31,51 @@ func Test_osResolver_Resolve(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_osResolver_ResolveLanHostname(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
reqId := "req-id"
|
||||
ctx = context.WithValue(ctx, ReqIdCtxKey{}, reqId)
|
||||
ctx = LanQueryCtx(ctx)
|
||||
|
||||
go func(ctx context.Context) {
|
||||
defer cancel()
|
||||
id, ok := ctx.Value(ReqIdCtxKey{}).(string)
|
||||
if !ok || id != reqId {
|
||||
t.Error("missing request id")
|
||||
return
|
||||
}
|
||||
lan, ok := ctx.Value(LanQueryCtxKey{}).(bool)
|
||||
if !ok || !lan {
|
||||
t.Error("not a LAN query")
|
||||
return
|
||||
}
|
||||
resolver := &osResolver{}
|
||||
resolver.publicServers.Store(&[]string{"76.76.2.0:53"})
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion("controld.com.", dns.TypeA)
|
||||
m.RecursionDesired = true
|
||||
_, err := resolver.Resolve(ctx, m)
|
||||
if err == nil {
|
||||
t.Error("os resolver succeeded unexpectedly")
|
||||
return
|
||||
}
|
||||
}(ctx)
|
||||
|
||||
select {
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Error("os resolver hangs")
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}
|
||||
|
||||
func Test_osResolver_ResolveWithNonSuccessAnswer(t *testing.T) {
|
||||
ns := make([]string, 0, 2)
|
||||
servers := make([]*dns.Server, 0, 2)
|
||||
successHandler := dns.HandlerFunc(func(w dns.ResponseWriter, msg *dns.Msg) {
|
||||
m := new(dns.Msg)
|
||||
m.SetRcode(msg, dns.RcodeSuccess)
|
||||
w.WriteMsg(m)
|
||||
})
|
||||
nonSuccessHandlerWithRcode := func(rcode int) dns.HandlerFunc {
|
||||
return dns.HandlerFunc(func(w dns.ResponseWriter, msg *dns.Msg) {
|
||||
m := new(dns.Msg)
|
||||
m.SetRcode(msg, rcode)
|
||||
w.WriteMsg(m)
|
||||
})
|
||||
}
|
||||
|
||||
handlers := []dns.Handler{
|
||||
nonSuccessHandlerWithRcode(dns.RcodeRefused),
|
||||
nonSuccessHandlerWithRcode(dns.RcodeNameError),
|
||||
successHandler,
|
||||
successHandler(),
|
||||
}
|
||||
for i := range handlers {
|
||||
pc, err := net.ListenPacket("udp", ":0")
|
||||
@@ -69,7 +95,8 @@ func Test_osResolver_ResolveWithNonSuccessAnswer(t *testing.T) {
|
||||
server.Shutdown()
|
||||
}
|
||||
}()
|
||||
resolver := &osResolver{nameservers: ns}
|
||||
resolver := &osResolver{}
|
||||
resolver.publicServers.Store(&ns)
|
||||
msg := new(dns.Msg)
|
||||
msg.SetQuestion(".", dns.TypeNS)
|
||||
answer, err := resolver.Resolve(context.Background(), msg)
|
||||
@@ -81,6 +108,19 @@ func Test_osResolver_ResolveWithNonSuccessAnswer(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_osResolver_InitializationRace(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
n := 10
|
||||
wg.Add(n)
|
||||
for range n {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
InitializeOsResolver(false)
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func Test_upstreamTypeFromEndpoint(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -134,3 +174,19 @@ func runLocalPacketConnTestServer(t *testing.T, pc net.PacketConn, handler dns.H
|
||||
waitLock.Lock()
|
||||
return server, addr, nil
|
||||
}
|
||||
|
||||
func successHandler() dns.HandlerFunc {
|
||||
return func(w dns.ResponseWriter, msg *dns.Msg) {
|
||||
m := new(dns.Msg)
|
||||
m.SetRcode(msg, dns.RcodeSuccess)
|
||||
w.WriteMsg(m)
|
||||
}
|
||||
}
|
||||
|
||||
func nonSuccessHandlerWithRcode(rcode int) dns.HandlerFunc {
|
||||
return func(w dns.ResponseWriter, msg *dns.Msg) {
|
||||
m := new(dns.Msg)
|
||||
m.SetRcode(msg, rcode)
|
||||
w.WriteMsg(m)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user