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/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index e791e20..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, @@ -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") diff --git a/cmd/ctrld/dns_proxy.go b/cmd/ctrld/dns_proxy.go index bc52332..733ae63 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) { @@ -330,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/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 } diff --git a/config.go b/config.go index 76fa51c..81366c5 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) @@ -203,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 @@ -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] +} 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: + } +} 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"