From c7bad63869f6fc6247062f915426711f703a855a Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Wed, 26 Apr 2023 18:37:52 +0700 Subject: [PATCH] all: allow chosing random address and port for listener --- cmd/ctrld/dns_proxy.go | 76 +++++++++++++++++++++++++++++------------- config.go | 13 ++++++-- config_test.go | 9 ++++- docs/config.md | 8 ++--- 4 files changed, 74 insertions(+), 32 deletions(-) diff --git a/cmd/ctrld/dns_proxy.go b/cmd/ctrld/dns_proxy.go index a76f398..ec2ce30 100644 --- a/cmd/ctrld/dns_proxy.go +++ b/cmd/ctrld/dns_proxy.go @@ -9,6 +9,7 @@ import ( "runtime" "strconv" "strings" + "sync" "time" "github.com/miekg/dns" @@ -72,40 +73,37 @@ func (p *prog) serveDNS(listenerNum string) error { g, ctx := errgroup.WithContext(context.Background()) for _, proto := range []string{"udp", "tcp"} { proto := proto - // On Windows, there's no easy way for disabling/removing IPv6 DNS resolver, so we check whether we can - // listen on ::1, then spawn a listener for receiving DNS requests. if needLocalIPv6Listener() { g.Go(func() error { - s := &dns.Server{ - Addr: net.JoinHostPort("::1", strconv.Itoa(listenerConfig.Port)), - Net: proto, - Handler: handler, - } - go func() { - <-ctx.Done() - _ = s.Shutdown() - }() - if err := s.ListenAndServe(); err != nil { - mainLog.Error().Err(err).Msg("could not serving on ::1") + s, errCh := runDNSServer(net.JoinHostPort("::1", strconv.Itoa(listenerConfig.Port)), proto, handler) + defer s.Shutdown() + select { + case <-ctx.Done(): + case err := <-errCh: + // Local ipv6 listener should not terminate ctrld. + // It's a workaround for a quirk on Windows. + mainLog.Warn().Err(err).Msg("local ipv6 listener failed") } return nil }) } g.Go(func() error { - s := &dns.Server{ - Addr: dnsListenAddress(listenerConfig), - Net: proto, - Handler: handler, + s, errCh := runDNSServer(dnsListenAddress(listenerConfig), proto, handler) + defer s.Shutdown() + if listenerConfig.Port == 0 { + switch s.Net { + case "udp": + mainLog.Info().Msgf("Random port chosen for udp listener.%s: %s", listenerNum, s.PacketConn.LocalAddr()) + case "tcp": + mainLog.Info().Msgf("Random port chosen for tcp listener.%s: %s", listenerNum, s.Listener.Addr()) + } } - go func() { - <-ctx.Done() - _ = s.Shutdown() - }() - if err := s.ListenAndServe(); err != nil { - mainLog.Error().Err(err).Msgf("could not listen and serve on: %s", s.Addr) + select { + case <-ctx.Done(): + return nil + case err := <-errCh: return err } - return nil }) } return g.Wait() @@ -389,6 +387,8 @@ func ttlFromMsg(msg *dns.Msg) uint32 { } func needLocalIPv6Listener() bool { + // 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. return ctrldnet.SupportsIPv6ListenLocal() && runtime.GOOS == "windows" } @@ -412,3 +412,31 @@ func macFromMsg(msg *dns.Msg) string { } return "" } + +// runDNSServer starts a DNS server for given address and network, +// with the given handler. It ensures the server has started listening. +// Any error will be reported to the caller via returned channel. +// +// It's the caller responsibility to call Shutdown to close the server. +func runDNSServer(addr, network string, handler dns.Handler) (*dns.Server, <-chan error) { + s := &dns.Server{ + Addr: addr, + Net: network, + Handler: handler, + } + + waitLock := sync.Mutex{} + waitLock.Lock() + s.NotifyStartedFunc = waitLock.Unlock + + errCh := make(chan error) + go func() { + defer close(errCh) + if err := s.ListenAndServe(); err != nil { + mainLog.Error().Err(err).Msgf("could not listen and serve on: %s", s.Addr) + errCh <- err + } + }() + waitLock.Lock() + return s, errCh +} diff --git a/config.go b/config.go index 7e7bccb..9257351 100644 --- a/config.go +++ b/config.go @@ -133,8 +133,8 @@ type UpstreamConfig struct { // ListenerConfig specifies the networks configuration that ctrld will run on. type ListenerConfig struct { - IP string `mapstructure:"ip" toml:"ip,omitempty" validate:"ip"` - Port int `mapstructure:"port" toml:"port,omitempty" validate:"gt=0"` + IP string `mapstructure:"ip" toml:"ip,omitempty" validate:"iporempty"` + Port int `mapstructure:"port" toml:"port,omitempty" validate:"gte=0"` Restricted bool `mapstructure:"restricted" toml:"restricted,omitempty"` Policy *ListenerPolicyConfig `mapstructure:"policy" toml:"policy,omitempty"` } @@ -329,6 +329,7 @@ func (lc *ListenerConfig) Init() { // ValidateConfig validates the given config. func ValidateConfig(validate *validator.Validate, cfg *Config) error { _ = validate.RegisterValidation("dnsrcode", validateDnsRcode) + _ = validate.RegisterValidation("iporempty", validateIpOrEmpty) return validate.Struct(cfg) } @@ -336,6 +337,14 @@ func validateDnsRcode(fl validator.FieldLevel) bool { return dnsrcode.FromString(fl.Field().String()) != -1 } +func validateIpOrEmpty(fl validator.FieldLevel) bool { + val := fl.Field().String() + if val == "" { + return true + } + return net.ParseIP(val) != nil +} + func defaultPortFor(typ string) string { switch typ { case ResolverTypeDOH, ResolverTypeDOH3: diff --git a/config_test.go b/config_test.go index 90cd81a..ddbc97b 100644 --- a/config_test.go +++ b/config_test.go @@ -65,6 +65,7 @@ func TestConfigValidation(t *testing.T) { {"invalid Config", &ctrld.Config{}, true}, {"default Config", defaultConfig(t), false}, {"sample Config", testhelper.SampleConfig(t), false}, + {"empty listener IP", emptyListenerIP(t), false}, {"invalid cidr", invalidNetworkConfig(t), true}, {"invalid upstream type", invalidUpstreamType(t), true}, {"invalid upstream timeout", invalidUpstreamTimeout(t), true}, @@ -134,9 +135,15 @@ func invalidListenerIP(t *testing.T) *ctrld.Config { return cfg } +func emptyListenerIP(t *testing.T) *ctrld.Config { + cfg := defaultConfig(t) + cfg.Listener["0"].IP = "" + return cfg +} + func invalidListenerPort(t *testing.T) *ctrld.Config { cfg := defaultConfig(t) - cfg.Listener["0"].Port = 0 + cfg.Listener["0"].Port = -1 return cfg } diff --git a/docs/config.md b/docs/config.md index 4f12736..e8cec53 100644 --- a/docs/config.md +++ b/docs/config.md @@ -271,16 +271,14 @@ The `[listener]` section specifies the ip and port of the local DNS server. You ``` ### ip -IP address that serves the incoming requests. +IP address that serves the incoming requests. If `ip` is empty, ctrld will listen on all available addresses. -- Type: string -- Required: yes +- Type: ip address ### port -Port number that the listener will listen on for incoming requests. +Port number that the listener will listen on for incoming requests. If `port` is `0`, a random available port will be chosen. - Type: number -- Required: yes ### restricted If set to `true` makes the listener `REFUSE` DNS queries from all source IP addresses that are not explicitly defined in the policy using a `network`.