From 3e6f6cc721237e02d349c63fba3d8d45ba6aba98 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Tue, 14 Mar 2023 09:43:40 +0700 Subject: [PATCH 1/7] cmd/ctrld: add TCP listener Fixes #25 --- cmd/ctrld/dns_proxy.go | 44 +++++++++++++++++++++++++++--------------- cmd/ctrld/prog.go | 4 ++-- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/cmd/ctrld/dns_proxy.go b/cmd/ctrld/dns_proxy.go index bc52332..3098cc1 100644 --- a/cmd/ctrld/dns_proxy.go +++ b/cmd/ctrld/dns_proxy.go @@ -12,6 +12,7 @@ import ( "time" "github.com/miekg/dns" + "golang.org/x/sync/errgroup" "github.com/Control-D-Inc/ctrld" "github.com/Control-D-Inc/ctrld/internal/dnscache" @@ -20,7 +21,7 @@ import ( const staleTTL = 60 * time.Second -func (p *prog) serveUDP(listenerNum string) error { +func (p *prog) serveDNS(listenerNum string) error { listenerConfig := p.cfg.Listener[listenerNum] // make sure ip is allocated if allocErr := p.allocateIP(listenerConfig.IP); allocErr != nil { @@ -55,27 +56,38 @@ func (p *prog) serveUDP(listenerNum string) error { } }) - // On Windows, there's no easy way for disabling/removing IPv6 DNS resolver, so we check whether we can - // listen on ::1, then spawn a listener for receiving DNS requests. - if runtime.GOOS == "windows" && ctrldnet.SupportsIPv6ListenLocal() { - go func() { + g := new(errgroup.Group) + for _, proto := range []string{"udp", "tcp"} { + proto := proto + // On Windows, there's no easy way for disabling/removing IPv6 DNS resolver, so we check whether we can + // listen on ::1, then spawn a listener for receiving DNS requests. + if runtime.GOOS == "windows" && ctrldnet.SupportsIPv6ListenLocal() { + g.Go(func() error { + s := &dns.Server{ + Addr: net.JoinHostPort("::1", strconv.Itoa(listenerConfig.Port)), + Net: proto, + Handler: handler, + } + if err := s.ListenAndServe(); err != nil { + mainLog.Error().Err(err).Msg("could not serving on ::1") + } + return nil + }) + } + g.Go(func() error { s := &dns.Server{ - Addr: net.JoinHostPort("::1", strconv.Itoa(listenerConfig.Port)), - Net: "udp", + Addr: net.JoinHostPort(listenerConfig.IP, strconv.Itoa(listenerConfig.Port)), + Net: proto, Handler: handler, } if err := s.ListenAndServe(); err != nil { - mainLog.Error().Err(err).Msg("could not serving on ::1") + mainLog.Error().Err(err).Msgf("could not listen and serve on: %s", s.Addr) + return err } - }() + return nil + }) } - - s := &dns.Server{ - Addr: net.JoinHostPort(listenerConfig.IP, strconv.Itoa(listenerConfig.Port)), - Net: "udp", - Handler: handler, - } - return s.ListenAndServe() + return g.Wait() } func (p *prog) upstreamFor(ctx context.Context, defaultUpstreamNum string, lc *ctrld.ListenerConfig, addr net.Addr, domain string) ([]string, bool) { diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index 6b58116..52b9c85 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -85,7 +85,7 @@ func (p *prog) run() { } addr := net.JoinHostPort(listenerConfig.IP, strconv.Itoa(listenerConfig.Port)) mainLog.Info().Msgf("Starting DNS server on listener.%s: %s", listenerNum, addr) - err := p.serveUDP(listenerNum) + err := p.serveDNS(listenerNum) if err != nil && !defaultConfigWritten && cdUID == "" { mainLog.Fatal().Err(err).Msgf("Unable to start dns proxy on listener.%s", listenerNum) return @@ -109,7 +109,7 @@ func (p *prog) run() { p.cfg.Service.AllocateIP = true p.preRun() mainLog.Info().Msgf("Starting DNS server on listener.%s: %s", listenerNum, net.JoinHostPort(ip, strconv.Itoa(port))) - if err := p.serveUDP(listenerNum); err != nil { + if err := p.serveDNS(listenerNum); err != nil { mainLog.Fatal().Err(err).Msgf("Unable to start dns proxy on listener.%s", listenerNum) return } From 096e7ea429daf4041d939605ed095f2d78dd4cfd Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Wed, 15 Mar 2023 10:33:37 +0700 Subject: [PATCH 2/7] internal/net: enforce timeout for probing stack On Windows host with StarLink network, ctrld hangs on startup for ~30s before continue running. This dues to IPv6 is configured but no external IPv6 can be reached. When probing stack, ctrld is dialing using ipv6 without any timeout set, so the dialing timeout is enforced by OS. This commit adds a timeout for probing dialer, so we ensure the probing process will fail fast. --- internal/net/net.go | 13 ++++++++++--- internal/net/net_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 internal/net/net_test.go diff --git a/internal/net/net.go b/internal/net/net.go index a155f2c..888a2d6 100644 --- a/internal/net/net.go +++ b/internal/net/net.go @@ -28,6 +28,13 @@ var Dialer = &net.Dialer{ }, } +const probeStackTimeout = 2 * time.Second + +var probeStackDialer = &net.Dialer{ + Resolver: Dialer.Resolver, + Timeout: probeStackTimeout, +} + var ( stackOnce atomic.Pointer[sync.Once] ipv4Enabled bool @@ -41,12 +48,12 @@ func init() { } func supportIPv4() bool { - _, err := Dialer.Dial("tcp4", net.JoinHostPort(controldIPv4Test, "80")) + _, err := probeStackDialer.Dial("tcp4", net.JoinHostPort(controldIPv4Test, "80")) return err == nil } func supportIPv6(ctx context.Context) bool { - _, err := Dialer.DialContext(ctx, "tcp6", net.JoinHostPort(controldIPv6Test, "80")) + _, err := probeStackDialer.DialContext(ctx, "tcp6", net.JoinHostPort(controldIPv6Test, "80")) return err == nil } @@ -61,7 +68,7 @@ func supportListenIPv6Local() bool { func probeStack() { b := backoff.NewBackoff("probeStack", func(format string, args ...any) {}, time.Minute) for { - if _, err := Dialer.Dial("udp", bootstrapDNS); err == nil { + if _, err := probeStackDialer.Dial("udp", bootstrapDNS); err == nil { hasNetworkUp = true break } else { diff --git a/internal/net/net_test.go b/internal/net/net_test.go new file mode 100644 index 0000000..d28dbed --- /dev/null +++ b/internal/net/net_test.go @@ -0,0 +1,24 @@ +package net + +import ( + "context" + "testing" + "time" +) + +func TestProbeStackTimeout(t *testing.T) { + done := make(chan struct{}) + started := make(chan struct{}) + go func() { + defer close(done) + close(started) + supportIPv6(context.Background()) + }() + + <-started + select { + case <-time.After(probeStackTimeout + time.Second): + t.Error("probeStack timeout is not enforce") + case <-done: + } +} From 77b62f8734c07d66a6dee5683680b1eb844d1fba Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Wed, 15 Mar 2023 20:04:26 +0700 Subject: [PATCH 3/7] cmd/ctrld: add default timeout for os resolver So it can fail fast if internet broken suddenly. While at it, also filtering out ipv6 nameservers if ipv6 not available. --- cmd/ctrld/dns_proxy.go | 5 +++-- config.go | 23 +++++++++++++++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/cmd/ctrld/dns_proxy.go b/cmd/ctrld/dns_proxy.go index 3098cc1..733ae63 100644 --- a/cmd/ctrld/dns_proxy.go +++ b/cmd/ctrld/dns_proxy.go @@ -342,6 +342,7 @@ func ttlFromMsg(msg *dns.Msg) uint32 { } var osUpstreamConfig = &ctrld.UpstreamConfig{ - Name: "OS resolver", - Type: ctrld.ResolverTypeOS, + Name: "OS resolver", + Type: ctrld.ResolverTypeOS, + Timeout: 2000, } diff --git a/config.go b/config.go index 76fa51c..b1582f6 100644 --- a/config.go +++ b/config.go @@ -165,11 +165,15 @@ func (uc *UpstreamConfig) SetupBootstrapIP() { return "" } - resolver := &osResolver{nameservers: nameservers()} + resolver := &osResolver{nameservers: availableNameservers()} resolver.nameservers = append([]string{net.JoinHostPort(bootstrapDNS, "53")}, resolver.nameservers...) ProxyLog.Debug().Msgf("Resolving %q using bootstrap DNS %q", uc.Domain, resolver.nameservers) + timeoutMs := 2000 + if uc.Timeout > 0 && uc.Timeout < timeoutMs { + timeoutMs = uc.Timeout + } do := func(dnsType uint16) { - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(uc.Timeout)*time.Millisecond) + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutMs)*time.Millisecond) defer cancel() m := new(dns.Msg) m.SetQuestion(uc.Domain+".", dnsType) @@ -349,3 +353,18 @@ func defaultPortFor(typ string) string { } return "53" } + +func availableNameservers() []string { + nss := nameservers() + n := 0 + for _, ns := range nss { + ip, _, _ := net.SplitHostPort(ns) + // skipping invalid entry or ipv6 nameserver if ipv6 not available. + if ip == "" || (ctrldnet.IsIPv6(ip) && !ctrldnet.SupportsIPv6()) { + continue + } + nss[n] = ns + n++ + } + return nss[:n] +} From e4eb3b2ded524fef470a4899330b50bae879ae0b Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Wed, 15 Mar 2023 23:26:57 +0700 Subject: [PATCH 4/7] Do not query ipv6 eagerly when setup bootstrap IP We only need on demand information when re-bootstrapping. On Bootsrap, this is already checked by ctrldnet.Up, so on demand query will cause un-necessary slow down if external ipv6 is slow to response. --- config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.go b/config.go index b1582f6..81366c5 100644 --- a/config.go +++ b/config.go @@ -207,7 +207,7 @@ func (uc *UpstreamConfig) SetupBootstrapIP() { uc.nextBootstrapIP.Add(1) // If this is an ipv6, and ipv6 is not available, don't use it as bootstrap ip. - if !ctrldnet.IPv6Available(ctx) && ctrldnet.IsIPv6(ip) { + if !ctrldnet.SupportsIPv6() && ctrldnet.IsIPv6(ip) { continue } uc.BootstrapIP = ip From ccf07a7d1cb2df40fc588448c6677b4a10d70306 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Thu, 16 Mar 2023 09:33:09 +0700 Subject: [PATCH 5/7] cmd/ctrld: log that ctrld is starting --- cmd/ctrld/cli.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index e791e20..8d9d130 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -112,6 +112,7 @@ func initCLI() { if err := v.Unmarshal(&cfg); err != nil { log.Fatalf("failed to unmarshal config: %v", err) } + fmt.Println("starting ctrld...") // Wait for network up. if !ctrldnet.Up() { log.Fatal("network is not up yet") From 5b362412be5ac66f14634000ff5220d794e0565e Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Thu, 16 Mar 2023 10:40:17 +0700 Subject: [PATCH 6/7] Add quic free version to goreleaser --- .goreleaser-qf.yaml | 40 ++++++++++++++++++++++++++++++++++++++++ scripts/upx.sh | 4 ++++ 2 files changed, 44 insertions(+) create mode 100644 .goreleaser-qf.yaml diff --git a/.goreleaser-qf.yaml b/.goreleaser-qf.yaml new file mode 100644 index 0000000..9e72ed4 --- /dev/null +++ b/.goreleaser-qf.yaml @@ -0,0 +1,40 @@ +before: + hooks: + - go mod tidy +builds: + - id: ctrld + env: + - CGO_ENABLED=0 + flags: + - -trimpath + ldflags: + - -s -w + goos: + - darwin + - linux + - windows + goarch: + - amd64 + - arm64 + tags: + - qf + main: ./cmd/ctrld + hooks: + post: /bin/sh ./scripts/upx.sh {{ .Path }} +archives: + - format_overrides: + - goos: windows + format: zip + strip_parent_binary_folder: true + wrap_in_directory: true + files: + - README.md +checksum: + name_template: 'checksums.txt' +snapshot: + name_template: "{{ incpatch .Version }}-next" +changelog: + sort: asc + filters: + exclude: + - '^docs:' diff --git a/scripts/upx.sh b/scripts/upx.sh index 5d366eb..852777f 100755 --- a/scripts/upx.sh +++ b/scripts/upx.sh @@ -18,6 +18,10 @@ case "$binary" in echo >&2 "upx does not work with windows arm/arm64 binary yet" exit 0 ;; + *_darwin_*) + echo >&2 "upx claims to work with darwin binary, but testing show that it is broken" + exit 0 + ;; esac upx -- "$binary" From 4bfcacaf3cdba0a62df12bc12baea35f57ff09cd Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Thu, 16 Mar 2023 10:53:33 +0700 Subject: [PATCH 7/7] cmd/ctrld: bump version to v1.1.2 --- cmd/ctrld/cli.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index 8d9d130..bea5161 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -70,7 +70,7 @@ func initCLI() { rootCmd := &cobra.Command{ Use: "ctrld", Short: strings.TrimLeft(rootShortDesc, "\n"), - Version: "1.1.1", + Version: "1.1.2", } rootCmd.PersistentFlags().CountVarP( &verbose,