diff --git a/ethernet/definitions.go b/ethernet/definitions.go index e131f6a..6303911 100644 --- a/ethernet/definitions.go +++ b/ethernet/definitions.go @@ -33,6 +33,28 @@ func BroadcastAddr() [6]byte { return [6]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff} } +// IPv4MulticastToMAC maps an IPv4 multicast address to the corresponding +// Ethernet multicast MAC address. +// +// If ip is not in the IPv4 multicast range (224.0.0.0/4), ok is false and +// mac is zero. +func MulticastAddrFrom4(ip [4]byte) (mac [6]byte, ok bool) { + if ip[0]&0xf0 != 0xe0 { + return mac, false + } + + mac[0] = 0x01 + mac[1] = 0x00 + mac[2] = 0x5e + + // Lower 23 bits of IP + mac[3] = ip[1] & 0x7f // drop top bit + mac[4] = ip[2] + mac[5] = ip[3] + + return mac, true +} + //go:generate stringer -type=Type -linecomment -output stringers.go . type Type uint16 diff --git a/ipv4/frame.go b/ipv4/frame.go index a90f1e9..d1e3ed0 100644 --- a/ipv4/frame.go +++ b/ipv4/frame.go @@ -22,6 +22,7 @@ func NewFrame(buf []byte) (Frame, error) { // Frame encapsulates the raw data of an IPv4 packet // and provides methods for manipulating, validating and // retreiving fields and payload data. See [RFC791]. +// Below is an example of setting IPv4 fields for MDNS: // // [RFC791]: https://tools.ietf.org/html/rfc791 type Frame struct { @@ -156,6 +157,12 @@ func (ifrm Frame) CRCWriteTCPPseudo(crc *lneto.CRC791) { crc.AddUint16(uint16(ifrm.Protocol())) } +// CRCWriteUDPPseudo writes the IPv4 UDP pseudo-header into crc for checksum +// calculation. Typical usage for computing or validating a UDP checksum: +// +// ifrm.CRCWriteUDPPseudo(&crc, ufrm.Length()) +// // ufrm.SetCRC(0) // Zero before computing checksum for transmission. +// crcValue := crc.PayloadSum16(ifrm.Payload()) // For received frames, crcValue should be zero if valid. func (ifrm Frame) CRCWriteUDPPseudo(crc *lneto.CRC791, udpLength uint16) { crc.WriteEven(ifrm.sourceAndDestinationAddr()) crc.AddUint16(udpLength) diff --git a/mdns/definitions.go b/mdns/definitions.go index d6ca4ae..ba470c7 100644 --- a/mdns/definitions.go +++ b/mdns/definitions.go @@ -4,6 +4,19 @@ import ( "github.com/soypat/lneto/dns" ) +// IPv4MulticastAddr is the IPv4 multicast address used by mDNS (224.0.0.251). +// Defined by RFC 6762. Packets sent to this address use UDP port 5353 and are +// link-local (not routed beyond the local network segment). +func IPv4MulticastAddr() [4]byte { + return [4]byte{224, 0, 0, 251} +} + +// IPv4MulticastMAC is the Ethernet multicast MAC address corresponding to +// 224.0.0.251 (01:00:5e:00:00:fb). Used for L2 delivery of mDNS over Ethernet. +func IPv4MulticastMAC() [6]byte { + return [6]byte{0x01, 0x00, 0x5e, 0x00, 0x00, 0xfb} +} + // Service describes a service to advertise via mDNS. // A single Service produces PTR, SRV, TXT, and A resource records. // @@ -68,6 +81,23 @@ func matchQuestion(q *dns.Question, svc *Service) bool { } return false } +func MulticastMAC(ip [4]byte) (mac [6]byte, ok bool) { + // Check IPv4 multicast range: 224.0.0.0/4 + if ip[0]&0xf0 != 0xe0 { + return mac, false + } + + mac[0] = 0x01 + mac[1] = 0x00 + mac[2] = 0x5e + + // Lower 23 bits of IP + mac[3] = ip[1] & 0x7f // drop top bit + mac[4] = ip[2] + mac[5] = ip[3] + + return mac, true +} // addServiceAnswers adds the appropriate answer records for a matched question. // It grows ans in-place, reusing existing Resource buffers when available. diff --git a/x/xnet/xnet_mdns_test.go b/x/xnet/xnet_mdns_test.go index e9d68dd..8b2cc25 100644 --- a/x/xnet/xnet_mdns_test.go +++ b/x/xnet/xnet_mdns_test.go @@ -5,9 +5,12 @@ import ( "net/netip" "testing" + "github.com/soypat/lneto" "github.com/soypat/lneto/dns" "github.com/soypat/lneto/ethernet" + "github.com/soypat/lneto/ipv4" "github.com/soypat/lneto/mdns" + "github.com/soypat/lneto/udp" ) func TestMDNS_QueryResponse(t *testing.T) { @@ -330,3 +333,129 @@ func mdnsQueryRespond(t *testing.T, querier, responder *StackAsync, buf []byte) t.Fatal("querier demux:", err) } } + +func TestMDNS_RealWorldQueries(t *testing.T) { + const MTU = 1500 + + responderMAC := [6]byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x01} + querierMAC := [6]byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x02} + + responderIP := netip.AddrFrom4([4]byte{192, 168, 50, 81}) + querierIP := netip.AddrFrom4([4]byte{192, 168, 50, 213}) + + mcastAddr := mdns.IPv4MulticastAddr() + + // Service hosted by responder. + hostName := dns.MustNewName("server.local") + svc := mdns.Service{ + Name: hostName, + Host: hostName, + Addr: []byte{192, 168, 50, 81}, + Port: 80, + } + + responderStack, _ := newMDNSStack(t, "responder", 1, + responderIP, responderMAC, querierMAC, + mdns.ClientConfig{ + LocalPort: mdns.Port, + Services: []mdns.Service{svc}, + MulticastAddr: mcastAddr[:], + }, + ) + + pkts := []struct { + name string + qname string + qtype dns.Type + unicast bool // QU bit + }{ + // Frame 1: QM multicast A server.local + {"QM_A_server", "server.local", dns.TypeA, false}, + + // Frame 2: QU unicast AAAA random.local + {"QU_AAAA_random", "rds-th-TH010-e6614864d3511735.local", dns.TypeAAAA, true}, + + // Frame 3: QU unicast A random.local + {"QU_A_random", "rds-th-TH010-e6614864d3511735.local", dns.TypeA, true}, + } + + var buf [MTU + ethernet.MaxOverheadSize]byte + checkNoData := func(msg string) { + t.Helper() + n, err := responderStack.Encapsulate(buf[:], -1, 0) + if err != nil { + t.Fatal(err) + } else if n != 0 { + t.Errorf(" %s: expected no data sent: %d", msg, n) + } + } + checkNoData("before transaction") + var msg dns.Message + for _, q := range pkts { + msg.Reset() + msg.AddQuestions([]dns.Question{ + { + Name: dns.MustNewName(q.qname), + Type: q.qtype, + Class: withQU(q.unicast), + }, + }) + efrm, _ := ethernet.NewFrame(buf[:]) + *efrm.DestinationHardwareAddr(), _ = ethernet.MulticastAddrFrom4(mdns.IPv4MulticastAddr()) + *efrm.SourceHardwareAddr() = querierMAC + efrm.SetEtherType(ethernet.TypeIPv4) + + ifrm, _ := ipv4.NewFrame(efrm.Payload()) + ifrm.SetVersionAndIHL(4, 5) // No options. IHL=5, IPLEN=4*IHL=20 + ifrm.SetToS(ipv4.NewToS(0, 0)) + ifrm.SetTotalLength(20 + 8 + msg.Len()) + ifrm.SetID(1337) + ifrm.SetFlags(ipv4.FlagDontFragment) + ifrm.SetTTL(64) + ifrm.SetProtocol(lneto.IPProtoUDP) + + *ifrm.SourceAddr() = querierIP.As4() + *ifrm.DestinationAddr() = mdns.IPv4MulticastAddr() + + ifrm.SetCRC(0) // Zero CRC before calculating CRC, as custom with lneto. + ifrm.SetCRC(ifrm.CalculateHeaderCRC()) + + ufrm, _ := udp.NewFrame(ifrm.Payload()) + ufrm.SetSourcePort(mdns.Port) + ufrm.SetDestinationPort(mdns.Port) + ufrm.SetLength(8 + msg.Len()) + + mdnsPayload, _ := msg.AppendTo(ufrm.Payload()[:0], 0, 0) + if len(mdnsPayload) != int(msg.Len()) { + t.Fatal("unreachable") + } + var crc lneto.CRC791 + ufrm.SetCRC(0) + ifrm.CRCWriteUDPPseudo(&crc, ufrm.Length()) + got := crc.PayloadSum16(ifrm.Payload()) + ufrm.SetCRC(got) + err := responderStack.Demux(buf[:14+20+8+msg.Len()], 0) + if err != nil { + t.Fatal(err) + } + } + n, err := responderStack.Encapsulate(buf[:], -1, 0) + if err != nil { + t.Fatal(err) + } else if n < 14+20+8+dns.SizeHeader { + t.Error("expected response", n) + } + n, err = responderStack.Encapsulate(buf[:], -1, 0) + if err != nil { + t.Fatal(err) + } else if n != 0 { + t.Error("expected single response") + } +} + +func withQU(unicast bool) dns.Class { + if !unicast { + return dns.ClassINET + } + return dns.ClassINET | (1 << 15) // QU bit +}