// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package mdns import ( "context" "errors" "fmt" "net" "net/netip" "sync" "time" "github.com/pion/logging" "golang.org/x/net/dns/dnsmessage" "golang.org/x/net/ipv4" "golang.org/x/net/ipv6" ) // Conn represents a mDNS Server type Conn struct { mu sync.RWMutex name string log logging.LeveledLogger multicastPktConnV4 ipPacketConn multicastPktConnV6 ipPacketConn dstAddr4 *net.UDPAddr dstAddr6 *net.UDPAddr unicastPktConnV4 ipPacketConn unicastPktConnV6 ipPacketConn queryInterval time.Duration localNames []string queries []*query ifaces map[int]netInterface closed chan interface{} } type query struct { nameWithSuffix string queryResultChan chan queryResult } type queryResult struct { answer dnsmessage.ResourceHeader addr netip.Addr } const ( defaultQueryInterval = time.Second destinationAddress4 = "224.0.0.251:5353" destinationAddress6 = "[FF02::FB]:5353" maxMessageRecords = 3 responseTTL = 120 // maxPacketSize is the maximum size of a mdns packet. // From RFC 6762: // Even when fragmentation is used, a Multicast DNS packet, including IP // and UDP headers, MUST NOT exceed 9000 bytes. // https://datatracker.ietf.org/doc/html/rfc6762#section-17 maxPacketSize = 9000 ) var ( errNoPositiveMTUFound = errors.New("no positive MTU found") errNoPacketConn = errors.New("must supply at least a multicast IPv4 or IPv6 PacketConn") errNoUsableInterfaces = errors.New("no usable interfaces found for mDNS") errFailedToClose = errors.New("failed to close mDNS Conn") ) type netInterface struct { net.Interface ipAddrs []netip.Addr supportsV4 bool supportsV6 bool } // Server establishes a mDNS connection over an existing conn. // Either one or both of the multicast packet conns should be provided. // The presence of each IP type of PacketConn will dictate what kinds // of questions are sent for queries. That is, if an ipv6.PacketConn is // provided, then AAAA questions will be sent. A questions will only be // sent if an ipv4.PacketConn is also provided. In the future, we may // add a QueryAddr method that allows specifying this more clearly. // //nolint:gocognit func Server( multicastPktConnV4 *ipv4.PacketConn, multicastPktConnV6 *ipv6.PacketConn, config *Config, ) (*Conn, error) { if config == nil { return nil, errNilConfig } loggerFactory := config.LoggerFactory if loggerFactory == nil { loggerFactory = logging.NewDefaultLoggerFactory() } log := loggerFactory.NewLogger("mdns") c := &Conn{ queryInterval: defaultQueryInterval, log: log, closed: make(chan interface{}), } c.name = config.Name if c.name == "" { c.name = fmt.Sprintf("%p", &c) } if multicastPktConnV4 == nil && multicastPktConnV6 == nil { return nil, errNoPacketConn } ifaces := config.Interfaces if ifaces == nil { var err error ifaces, err = net.Interfaces() if err != nil { return nil, err } } var unicastPktConnV4 *ipv4.PacketConn { addr4, err := net.ResolveUDPAddr("udp4", "0.0.0.0:0") if err != nil { return nil, err } unicastConnV4, err := net.ListenUDP("udp4", addr4) if err != nil { log.Warnf("[%s] failed to listen on unicast IPv4 %s: %s; will not be able to receive unicast responses on IPv4", c.name, addr4, err) } else { unicastPktConnV4 = ipv4.NewPacketConn(unicastConnV4) } } var unicastPktConnV6 *ipv6.PacketConn { addr6, err := net.ResolveUDPAddr("udp6", "[::]:") if err != nil { return nil, err } unicastConnV6, err := net.ListenUDP("udp6", addr6) if err != nil { log.Warnf("[%s] failed to listen on unicast IPv6 %s: %s; will not be able to receive unicast responses on IPv6", c.name, addr6, err) } else { unicastPktConnV6 = ipv6.NewPacketConn(unicastConnV6) } } mutlicastGroup4 := net.IPv4(224, 0, 0, 251) multicastGroupAddr4 := &net.UDPAddr{IP: mutlicastGroup4} // FF02::FB mutlicastGroup6 := net.IP{0xff, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xfb} multicastGroupAddr6 := &net.UDPAddr{IP: mutlicastGroup6} inboundBufferSize := 0 joinErrCount := 0 ifacesToUse := make(map[int]netInterface, len(ifaces)) for i := range ifaces { ifc := ifaces[i] if !config.IncludeLoopback && ifc.Flags&net.FlagLoopback == net.FlagLoopback { continue } if ifc.Flags&net.FlagUp == 0 { continue } addrs, err := ifc.Addrs() if err != nil { continue } var supportsV4, supportsV6 bool ifcIPAddrs := make([]netip.Addr, 0, len(addrs)) for _, addr := range addrs { var ipToConv net.IP switch addr := addr.(type) { case *net.IPNet: ipToConv = addr.IP case *net.IPAddr: ipToConv = addr.IP default: continue } ipAddr, ok := netip.AddrFromSlice(ipToConv) if !ok { continue } if multicastPktConnV4 != nil { // don't want mapping since we also support IPv4/A ipAddr = ipAddr.Unmap() } ipAddr = addrWithOptionalZone(ipAddr, ifc.Name) if ipAddr.Is6() && !ipAddr.Is4In6() { supportsV6 = true } else { // we'll claim we support v4 but defer if we send it or not // based on IPv4-to-IPv6 mapping rules later (search for Is4In6 below) supportsV4 = true } ifcIPAddrs = append(ifcIPAddrs, ipAddr) } if !(supportsV4 || supportsV6) { continue } var atLeastOneJoin bool if supportsV4 && multicastPktConnV4 != nil { if err := multicastPktConnV4.JoinGroup(&ifc, multicastGroupAddr4); err == nil { atLeastOneJoin = true } } if supportsV6 && multicastPktConnV6 != nil { if err := multicastPktConnV6.JoinGroup(&ifc, multicastGroupAddr6); err == nil { atLeastOneJoin = true } } if !atLeastOneJoin { joinErrCount++ continue } ifacesToUse[ifc.Index] = netInterface{ Interface: ifc, ipAddrs: ifcIPAddrs, supportsV4: supportsV4, supportsV6: supportsV6, } if ifc.MTU > inboundBufferSize { inboundBufferSize = ifc.MTU } } if len(ifacesToUse) == 0 { return nil, errNoUsableInterfaces } if inboundBufferSize == 0 { return nil, errNoPositiveMTUFound } if inboundBufferSize > maxPacketSize { inboundBufferSize = maxPacketSize } if joinErrCount >= len(ifaces) { return nil, errJoiningMulticastGroup } dstAddr4, err := net.ResolveUDPAddr("udp4", destinationAddress4) if err != nil { return nil, err } dstAddr6, err := net.ResolveUDPAddr("udp6", destinationAddress6) if err != nil { return nil, err } var localNames []string for _, l := range config.LocalNames { localNames = append(localNames, l+".") } c.dstAddr4 = dstAddr4 c.dstAddr6 = dstAddr6 c.localNames = localNames c.ifaces = ifacesToUse if config.QueryInterval != 0 { c.queryInterval = config.QueryInterval } if multicastPktConnV4 != nil { if err := multicastPktConnV4.SetControlMessage(ipv4.FlagInterface, true); err != nil { c.log.Warnf("[%s] failed to SetControlMessage(ipv4.FlagInterface) on multicast IPv4 PacketConn %v", c.name, err) } if err := multicastPktConnV4.SetControlMessage(ipv4.FlagDst, true); err != nil { c.log.Warnf("[%s] failed to SetControlMessage(ipv4.FlagDst) on multicast IPv4 PacketConn %v", c.name, err) } c.multicastPktConnV4 = ipPacketConn4{c.name, multicastPktConnV4, log} } if multicastPktConnV6 != nil { if err := multicastPktConnV6.SetControlMessage(ipv6.FlagInterface, true); err != nil { c.log.Warnf("[%s] failed to SetControlMessage(ipv6.FlagInterface) on multicast IPv6 PacketConn %v", c.name, err) } if err := multicastPktConnV6.SetControlMessage(ipv6.FlagDst, true); err != nil { c.log.Warnf("[%s] failed to SetControlMessage(ipv6.FlagInterface) on multicast IPv6 PacketConn %v", c.name, err) } c.multicastPktConnV6 = ipPacketConn6{c.name, multicastPktConnV6, log} } if unicastPktConnV4 != nil { if err := unicastPktConnV4.SetControlMessage(ipv4.FlagInterface, true); err != nil { c.log.Warnf("[%s] failed to SetControlMessage(ipv4.FlagInterface) on unicast IPv4 PacketConn %v", c.name, err) } if err := unicastPktConnV4.SetControlMessage(ipv4.FlagDst, true); err != nil { c.log.Warnf("[%s] failed to SetControlMessage(ipv4.FlagInterface) on unicast IPv4 PacketConn %v", c.name, err) } c.unicastPktConnV4 = ipPacketConn4{c.name, unicastPktConnV4, log} } if unicastPktConnV6 != nil { if err := unicastPktConnV6.SetControlMessage(ipv6.FlagInterface, true); err != nil { c.log.Warnf("[%s] failed to SetControlMessage(ipv6.FlagInterface) on unicast IPv6 PacketConn %v", c.name, err) } if err := unicastPktConnV6.SetControlMessage(ipv6.FlagDst, true); err != nil { c.log.Warnf("[%s] failed to SetControlMessage(ipv6.FlagInterface) on unicast IPv6 PacketConn %v", c.name, err) } c.unicastPktConnV6 = ipPacketConn6{c.name, unicastPktConnV6, log} } if config.IncludeLoopback { // this is an efficient way for us to send ourselves a message faster instead of it going // further out into the network stack. if multicastPktConnV4 != nil { if err := multicastPktConnV4.SetMulticastLoopback(true); err != nil { c.log.Warnf("[%s] failed to SetMulticastLoopback(true) on multicast IPv4 PacketConn %v; this may cause inefficient network path c.name,communications", c.name, err) } } if multicastPktConnV6 != nil { if err := multicastPktConnV6.SetMulticastLoopback(true); err != nil { c.log.Warnf("[%s] failed to SetMulticastLoopback(true) on multicast IPv6 PacketConn %v; this may cause inefficient network path c.name,communications", c.name, err) } } if unicastPktConnV4 != nil { if err := unicastPktConnV4.SetMulticastLoopback(true); err != nil { c.log.Warnf("[%s] failed to SetMulticastLoopback(true) on unicast IPv4 PacketConn %v; this may cause inefficient network path c.name,communications", c.name, err) } } if unicastPktConnV6 != nil { if err := unicastPktConnV6.SetMulticastLoopback(true); err != nil { c.log.Warnf("[%s] failed to SetMulticastLoopback(true) on unicast IPv6 PacketConn %v; this may cause inefficient network path c.name,communications", c.name, err) } } } // https://www.rfc-editor.org/rfc/rfc6762.html#section-17 // Multicast DNS messages carried by UDP may be up to the IP MTU of the // physical interface, less the space required for the IP header (20 // bytes for IPv4; 40 bytes for IPv6) and the UDP header (8 bytes). started := make(chan struct{}) go c.start(started, inboundBufferSize-20-8, config) <-started return c, nil } // Close closes the mDNS Conn func (c *Conn) Close() error { select { case <-c.closed: return nil default: } // Once on go1.20, can use errors.Join var errs []error if c.multicastPktConnV4 != nil { if err := c.multicastPktConnV4.Close(); err != nil { errs = append(errs, err) } } if c.multicastPktConnV6 != nil { if err := c.multicastPktConnV6.Close(); err != nil { errs = append(errs, err) } } if c.unicastPktConnV4 != nil { if err := c.unicastPktConnV4.Close(); err != nil { errs = append(errs, err) } } if c.unicastPktConnV6 != nil { if err := c.unicastPktConnV6.Close(); err != nil { errs = append(errs, err) } } if len(errs) == 0 { <-c.closed return nil } rtrn := errFailedToClose for _, err := range errs { rtrn = fmt.Errorf("%w\n%w", err, rtrn) } return rtrn } // Query sends mDNS Queries for the following name until // either the Context is canceled/expires or we get a result // // Deprecated: Use QueryAddr instead as it supports the easier to use netip.Addr. func (c *Conn) Query(ctx context.Context, name string) (dnsmessage.ResourceHeader, net.Addr, error) { header, addr, err := c.QueryAddr(ctx, name) if err != nil { return header, nil, err } return header, &net.IPAddr{ IP: addr.AsSlice(), Zone: addr.Zone(), }, nil } // QueryAddr sends mDNS Queries for the following name until // either the Context is canceled/expires or we get a result func (c *Conn) QueryAddr(ctx context.Context, name string) (dnsmessage.ResourceHeader, netip.Addr, error) { select { case <-c.closed: return dnsmessage.ResourceHeader{}, netip.Addr{}, errConnectionClosed default: } nameWithSuffix := name + "." queryChan := make(chan queryResult, 1) query := &query{nameWithSuffix, queryChan} c.mu.Lock() c.queries = append(c.queries, query) c.mu.Unlock() defer func() { c.mu.Lock() defer c.mu.Unlock() for i := len(c.queries) - 1; i >= 0; i-- { if c.queries[i] == query { c.queries = append(c.queries[:i], c.queries[i+1:]...) } } }() ticker := time.NewTicker(c.queryInterval) defer ticker.Stop() c.sendQuestion(nameWithSuffix) for { select { case <-ticker.C: c.sendQuestion(nameWithSuffix) case <-c.closed: return dnsmessage.ResourceHeader{}, netip.Addr{}, errConnectionClosed case res := <-queryChan: // Given https://datatracker.ietf.org/doc/html/draft-ietf-mmusic-mdns-ice-candidates#section-3.2.2-2 // An ICE agent SHOULD ignore candidates where the hostname resolution returns more than one IP address. // // We will take the first we receive which could result in a race between two suitable addresses where // one is better than the other (e.g. localhost vs LAN). return res.answer, res.addr, nil case <-ctx.Done(): return dnsmessage.ResourceHeader{}, netip.Addr{}, errContextElapsed } } } type ipToBytesError struct { addr netip.Addr expectedType string } func (err ipToBytesError) Error() string { return fmt.Sprintf("ip (%s) is not %s", err.addr, err.expectedType) } // assumes ipv4-to-ipv6 mapping has been checked func ipv4ToBytes(ipAddr netip.Addr) ([4]byte, error) { if !ipAddr.Is4() { return [4]byte{}, ipToBytesError{ipAddr, "IPv4"} } md, err := ipAddr.MarshalBinary() if err != nil { return [4]byte{}, err } // net.IPs are stored in big endian / network byte order var out [4]byte copy(out[:], md) return out, nil } // assumes ipv4-to-ipv6 mapping has been checked func ipv6ToBytes(ipAddr netip.Addr) ([16]byte, error) { if !ipAddr.Is6() { return [16]byte{}, ipToBytesError{ipAddr, "IPv6"} } md, err := ipAddr.MarshalBinary() if err != nil { return [16]byte{}, err } // net.IPs are stored in big endian / network byte order var out [16]byte copy(out[:], md) return out, nil } type ipToAddrError struct { ip []byte } func (err ipToAddrError) Error() string { return fmt.Sprintf("failed to convert ip address '%s' to netip.Addr", err.ip) } func interfaceForRemote(remote string) (*netip.Addr, error) { conn, err := net.Dial("udp", remote) if err != nil { return nil, err } localAddr, ok := conn.LocalAddr().(*net.UDPAddr) if !ok { return nil, errFailedCast } if err := conn.Close(); err != nil { return nil, err } ipAddr, ok := netip.AddrFromSlice(localAddr.IP) if !ok { return nil, ipToAddrError{localAddr.IP} } ipAddr = addrWithOptionalZone(ipAddr, localAddr.Zone) return &ipAddr, nil } type writeType byte const ( writeTypeQuestion writeType = iota writeTypeAnswer ) func (c *Conn) sendQuestion(name string) { packedName, err := dnsmessage.NewName(name) if err != nil { c.log.Warnf("[%s] failed to construct mDNS packet %v", c.name, err) return } // https://datatracker.ietf.org/doc/html/draft-ietf-rtcweb-mdns-ice-candidates-04#section-3.2.1 // // 2. Otherwise, resolve the candidate using mDNS. The ICE agent // SHOULD set the unicast-response bit of the corresponding mDNS // query message; this minimizes multicast traffic, as the response // is probably only useful to the querying node. // // 18.12. Repurposing of Top Bit of qclass in Question Section // // In the Question Section of a Multicast DNS query, the top bit of the // qclass field is used to indicate that unicast responses are preferred // for this particular question. (See Section 5.4.) // // We'll follow this up sending on our unicast based packet connections so that we can // get a unicast response back. msg := dnsmessage.Message{ Header: dnsmessage.Header{}, } // limit what we ask for based on what IPv is available. In the future, // this could be an option since there's no reason you cannot get an // A record on an IPv6 sourced question and vice versa. if c.multicastPktConnV4 != nil { msg.Questions = append(msg.Questions, dnsmessage.Question{ Type: dnsmessage.TypeA, Class: dnsmessage.ClassINET | (1 << 15), Name: packedName, }) } if c.multicastPktConnV6 != nil { msg.Questions = append(msg.Questions, dnsmessage.Question{ Type: dnsmessage.TypeAAAA, Class: dnsmessage.ClassINET | (1 << 15), Name: packedName, }) } rawQuery, err := msg.Pack() if err != nil { c.log.Warnf("[%s] failed to construct mDNS packet %v", c.name, err) return } c.writeToSocket(-1, rawQuery, false, false, writeTypeQuestion, nil) } //nolint:gocognit func (c *Conn) writeToSocket( ifIndex int, b []byte, hasLoopbackData bool, hasIPv6Zone bool, wType writeType, unicastDst *net.UDPAddr, ) { var dst4, dst6 net.Addr if wType == writeTypeAnswer { if unicastDst == nil { dst4 = c.dstAddr4 dst6 = c.dstAddr6 } else { if unicastDst.IP.To4() == nil { dst6 = unicastDst } else { dst4 = unicastDst } } } if ifIndex != -1 { if wType == writeTypeQuestion { c.log.Errorf("[%s] Unexpected question using specific interface index %d; dropping question", c.name, ifIndex) return } ifc, ok := c.ifaces[ifIndex] if !ok { c.log.Warnf("[%s] no interface for %d", c.name, ifIndex) return } if hasLoopbackData && ifc.Flags&net.FlagLoopback == 0 { // avoid accidentally tricking the destination that itself is the same as us c.log.Debugf("[%s] interface is not loopback %d", c.name, ifIndex) return } c.log.Debugf("[%s] writing answer to IPv4: %v, IPv6: %v", c.name, dst4, dst6) if ifc.supportsV4 && c.multicastPktConnV4 != nil && dst4 != nil { if !hasIPv6Zone { if _, err := c.multicastPktConnV4.WriteTo(b, &ifc.Interface, nil, dst4); err != nil { c.log.Warnf("[%s] failed to send mDNS packet on IPv4 interface %d: %v", c.name, ifIndex, err) } } else { c.log.Debugf("[%s] refusing to send mDNS packet with IPv6 zone over IPv4", c.name) } } if ifc.supportsV6 && c.multicastPktConnV6 != nil && dst6 != nil { if _, err := c.multicastPktConnV6.WriteTo(b, &ifc.Interface, nil, dst6); err != nil { c.log.Warnf("[%s] failed to send mDNS packet on IPv6 interface %d: %v", c.name, ifIndex, err) } } return } for ifcIdx := range c.ifaces { ifc := c.ifaces[ifcIdx] if hasLoopbackData { c.log.Debugf("[%s] Refusing to send loopback data with non-specific interface", c.name) continue } if wType == writeTypeQuestion { // we'll write via unicast if we can in case the responder chooses to respond to the address the request // came from (i.e. not respecting unicast-response bit). If we were to use the multicast packet // conn here, we'd be writing from a specific multicast address which won't be able to receive unicast // traffic (it only works when listening on 0.0.0.0/[::]). if c.unicastPktConnV4 == nil && c.unicastPktConnV6 == nil { c.log.Debugf("[%s] writing question to multicast IPv4/6 %s", c.name, c.dstAddr4) if ifc.supportsV4 && c.multicastPktConnV4 != nil { if _, err := c.multicastPktConnV4.WriteTo(b, &ifc.Interface, nil, c.dstAddr4); err != nil { c.log.Warnf("[%s] failed to send mDNS packet (multicast) on IPv4 interface %d: %v", c.name, ifc.Index, err) } } if ifc.supportsV6 && c.multicastPktConnV6 != nil { if _, err := c.multicastPktConnV6.WriteTo(b, &ifc.Interface, nil, c.dstAddr6); err != nil { c.log.Warnf("[%s] failed to send mDNS packet (multicast) on IPv6 interface %d: %v", c.name, ifc.Index, err) } } } if ifc.supportsV4 && c.unicastPktConnV4 != nil { c.log.Debugf("[%s] writing question to unicast IPv4 %s", c.name, c.dstAddr4) if _, err := c.unicastPktConnV4.WriteTo(b, &ifc.Interface, nil, c.dstAddr4); err != nil { c.log.Warnf("[%s] failed to send mDNS packet (unicast) on interface %d: %v", c.name, ifc.Index, err) } } if ifc.supportsV6 && c.unicastPktConnV6 != nil { c.log.Debugf("[%s] writing question to unicast IPv6 %s", c.name, c.dstAddr6) if _, err := c.unicastPktConnV6.WriteTo(b, &ifc.Interface, nil, c.dstAddr6); err != nil { c.log.Warnf("[%s] failed to send mDNS packet (unicast) on interface %d: %v", c.name, ifc.Index, err) } } } else { c.log.Debugf("[%s] writing answer to IPv4: %v, IPv6: %v", c.name, dst4, dst6) if ifc.supportsV4 && c.multicastPktConnV4 != nil && dst4 != nil { if !hasIPv6Zone { if _, err := c.multicastPktConnV4.WriteTo(b, &ifc.Interface, nil, dst4); err != nil { c.log.Warnf("[%s] failed to send mDNS packet (multicast) on IPv4 interface %d: %v", c.name, ifIndex, err) } } else { c.log.Debugf("[%s] refusing to send mDNS packet with IPv6 zone over IPv4", c.name) } } if ifc.supportsV6 && c.multicastPktConnV6 != nil && dst6 != nil { if _, err := c.multicastPktConnV6.WriteTo(b, &ifc.Interface, nil, dst6); err != nil { c.log.Warnf("[%s] failed to send mDNS packet (multicast) on IPv6 interface %d: %v", c.name, ifIndex, err) } } } } } func createAnswer(id uint16, name string, addr netip.Addr) (dnsmessage.Message, error) { packedName, err := dnsmessage.NewName(name) if err != nil { return dnsmessage.Message{}, err } msg := dnsmessage.Message{ Header: dnsmessage.Header{ ID: id, Response: true, Authoritative: true, }, Answers: []dnsmessage.Resource{ { Header: dnsmessage.ResourceHeader{ Class: dnsmessage.ClassINET, Name: packedName, TTL: responseTTL, }, }, }, } if addr.Is4() { ipBuf, err := ipv4ToBytes(addr) if err != nil { return dnsmessage.Message{}, err } msg.Answers[0].Header.Type = dnsmessage.TypeA msg.Answers[0].Body = &dnsmessage.AResource{ A: ipBuf, } } else if addr.Is6() { // we will lose the zone here, but the receiver can reconstruct it ipBuf, err := ipv6ToBytes(addr) if err != nil { return dnsmessage.Message{}, err } msg.Answers[0].Header.Type = dnsmessage.TypeAAAA msg.Answers[0].Body = &dnsmessage.AAAAResource{ AAAA: ipBuf, } } return msg, nil } func (c *Conn) sendAnswer(queryID uint16, name string, ifIndex int, result netip.Addr, dst *net.UDPAddr) { answer, err := createAnswer(queryID, name, result) if err != nil { c.log.Warnf("[%s] failed to create mDNS answer %v", c.name, err) return } rawAnswer, err := answer.Pack() if err != nil { c.log.Warnf("[%s] failed to construct mDNS packet %v", c.name, err) return } c.writeToSocket( ifIndex, rawAnswer, result.IsLoopback(), result.Is6() && result.Zone() != "", writeTypeAnswer, dst, ) } type ipControlMessage struct { IfIndex int Dst net.IP } type ipPacketConn interface { ReadFrom(b []byte) (n int, cm *ipControlMessage, src net.Addr, err error) WriteTo(b []byte, via *net.Interface, cm *ipControlMessage, dst net.Addr) (n int, err error) Close() error } type ipPacketConn4 struct { name string conn *ipv4.PacketConn log logging.LeveledLogger } func (c ipPacketConn4) ReadFrom(b []byte) (n int, cm *ipControlMessage, src net.Addr, err error) { n, cm4, src, err := c.conn.ReadFrom(b) if err != nil || cm4 == nil { return n, nil, src, err } return n, &ipControlMessage{IfIndex: cm4.IfIndex, Dst: cm4.Dst}, src, err } func (c ipPacketConn4) WriteTo(b []byte, via *net.Interface, cm *ipControlMessage, dst net.Addr) (n int, err error) { var cm4 *ipv4.ControlMessage if cm != nil { cm4 = &ipv4.ControlMessage{ IfIndex: cm.IfIndex, } } if err := c.conn.SetMulticastInterface(via); err != nil { c.log.Warnf("[%s] failed to set multicast interface for %d: %v", c.name, via.Index, err) return 0, err } return c.conn.WriteTo(b, cm4, dst) } func (c ipPacketConn4) Close() error { return c.conn.Close() } type ipPacketConn6 struct { name string conn *ipv6.PacketConn log logging.LeveledLogger } func (c ipPacketConn6) ReadFrom(b []byte) (n int, cm *ipControlMessage, src net.Addr, err error) { n, cm6, src, err := c.conn.ReadFrom(b) if err != nil || cm6 == nil { return n, nil, src, err } return n, &ipControlMessage{IfIndex: cm6.IfIndex, Dst: cm6.Dst}, src, err } func (c ipPacketConn6) WriteTo(b []byte, via *net.Interface, cm *ipControlMessage, dst net.Addr) (n int, err error) { var cm6 *ipv6.ControlMessage if cm != nil { cm6 = &ipv6.ControlMessage{ IfIndex: cm.IfIndex, } } if err := c.conn.SetMulticastInterface(via); err != nil { c.log.Warnf("[%s] failed to set multicast interface for %d: %v", c.name, via.Index, err) return 0, err } return c.conn.WriteTo(b, cm6, dst) } func (c ipPacketConn6) Close() error { return c.conn.Close() } func (c *Conn) readLoop(name string, pktConn ipPacketConn, inboundBufferSize int, config *Config) { //nolint:gocognit b := make([]byte, inboundBufferSize) p := dnsmessage.Parser{} for { n, cm, src, err := pktConn.ReadFrom(b) if err != nil { if errors.Is(err, net.ErrClosed) { return } c.log.Warnf("[%s] failed to ReadFrom %q %v", c.name, src, err) continue } c.log.Debugf("[%s] got read on %s from %s", c.name, name, src) var ifIndex int var pktDst net.IP if cm != nil { ifIndex = cm.IfIndex pktDst = cm.Dst } else { ifIndex = -1 } srcAddr, ok := src.(*net.UDPAddr) if !ok { c.log.Warnf("[%s] expected source address %s to be UDP but got %", c.name, src, src) continue } func() { header, err := p.Start(b[:n]) if err != nil { c.log.Warnf("[%s] failed to parse mDNS packet %v", c.name, err) return } for i := 0; i <= maxMessageRecords; i++ { q, err := p.Question() if errors.Is(err, dnsmessage.ErrSectionDone) { break } else if err != nil { c.log.Warnf("[%s] failed to parse mDNS packet %v", c.name, err) return } if q.Type != dnsmessage.TypeA && q.Type != dnsmessage.TypeAAAA { continue } // https://datatracker.ietf.org/doc/html/rfc6762#section-6 // The destination UDP port in all Multicast DNS responses MUST be 5353, // and the destination address MUST be the mDNS IPv4 link-local // multicast address 224.0.0.251 or its IPv6 equivalent FF02::FB, except // when generating a reply to a query that explicitly requested a // unicast response shouldUnicastResponse := (q.Class&(1<<15)) != 0 || // via the unicast-response bit srcAddr.Port != 5353 || // by virtue of being a legacy query (Section 6.7), or (len(pktDst) != 0 && !(pktDst.Equal(c.dstAddr4.IP) || // by virtue of being a direct unicast query pktDst.Equal(c.dstAddr6.IP))) var dst *net.UDPAddr if shouldUnicastResponse { dst = srcAddr } queryWantsV4 := q.Type == dnsmessage.TypeA for _, localName := range c.localNames { if localName == q.Name.String() { var localAddress *netip.Addr if config.LocalAddress != nil { // this means the LocalAddress does not support link-local since // we have no zone to set here. ipAddr, ok := netip.AddrFromSlice(config.LocalAddress) if !ok { c.log.Warnf("[%s] failed to convert config.LocalAddress '%s' to netip.Addr", c.name, config.LocalAddress) continue } if c.multicastPktConnV4 != nil { // don't want mapping since we also support IPv4/A ipAddr = ipAddr.Unmap() } localAddress = &ipAddr } else { // prefer the address of the interface if we know its index, but otherwise // derive it from the address we read from. We do this because even if // multicast loopback is in use or we send from a loopback interface, // there are still cases where the IP packet will contain the wrong // source IP (e.g. a LAN interface). // For example, we can have a packet that has: // Source: 192.168.65.3 // Destination: 224.0.0.251 // Interface Index: 1 // Interface Addresses @ 1: [127.0.0.1/8 ::1/128] if ifIndex != -1 { ifc, ok := c.ifaces[ifIndex] if !ok { c.log.Warnf("[%s] no interface for %d", c.name, ifIndex) return } var selectedAddrs []netip.Addr for _, addr := range ifc.ipAddrs { addrCopy := addr // match up respective IP types based on question if queryWantsV4 { if addrCopy.Is4In6() { // we may allow 4-in-6, but the question wants an A record addrCopy = addrCopy.Unmap() } if !addrCopy.Is4() { continue } } else { // queryWantsV6 if !addrCopy.Is6() { continue } if !isSupportedIPv6(addrCopy, c.multicastPktConnV4 == nil) { c.log.Debugf("[%s] interface %d address not a supported IPv6 address %s", c.name, ifIndex, &addrCopy) continue } } selectedAddrs = append(selectedAddrs, addrCopy) } if len(selectedAddrs) == 0 { c.log.Debugf("[%s] failed to find suitable IP for interface %d; deriving address from source address c.name,instead", c.name, ifIndex) } else { // choose the best match var choice *netip.Addr for _, option := range selectedAddrs { optCopy := option if option.Is4() { // select first choice = &optCopy break } // we're okay with 4In6 for now but ideally we get a an actual IPv6. // Maybe in the future we never want this but it does look like Docker // can route IPv4 over IPv6. if choice == nil { choice = &optCopy } else if !optCopy.Is4In6() { choice = &optCopy } if !optCopy.Is4In6() { break } // otherwise keep searching for an actual IPv6 } localAddress = choice } } if ifIndex == -1 || localAddress == nil { localAddress, err = interfaceForRemote(src.String()) if err != nil { c.log.Warnf("[%s] failed to get local interface to communicate with %s: %v", c.name, src.String(), err) continue } } } if queryWantsV4 { if !localAddress.Is4() { c.log.Debugf("[%s] have IPv6 address %s to respond with but question is for A not c.name,AAAA", c.name, localAddress) continue } } else { if !localAddress.Is6() { c.log.Debugf("[%s] have IPv4 address %s to respond with but question is for AAAA not c.name,A", c.name, localAddress) continue } if !isSupportedIPv6(*localAddress, c.multicastPktConnV4 == nil) { c.log.Debugf("[%s] got local interface address but not a supported IPv6 address %v", c.name, localAddress) continue } } if dst != nil && len(dst.IP) == net.IPv4len && localAddress.Is6() && localAddress.Zone() != "" && (localAddress.IsLinkLocalUnicast() || localAddress.IsLinkLocalMulticast()) { // This case happens when multicast v4 picks up an AAAA question that has a zone // in the address. Since we cannot send this zone over DNS (it's meaningless), // the other side can only infer this via the response interface on the other // side (some IPv6 interface). c.log.Debugf("[%s] refusing to send link-local address %s to an IPv4 destination %s", c.name, localAddress, dst) continue } c.log.Debugf("[%s] sending response for %s on ifc %d of %s to %s", c.name, q.Name, ifIndex, *localAddress, dst) c.sendAnswer(header.ID, q.Name.String(), ifIndex, *localAddress, dst) } } } for i := 0; i <= maxMessageRecords; i++ { a, err := p.AnswerHeader() if errors.Is(err, dnsmessage.ErrSectionDone) { return } if err != nil { c.log.Warnf("[%s] failed to parse mDNS packet %v", c.name, err) return } if a.Type != dnsmessage.TypeA && a.Type != dnsmessage.TypeAAAA { continue } c.mu.Lock() queries := make([]*query, len(c.queries)) copy(queries, c.queries) c.mu.Unlock() var answered []*query for _, query := range queries { queryCopy := query if queryCopy.nameWithSuffix == a.Name.String() { addr, err := addrFromAnswerHeader(a, p) if err != nil { c.log.Warnf("[%s] failed to parse mDNS answer %v", c.name, err) return } resultAddr := *addr // DNS records don't contain IPv6 zones. // We're trusting that since we're on the same link, that we will only // be sent link-local addresses from that source's interface's address. // If it's not present, we're out of luck since we cannot rely on the // interface zone to be the same as the source's. resultAddr = addrWithOptionalZone(resultAddr, srcAddr.Zone) select { case queryCopy.queryResultChan <- queryResult{a, resultAddr}: answered = append(answered, queryCopy) default: } } } c.mu.Lock() for queryIdx := len(c.queries) - 1; queryIdx >= 0; queryIdx-- { for answerIdx := len(answered) - 1; answerIdx >= 0; answerIdx-- { if c.queries[queryIdx] == answered[answerIdx] { c.queries = append(c.queries[:queryIdx], c.queries[queryIdx+1:]...) answered = append(answered[:answerIdx], answered[answerIdx+1:]...) queryIdx-- break } } } c.mu.Unlock() } }() } } func (c *Conn) start(started chan<- struct{}, inboundBufferSize int, config *Config) { defer func() { c.mu.Lock() defer c.mu.Unlock() close(c.closed) }() var numReaders int readerStarted := make(chan struct{}) readerEnded := make(chan struct{}) if c.multicastPktConnV4 != nil { numReaders++ go func() { defer func() { readerEnded <- struct{}{} }() readerStarted <- struct{}{} c.readLoop("multi4", c.multicastPktConnV4, inboundBufferSize, config) }() } if c.multicastPktConnV6 != nil { numReaders++ go func() { defer func() { readerEnded <- struct{}{} }() readerStarted <- struct{}{} c.readLoop("multi6", c.multicastPktConnV6, inboundBufferSize, config) }() } if c.unicastPktConnV4 != nil { numReaders++ go func() { defer func() { readerEnded <- struct{}{} }() readerStarted <- struct{}{} c.readLoop("uni4", c.unicastPktConnV4, inboundBufferSize, config) }() } if c.unicastPktConnV6 != nil { numReaders++ go func() { defer func() { readerEnded <- struct{}{} }() readerStarted <- struct{}{} c.readLoop("uni6", c.unicastPktConnV6, inboundBufferSize, config) }() } for i := 0; i < numReaders; i++ { <-readerStarted } close(started) for i := 0; i < numReaders; i++ { <-readerEnded } } func addrFromAnswerHeader(a dnsmessage.ResourceHeader, p dnsmessage.Parser) (addr *netip.Addr, err error) { if a.Type == dnsmessage.TypeA { resource, err := p.AResource() if err != nil { return nil, err } ipAddr, ok := netip.AddrFromSlice(resource.A[:]) if !ok { return nil, fmt.Errorf("failed to convert A record: %w", ipToAddrError{resource.A[:]}) } ipAddr = ipAddr.Unmap() // do not want 4-in-6 addr = &ipAddr } else { resource, err := p.AAAAResource() if err != nil { return nil, err } ipAddr, ok := netip.AddrFromSlice(resource.AAAA[:]) if !ok { return nil, fmt.Errorf("failed to convert AAAA record: %w", ipToAddrError{resource.AAAA[:]}) } addr = &ipAddr } return } func isSupportedIPv6(addr netip.Addr, ipv6Only bool) bool { if !addr.Is6() { return false } // IPv4-mapped-IPv6 addresses cannot be connected to unless // unmapped. if !ipv6Only && addr.Is4In6() { return false } return true } func addrWithOptionalZone(addr netip.Addr, zone string) netip.Addr { if zone == "" { return addr } if addr.Is6() && (addr.IsLinkLocalUnicast() || addr.IsLinkLocalMulticast()) { return addr.WithZone(zone) } return addr }