Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Only include loopback address in responses when configured to #173

Merged
merged 2 commits into from
Feb 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,10 @@ type Config struct {
LocalAddress net.IP

LoggerFactory logging.LoggerFactory

// IncludeLoopback will include loopback interfaces to be eligble for queries and answers.
IncludeLoopback bool

// Interfaces will override the interfaces used for queries and answers.
Interfaces []net.Interface
}
219 changes: 190 additions & 29 deletions conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import (
"context"
"errors"
"math/big"
"fmt"
"net"
"sync"
"time"
Expand Down Expand Up @@ -57,22 +57,32 @@

var errNoPositiveMTUFound = errors.New("no positive MTU found")

// Server establishes a mDNS connection over an existing conn
// Server establishes a mDNS connection over an existing conn.
//
// Currently, the server only supports listening on an IPv4 connection, but internally
// it supports answering with IPv6 AAAA records if this were ever to change.
func Server(conn *ipv4.PacketConn, config *Config) (*Conn, error) {
if config == nil {
return nil, errNilConfig
}

ifaces, err := net.Interfaces()
if err != nil {
return nil, err
ifaces := config.Interfaces
if ifaces == nil {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Helpful for tests!

var err error
ifaces, err = net.Interfaces()
if err != nil {
return nil, err
}

Check warning on line 75 in conn.go

View check run for this annotation

Codecov / codecov/patch

conn.go#L74-L75

Added lines #L74 - L75 were not covered by tests
}

inboundBufferSize := 0
joinErrCount := 0
ifacesToUse := make([]net.Interface, 0, len(ifaces))
for i, ifc := range ifaces {
if err = conn.JoinGroup(&ifaces[i], &net.UDPAddr{IP: net.IPv4(224, 0, 0, 251)}); err != nil {
if !config.IncludeLoopback && ifc.Flags&net.FlagLoopback == net.FlagLoopback {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the fix

continue
}
if err := conn.JoinGroup(&ifaces[i], &net.UDPAddr{IP: net.IPv4(224, 0, 0, 251)}); err != nil {
joinErrCount++
continue
}
Expand Down Expand Up @@ -127,6 +137,14 @@
c.log.Warnf("Failed to SetControlMessage on PacketConn %v", err)
}

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 err := conn.SetMulticastLoopback(true); err != nil {
c.log.Warnf("Failed to SetMulticastLoopback(true) on PacketConn %v; this may cause inefficient network path communications", err)
}

Check warning on line 145 in conn.go

View check run for this annotation

Codecov / codecov/patch

conn.go#L144-L145

Added lines #L144 - L145 were not covered by tests
}

// 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
Expand Down Expand Up @@ -189,23 +207,49 @@
case <-c.closed:
return dnsmessage.ResourceHeader{}, nil, errConnectionClosed
case res := <-queryChan:
// Given https://datatracker.ietf.org/doc/html/draft-ietf-mmusic-mdns-ice-candidates#section-3.2.2-2
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering why we don't collect all responses and either way, we're not supposed to!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch!

// 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{}, nil, errContextElapsed
}
}
}

func ipToBytes(ip net.IP) (out [4]byte) {
type ipToBytesError struct {
ip net.IP
expectedType string
}

func (err ipToBytesError) Error() string {
return fmt.Sprintf("ip (%s) is not %s", err.ip, err.expectedType)

Check warning on line 228 in conn.go

View check run for this annotation

Codecov / codecov/patch

conn.go#L227-L228

Added lines #L227 - L228 were not covered by tests
}

func ipv4ToBytes(ip net.IP) ([4]byte, error) {
rawIP := ip.To4()
if rawIP == nil {
return
return [4]byte{}, ipToBytesError{ip, "IPv4"}
}

ipInt := big.NewInt(0)
ipInt.SetBytes(rawIP)
copy(out[:], ipInt.Bytes())
return
// net.IPs are stored in big endian / network byte order
var out [4]byte
copy(out[:], rawIP[:])
return out, nil
}

func ipv6ToBytes(ip net.IP) ([16]byte, error) {
rawIP := ip.To16()
if rawIP == nil {
return [16]byte{}, ipToBytesError{ip, "IPv6"}
}

Check warning on line 247 in conn.go

View check run for this annotation

Codecov / codecov/patch

conn.go#L246-L247

Added lines #L246 - L247 were not covered by tests

// net.IPs are stored in big endian / network byte order
var out [16]byte
copy(out[:], rawIP[:])
return out, nil
}

func interfaceForRemote(remote string) (net.IP, error) {
Expand Down Expand Up @@ -253,14 +297,14 @@
c.writeToSocket(0, rawQuery, false)
}

func (c *Conn) writeToSocket(ifIndex int, b []byte, onlyLooback bool) {
func (c *Conn) writeToSocket(ifIndex int, b []byte, srcIfcIsLoopback bool) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better naming

if ifIndex != 0 {
ifc, err := net.InterfaceByIndex(ifIndex)
if err != nil {
c.log.Warnf("Failed to get interface interface for %d: %v", ifIndex, err)
c.log.Warnf("Failed to get interface for %d: %v", ifIndex, err)

Check warning on line 304 in conn.go

View check run for this annotation

Codecov / codecov/patch

conn.go#L304

Added line #L304 was not covered by tests
return
}
if onlyLooback && ifc.Flags&net.FlagLoopback == 0 {
if srcIfcIsLoopback && ifc.Flags&net.FlagLoopback == 0 {
// avoid accidentally tricking the destination that itself is the same as us
c.log.Warnf("Interface is not loopback %d", ifIndex)
return
Expand All @@ -275,7 +319,7 @@
return
}
for ifcIdx := range c.ifaces {
if onlyLooback && c.ifaces[ifcIdx].Flags&net.FlagLoopback == 0 {
if srcIfcIsLoopback && c.ifaces[ifcIdx].Flags&net.FlagLoopback == 0 {
// avoid accidentally tricking the destination that itself is the same as us
continue
}
Expand All @@ -289,11 +333,10 @@
}
}

func (c *Conn) sendAnswer(name string, ifIndex int, dst net.IP) {
func createAnswer(name string, addr net.IP) (dnsmessage.Message, error) {
packedName, err := dnsmessage.NewName(name)
if err != nil {
c.log.Warnf("Failed to construct mDNS packet %v", err)
return
return dnsmessage.Message{}, err

Check warning on line 339 in conn.go

View check run for this annotation

Codecov / codecov/patch

conn.go#L339

Added line #L339 was not covered by tests
}

msg := dnsmessage.Message{
Expand All @@ -309,20 +352,45 @@
Name: packedName,
TTL: responseTTL,
},
Body: &dnsmessage.AResource{
A: ipToBytes(dst),
},
},
},
}

rawAnswer, err := msg.Pack()
if ip4 := addr.To4(); ip4 != nil {
ipBuf, err := ipv4ToBytes(addr)
if err != nil {
return dnsmessage.Message{}, err
}

Check warning on line 363 in conn.go

View check run for this annotation

Codecov / codecov/patch

conn.go#L362-L363

Added lines #L362 - L363 were not covered by tests
msg.Answers[0].Body = &dnsmessage.AResource{
A: ipBuf,
}
} else {
ipBuf, err := ipv6ToBytes(addr)
if err != nil {
return dnsmessage.Message{}, err
}

Check warning on line 371 in conn.go

View check run for this annotation

Codecov / codecov/patch

conn.go#L370-L371

Added lines #L370 - L371 were not covered by tests
msg.Answers[0].Body = &dnsmessage.AAAAResource{
AAAA: ipBuf,
}
}

return msg, nil
}

func (c *Conn) sendAnswer(name string, ifIndex int, addr net.IP) {
answer, err := createAnswer(name, addr)
if err != nil {
c.log.Warnf("Failed to create mDNS answer %v", err)
return
}

Check warning on line 385 in conn.go

View check run for this annotation

Codecov / codecov/patch

conn.go#L383-L385

Added lines #L383 - L385 were not covered by tests

rawAnswer, err := answer.Pack()
if err != nil {
c.log.Warnf("Failed to construct mDNS packet %v", err)
return
}

c.writeToSocket(ifIndex, rawAnswer, dst.IsLoopback())
c.writeToSocket(ifIndex, rawAnswer, addr.IsLoopback())
}

func (c *Conn) start(inboundBufferSize int, config *Config) { //nolint gocognit
Expand All @@ -348,6 +416,17 @@
if cm != nil {
ifIndex = cm.IfIndex
}
var srcIP net.IP
switch addr := src.(type) {
case *net.UDPAddr:
srcIP = addr.IP
case *net.TCPAddr:
srcIP = addr.IP
default:
c.log.Warnf("Failed to determine address type %T for source address %s", src, src)
continue

Check warning on line 427 in conn.go

View check run for this annotation

Codecov / codecov/patch

conn.go#L423-L427

Added lines #L423 - L427 were not covered by tests
}
srcIsIPv4 := srcIP.To4() != nil

func() {
c.mu.RLock()
Expand All @@ -372,10 +451,70 @@
if config.LocalAddress != nil {
c.sendAnswer(q.Name.String(), ifIndex, config.LocalAddress)
} else {
localAddress, err := interfaceForRemote(src.String())
if err != nil {
c.log.Warnf("Failed to get local interface to communicate with %s: %v", src.String(), err)
continue
var localAddress net.IP

// prefer the address of the interface if we know its index, but otherwise
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this took a while to figure out but will provide further network path efficiencies but really it was to fix tests on linux :)

// 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 != 0 {
ifc, netErr := net.InterfaceByIndex(ifIndex)
if netErr != nil {
c.log.Warnf("Failed to get interface for %d: %v", ifIndex, netErr)
continue

Check warning on line 470 in conn.go

View check run for this annotation

Codecov / codecov/patch

conn.go#L469-L470

Added lines #L469 - L470 were not covered by tests
}
addrs, addrsErr := ifc.Addrs()
if addrsErr != nil {
c.log.Warnf("Failed to get addresses for interface %d: %v", ifIndex, addrsErr)
continue

Check warning on line 475 in conn.go

View check run for this annotation

Codecov / codecov/patch

conn.go#L474-L475

Added lines #L474 - L475 were not covered by tests
}
if len(addrs) == 0 {
c.log.Warnf("Expected more than one address for interface %d", ifIndex)
continue

Check warning on line 479 in conn.go

View check run for this annotation

Codecov / codecov/patch

conn.go#L478-L479

Added lines #L478 - L479 were not covered by tests
}
var selectedIP net.IP
for _, addr := range addrs {
var ip net.IP
switch addr := addr.(type) {
case *net.IPNet:
ip = addr.IP
case *net.IPAddr:
ip = addr.IP
default:
c.log.Warnf("Failed to determine address type %T from interface %d", addr, ifIndex)
continue

Check warning on line 491 in conn.go

View check run for this annotation

Codecov / codecov/patch

conn.go#L487-L491

Added lines #L487 - L491 were not covered by tests
}

// match up respective IP types
if ipv4 := ip.To4(); ipv4 == nil {
if srcIsIPv4 {
continue
} else if !isSupportedIPv6(ip) {
continue

Check warning on line 499 in conn.go

View check run for this annotation

Codecov / codecov/patch

conn.go#L496-L499

Added lines #L496 - L499 were not covered by tests
}
} else if !srcIsIPv4 {
continue

Check warning on line 502 in conn.go

View check run for this annotation

Codecov / codecov/patch

conn.go#L502

Added line #L502 was not covered by tests
}
selectedIP = ip
break
}
if selectedIP == nil {
c.log.Warnf("Failed to find suitable IP for interface %d; deriving address from source address instead", ifIndex)

Check warning on line 508 in conn.go

View check run for this annotation

Codecov / codecov/patch

conn.go#L508

Added line #L508 was not covered by tests
} else {
localAddress = selectedIP
}
} else if ifIndex == 0 || localAddress == nil {
localAddress, err = interfaceForRemote(src.String())
if err != nil {
c.log.Warnf("Failed to get local interface to communicate with %s: %v", src.String(), err)
continue

Check warning on line 516 in conn.go

View check run for this annotation

Codecov / codecov/patch

conn.go#L512-L516

Added lines #L512 - L516 were not covered by tests
}
}

c.sendAnswer(q.Name.String(), ifIndex, localAddress)
Expand Down Expand Up @@ -423,7 +562,7 @@
if err != nil {
return nil, err
}
ip = net.IP(resource.A[:])
ip = resource.A[:]
} else {
resource, err := p.AAAAResource()
if err != nil {
Expand All @@ -434,3 +573,25 @@

return
}

// The conditions of invalidation written below are defined in
// https://tools.ietf.org/html/rfc8445#section-5.1.1.1
func isSupportedIPv6(ip net.IP) bool {
if len(ip) != net.IPv6len ||
isZeros(ip[0:12]) || // !(IPv4-compatible IPv6)
ip[0] == 0xfe && ip[1]&0xc0 == 0xc0 || // !(IPv6 site-local unicast)
ip.IsLinkLocalUnicast() ||
ip.IsLinkLocalMulticast() {
return false
}
return true

Check warning on line 587 in conn.go

View check run for this annotation

Codecov / codecov/patch

conn.go#L579-L587

Added lines #L579 - L587 were not covered by tests
}

func isZeros(ip net.IP) bool {
for i := 0; i < len(ip); i++ {
if ip[i] != 0 {
return false
}

Check warning on line 594 in conn.go

View check run for this annotation

Codecov / codecov/patch

conn.go#L590-L594

Added lines #L590 - L594 were not covered by tests
}
return true

Check warning on line 596 in conn.go

View check run for this annotation

Codecov / codecov/patch

conn.go#L596

Added line #L596 was not covered by tests
}
Loading
Loading