all: allow chosing random address and port for listener

This commit is contained in:
Cuong Manh Le
2023-04-26 18:37:52 +07:00
committed by Cuong Manh Le
parent 69319c6b41
commit c7bad63869
4 changed files with 74 additions and 32 deletions

View File

@@ -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
}

View File

@@ -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:

View File

@@ -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
}

View File

@@ -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`.