package microvm import ( "context" "errors" "fmt" "log/slog" "net" "sync" "time" "github.com/miekg/dns" ) const ( dnsProxyIOTimeout = 10 * time.Second dnsProxyIdleTimeout = 30 * time.Second dnsProxyShutdownTimeout = 10 * time.Second dnsProxyMaxConnections = 64 dnsProxyMaxTCPQueries = 128 dnsProxyResolvConfPath = "/etc/resolv.conf" ) type DNSProxy struct { port uint32 srv *dns.Server closeOnce sync.Once closeErr error } func StartDNSProxy(ctx context.Context, cid uint32, logger *slog.Logger) (*DNSProxy, error) { if ctx == nil { ctx = context.Background() } if logger == nil { logger = slog.Default() } logger = logger.With("where", "dns_proxy", "cid", cid) ln, port, err := listenRandomVsockPort(ctx) if err != nil { return nil, fmt.Errorf("listen for dns proxy: %w", err) } resolver, err := newHostDNSResolver(dnsProxyResolvConfPath, logger) if err != nil { _ = ln.Close() return nil, err } listener := newLimitedListener( &cidFilteredVsockListener{ Listener: ln, cid: cid, logger: logger, }, dnsProxyMaxConnections, logger, ) proxy := &DNSProxy{ port: port, srv: &dns.Server{ Net: "tcp", Listener: listener, Handler: dns.HandlerFunc(resolver.ServeDNS), ReadTimeout: dnsProxyIOTimeout, WriteTimeout: dnsProxyIOTimeout, IdleTimeout: func() time.Duration { return dnsProxyIdleTimeout }, MaxTCPQueries: dnsProxyMaxTCPQueries, MsgInvalidFunc: func(_ []byte, err error) { logger.Warn("dns proxy invalid message", "error", err) }, }, } go func() { <-ctx.Done() _ = proxy.Close() }() go func() { if err := proxy.srv.ActivateAndServe(); err != nil && !errors.Is(err, net.ErrClosed) { logger.Warn("dns proxy stopped", "error", err) } }() logger.Info("started dns proxy", "port", port) return proxy, nil } func (p *DNSProxy) Port() uint32 { if p == nil { return 0 } return p.port } func (p *DNSProxy) Close() error { if p == nil || p.srv == nil { return nil } p.closeOnce.Do(func() { shutdownCtx, cancel := context.WithTimeout(context.Background(), dnsProxyShutdownTimeout) defer cancel() p.closeErr = p.srv.ShutdownContext(shutdownCtx) }) return p.closeErr } type limitedListener struct { net.Listener slots chan struct{} logger *slog.Logger } func newLimitedListener(listener net.Listener, limit int, logger *slog.Logger) net.Listener { if limit <= 0 { return listener } return &limitedListener{ Listener: listener, slots: make(chan struct{}, limit), logger: logger, } } func (l *limitedListener) Accept() (net.Conn, error) { for { conn, err := l.Listener.Accept() if err != nil { return nil, err } select { case l.slots <- struct{}{}: return &limitedConn{ Conn: conn, release: func() { <-l.slots }, }, nil default: l.logger.Warn("dns proxy dropped connection because workers are busy") _ = conn.Close() } } } type limitedConn struct { net.Conn once sync.Once release func() } func (c *limitedConn) Close() error { err := c.Conn.Close() c.once.Do(c.release) return err } type hostDNSResolver struct { upstreams []string attempts int timeout time.Duration logger *slog.Logger } func newHostDNSResolver(path string, logger *slog.Logger) (*hostDNSResolver, error) { config, err := dns.ClientConfigFromFile(path) if err != nil { return nil, fmt.Errorf("read host resolv.conf: %w", err) } if len(config.Servers) == 0 { return nil, fmt.Errorf("host resolv.conf has no nameservers") } port := config.Port if port == "" { port = "53" } upstreams := make([]string, 0, len(config.Servers)) for _, server := range config.Servers { upstreams = append(upstreams, net.JoinHostPort(server, port)) } timeout := time.Duration(config.Timeout) * time.Second if timeout <= 0 { timeout = dnsProxyIOTimeout } return &hostDNSResolver{ upstreams: upstreams, attempts: max(config.Attempts, 1), timeout: timeout, logger: logger, }, nil } func (r *hostDNSResolver) ServeDNS(w dns.ResponseWriter, req *dns.Msg) { resp, err := r.exchange(req) if err != nil { r.logger.Warn( "dns upstream exchange failed", "question", dnsQuestionLogValue(req), "error", err, ) if err := w.WriteMsg(rcodeResponse(req, dns.RcodeServerFailure)); err != nil { r.logger.Warn("dns proxy response write failed", "error", err) } return } filterDNSResponse(resp) if err := w.WriteMsg(resp); err != nil { r.logger.Warn("dns proxy response write failed", "error", err) } } func (r *hostDNSResolver) exchange(req *dns.Msg) (*dns.Msg, error) { var errs []error for range r.attempts { for _, upstream := range r.upstreams { resp, err := exchangeDNSAt(req, upstream, r.timeout) if err == nil { return resp, nil } errs = append(errs, fmt.Errorf("%s: %w", upstream, err)) } } return nil, errors.Join(errs...) } func exchangeDNSAt(req *dns.Msg, addr string, timeout time.Duration) (*dns.Msg, error) { resp, _, err := (&dns.Client{Net: "udp", Timeout: timeout}).Exchange(req, addr) if err != nil { return nil, err } if resp == nil { return nil, fmt.Errorf("empty udp response") } if !resp.Truncated { return resp, nil } resp, _, err = (&dns.Client{Net: "tcp", Timeout: timeout}).Exchange(req, addr) if err != nil { return nil, err } if resp == nil { return nil, fmt.Errorf("empty tcp response") } return resp, nil } func filterDNSResponse(msg *dns.Msg) { if msg == nil { return } msg.Answer = filterDNSRRs(msg.Answer) msg.Ns = filterDNSRRs(msg.Ns) msg.Extra = filterDNSRRs(msg.Extra) } func filterDNSRRs(rrs []dns.RR) []dns.RR { filtered := rrs[:0] for _, rr := range rrs { if rr := filterDNSRR(rr); rr != nil { filtered = append(filtered, rr) } } return filtered } func filterDNSRR(rr dns.RR) dns.RR { switch rr := rr.(type) { case *dns.A: if isBlockedNamespaceIP(rr.A) { return nil } case *dns.AAAA: if isBlockedNamespaceIP(rr.AAAA) { return nil } case *dns.SVCB: filterSVCBValues(&rr.Value) case *dns.HTTPS: filterSVCBValues(&rr.Value) } return rr } // this removes any blocked namespaces in ipv4/v6 hints func filterSVCBValues(values *[]dns.SVCBKeyValue) { filtered := (*values)[:0] for _, value := range *values { switch value := value.(type) { case *dns.SVCBIPv4Hint: value.Hint = filterDNSIPs(value.Hint) if len(value.Hint) == 0 { continue } case *dns.SVCBIPv6Hint: value.Hint = filterDNSIPs(value.Hint) if len(value.Hint) == 0 { continue } } filtered = append(filtered, value) } *values = filtered } func filterDNSIPs(ips []net.IP) []net.IP { filtered := ips[:0] for _, ip := range ips { if !isBlockedNamespaceIP(ip) { filtered = append(filtered, ip) } } return filtered } func isBlockedNamespaceIP(ip net.IP) bool { if ip == nil { return true } if ip4 := ip.To4(); ip4 != nil { return isBlockedByNamespaceNets(ip4, 32) } return isBlockedByNamespaceNets(ip, 128) } func isBlockedByNamespaceNets(ip net.IP, bits int) bool { for _, blockedNet := range blockedNamespaceNets { if blockedNet == nil { continue } _, blockedBits := blockedNet.Mask.Size() if blockedBits != bits { continue } if blockedNet.Contains(ip) { return true } } return false } func rcodeResponse(req *dns.Msg, rcode int) *dns.Msg { resp := new(dns.Msg) if req == nil { resp.Rcode = rcode return resp } resp.SetRcode(req, rcode) return resp } func dnsQuestionLogValue(msg *dns.Msg) string { if msg == nil || len(msg.Question) == 0 { return "" } q := msg.Question[0] qtype := dns.TypeToString[q.Qtype] if qtype == "" { qtype = fmt.Sprintf("TYPE%d", q.Qtype) } return fmt.Sprintf("%s/%s", q.Name, qtype) }