Skip to content

Commit

Permalink
Support IPv6 from mDNS
Browse files Browse the repository at this point in the history
This also adds support for IPv6 with zones, making use
of the netip package. This breaks the TCPMux and AllConnsGetter
interfaces to get more accurate Mux matching.
  • Loading branch information
edaniels committed Mar 25, 2024
1 parent 66051b6 commit 7511f91
Show file tree
Hide file tree
Showing 27 changed files with 613 additions and 302 deletions.
9 changes: 7 additions & 2 deletions active_tcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ func newActiveTCPConn(ctx context.Context, localAddress, remoteAddress string, l
log.Infof("Failed to dial TCP address %s: %v", remoteAddress, err)
return
}

a.remoteAddr.Store(conn.RemoteAddr())

go func() {
Expand Down Expand Up @@ -95,8 +94,9 @@ func (a *activeTCPConn) ReadFrom(buff []byte) (n int, srcAddr net.Addr, err erro
return 0, nil, io.ErrClosedPipe
}

srcAddr = a.RemoteAddr()
n, err = a.readBuffer.Read(buff)
// RemoteAddr is assuredly set *after* we can read from the buffer
srcAddr = a.RemoteAddr()
return
}

Expand All @@ -123,6 +123,11 @@ func (a *activeTCPConn) LocalAddr() net.Addr {
return &net.TCPAddr{}
}

// RemoteAddr returns the remote address of the connection which is only
// set once a background goroutine has successfully dialed. That means
// this may return ":0" for the address prior to that happening. If this
// becomes an issue, we can introduce a synchronization point between Dial
// and these methods.
func (a *activeTCPConn) RemoteAddr() net.Addr {
if v, ok := a.remoteAddr.Load().(*net.TCPAddr); ok {
return v
Expand Down
29 changes: 19 additions & 10 deletions active_tcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package ice

import (
"net"
"net/netip"
"testing"
"time"

Expand All @@ -17,21 +18,21 @@ import (
"github.com/stretchr/testify/require"
)

func getLocalIPAddress(t *testing.T, networkType NetworkType) net.IP {
func getLocalIPAddress(t *testing.T, networkType NetworkType) netip.Addr {
net, err := stdnet.NewNet()
require.NoError(t, err)
localIPs, err := localInterfaces(net, nil, nil, []NetworkType{networkType}, false)
_, localAddrs, err := localInterfaces(net, problematicNetworkInterfaces, nil, []NetworkType{networkType}, false)
require.NoError(t, err)
require.NotEmpty(t, localIPs)
return localIPs[0]
require.NotEmpty(t, localAddrs)
return localAddrs[0]
}

func ipv6Available(t *testing.T) bool {
net, err := stdnet.NewNet()
require.NoError(t, err)
localIPs, err := localInterfaces(net, nil, nil, []NetworkType{NetworkTypeTCP6}, false)
_, localAddrs, err := localInterfaces(net, problematicNetworkInterfaces, nil, []NetworkType{NetworkTypeTCP6}, false)
require.NoError(t, err)
return len(localIPs) > 0
return len(localAddrs) > 0
}

func TestActiveTCP(t *testing.T) {
Expand All @@ -43,7 +44,7 @@ func TestActiveTCP(t *testing.T) {
type testCase struct {
name string
networkTypes []NetworkType
listenIPAddress net.IP
listenIPAddress netip.Addr
selectedPairNetworkType string
}

Expand Down Expand Up @@ -84,8 +85,9 @@ func TestActiveTCP(t *testing.T) {
r := require.New(t)

listener, err := net.ListenTCP("tcp", &net.TCPAddr{
IP: testCase.listenIPAddress,
IP: testCase.listenIPAddress.AsSlice(),
Port: listenPort,
Zone: testCase.listenIPAddress.Zone(),
})
r.NoError(err)
defer func() {
Expand All @@ -112,8 +114,13 @@ func TestActiveTCP(t *testing.T) {
CandidateTypes: []CandidateType{CandidateTypeHost},
NetworkTypes: testCase.networkTypes,
LoggerFactory: loggerFactory,
IncludeLoopback: true,
HostAcceptanceMinWait: &hostAcceptanceMinWait,
InterfaceFilter: problematicNetworkInterfaces,

// Note: this will instruct the active agent to try to dial a loopback
// address which will fail because no test case is listening on it. This is
// fine so long as we're not expecting to test loopback functionality here.
IncludeLoopback: true,
})
r.NoError(err)
r.NotNil(passiveAgent)
Expand All @@ -123,6 +130,7 @@ func TestActiveTCP(t *testing.T) {
NetworkTypes: testCase.networkTypes,
LoggerFactory: loggerFactory,
HostAcceptanceMinWait: &hostAcceptanceMinWait,
InterfaceFilter: problematicNetworkInterfaces,
})
r.NoError(err)
r.NotNil(activeAgent)
Expand Down Expand Up @@ -166,7 +174,8 @@ func TestActiveTCP_NonBlocking(t *testing.T) {
defer test.TimeOut(time.Second * 5).Stop()

cfg := &AgentConfig{
NetworkTypes: supportedNetworkTypes(),
NetworkTypes: supportedNetworkTypes(),
InterfaceFilter: problematicNetworkInterfaces,
}

aAgent, err := NewAgent(cfg)
Expand Down
99 changes: 79 additions & 20 deletions addr.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,52 +4,111 @@
package ice

import (
"fmt"
"net"
"net/netip"
)

func parseMulticastAnswerAddr(in net.Addr) (net.IP, bool) {
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
}

// parseAddrFromIface should only be used when it's known the address belongs to that interface.
// e.g. it's LocalAddress on a listener.
func parseAddrFromIface(in net.Addr, ifcName string) (netip.Addr, int, NetworkType, error) {
addr, port, nt, err := parseAddr(in)
if err != nil {
return netip.Addr{}, 0, 0, err
}
switch in.(type) {
case *net.IPNet:
// net.IPNet does not have a Zone but we provide it from the interface
addr = addrWithOptionalZone(addr, ifcName)
}
return addr, port, nt, nil
}

func parseAddr(in net.Addr) (netip.Addr, int, NetworkType, error) {
switch addr := in.(type) {
case *net.IPNet:
ipAddr, err := ipAddrToNetIP(addr.IP, "")
if err != nil {
return netip.Addr{}, 0, 0, err
}
return ipAddr, 0, 0, nil
case *net.IPAddr:
return addr.IP, true
ipAddr, err := ipAddrToNetIP(addr.IP, addr.Zone)
if err != nil {
return netip.Addr{}, 0, 0, err
}
return ipAddr, 0, 0, nil
case *net.UDPAddr:
return addr.IP, true
ipAddr, err := ipAddrToNetIP(addr.IP, addr.Zone)
if err != nil {
return netip.Addr{}, 0, 0, err
}
var nt NetworkType
if ipAddr.Is4() {
nt = NetworkTypeUDP4
} else {
nt = NetworkTypeUDP6
}
return ipAddr, addr.Port, nt, nil
case *net.TCPAddr:
return addr.IP, true
ipAddr, err := ipAddrToNetIP(addr.IP, addr.Zone)
if err != nil {
return netip.Addr{}, 0, 0, err
}
var nt NetworkType
if ipAddr.Is4() {
nt = NetworkTypeTCP4
} else {
nt = NetworkTypeTCP6
}
return ipAddr, addr.Port, nt, nil
default:
return netip.Addr{}, 0, 0, fmt.Errorf("do not know how to parse address type %T", in)
}
return nil, false
}

func parseAddr(in net.Addr) (net.IP, int, NetworkType, bool) {
switch addr := in.(type) {
case *net.UDPAddr:
return addr.IP, addr.Port, NetworkTypeUDP4, true
case *net.TCPAddr:
return addr.IP, addr.Port, NetworkTypeTCP4, true
func ipAddrToNetIP(ip []byte, zone string) (netip.Addr, error) {
netIPAddr, ok := netip.AddrFromSlice(ip)
if !ok {
return netip.Addr{}, fmt.Errorf("failed to convert IP '%s' to netip.Addr", ip)
}
return nil, 0, 0, false
// we'd rather have an IPv4-mapped IPv6 become IPv4 so that it is usable.
netIPAddr = netIPAddr.Unmap()
netIPAddr = addrWithOptionalZone(netIPAddr, zone)
return netIPAddr, nil
}

func createAddr(network NetworkType, ip net.IP, port int) net.Addr {
func createAddr(network NetworkType, ip netip.Addr, port int) net.Addr {
switch {
case network.IsTCP():
return &net.TCPAddr{IP: ip, Port: port}
return &net.TCPAddr{IP: ip.AsSlice(), Port: port, Zone: ip.Zone()}
default:
return &net.UDPAddr{IP: ip, Port: port}
return &net.UDPAddr{IP: ip.AsSlice(), Port: port, Zone: ip.Zone()}
}
}

func addrEqual(a, b net.Addr) bool {
aIP, aPort, aType, aOk := parseAddr(a)
if !aOk {
aIP, aPort, aType, aErr := parseAddr(a)
if aErr != nil {
return false
}

bIP, bPort, bType, bOk := parseAddr(b)
if !bOk {
bIP, bPort, bType, bErr := parseAddr(b)
if bErr != nil {
return false
}

return aType == bType && aIP.Equal(bIP) && aPort == bPort
return aType == bType && aIP.Compare(bIP) == 0 && aPort == bPort
}

// AddrPort is an IP and a port number.
Expand Down
48 changes: 28 additions & 20 deletions agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,9 +228,22 @@ func NewAgent(config *AgentConfig) (*Agent, error) { //nolint:gocognit
}
}

localIfcs, _, err := localInterfaces(a.net, a.interfaceFilter, a.ipFilter, a.networkTypes, a.includeLoopback)
if err != nil {
return nil, fmt.Errorf("error getting local interfaces: %w", err)
}

// Opportunistic mDNS: If we can't open the connection, that's ok: we
// can continue without it.
if a.mDNSConn, a.mDNSMode, err = createMulticastDNS(a.net, mDNSMode, mDNSName, log); err != nil {
if a.mDNSConn, a.mDNSMode, err = createMulticastDNS(
a.net,
a.networkTypes,
localIfcs,
a.includeLoopback,
mDNSMode,
mDNSName,
log,
); err != nil {
log.Warnf("Failed to initialize mDNS %s: %v", mDNSName, err)
}

Expand Down Expand Up @@ -592,19 +605,14 @@ func (a *Agent) resolveAndAddMulticastCandidate(c *CandidateHost) {
if a.mDNSConn == nil {
return
}
_, src, err := a.mDNSConn.Query(c.context(), c.Address())

_, src, err := a.mDNSConn.QueryAddr(c.context(), c.Address())

Check failure on line 609 in agent.go

View workflow job for this annotation

GitHub Actions / lint / Go

a.mDNSConn.QueryAddr undefined (type *mdns.Conn has no field or method QueryAddr)) (typecheck)

Check failure on line 609 in agent.go

View workflow job for this annotation

GitHub Actions / lint / Go

a.mDNSConn.QueryAddr undefined (type *mdns.Conn has no field or method QueryAddr) (typecheck)

Check failure on line 609 in agent.go

View workflow job for this annotation

GitHub Actions / test (1.20) / Go 1.20

a.mDNSConn.QueryAddr undefined (type *mdns.Conn has no field or method QueryAddr)

Check failure on line 609 in agent.go

View workflow job for this annotation

GitHub Actions / test (1.21) / Go 1.21

a.mDNSConn.QueryAddr undefined (type *mdns.Conn has no field or method QueryAddr)
if err != nil {
a.log.Warnf("Failed to discover mDNS candidate %s: %v", c.Address(), err)
return
}

ip, ipOk := parseMulticastAnswerAddr(src)
if !ipOk {
a.log.Warnf("Failed to discover mDNS candidate %s: failed to parse IP", c.Address())
return
}

if err = c.setIP(ip); err != nil {
if err = c.setIPAddr(src); err != nil {
a.log.Warnf("Failed to discover mDNS candidate %s: %v", c.Address(), err)
return
}
Expand All @@ -626,7 +634,7 @@ func (a *Agent) requestConnectivityCheck() {
}

func (a *Agent) addRemotePassiveTCPCandidate(remoteCandidate Candidate) {
localIPs, err := localInterfaces(a.net, a.interfaceFilter, a.ipFilter, []NetworkType{remoteCandidate.NetworkType()}, a.includeLoopback)
_, localIPs, err := localInterfaces(a.net, a.interfaceFilter, a.ipFilter, []NetworkType{remoteCandidate.NetworkType()}, a.includeLoopback)
if err != nil {
a.log.Warnf("Failed to iterate local interfaces, host candidates will not be gathered %s", err)
return
Expand Down Expand Up @@ -841,9 +849,9 @@ func (a *Agent) deleteAllCandidates() {
}

func (a *Agent) findRemoteCandidate(networkType NetworkType, addr net.Addr) Candidate {
ip, port, _, ok := parseAddr(addr)
if !ok {
a.log.Warnf("Failed to parse address: %s", addr)
ip, port, _, err := parseAddr(addr)
if err != nil {
a.log.Warnf("Failed to parse address: %s; error: %s", addr, err)
return nil
}

Expand Down Expand Up @@ -873,15 +881,15 @@ func (a *Agent) sendBindingRequest(m *stun.Message, local, remote Candidate) {
func (a *Agent) sendBindingSuccess(m *stun.Message, local, remote Candidate) {
base := remote

ip, port, _, ok := parseAddr(base.addr())
if !ok {
a.log.Warnf("Failed to parse address: %s", base.addr())
ip, port, _, err := parseAddr(base.addr())
if err != nil {
a.log.Warnf("Failed to parse address: %s; error: %s", base.addr(), err)
return
}

if out, err := stun.Build(m, stun.BindingSuccess,
&stun.XORMappedAddress{
IP: ip,
IP: ip.AsSlice(),
Port: port,
},
stun.NewShortTermIntegrity(a.localPwd),
Expand Down Expand Up @@ -983,9 +991,9 @@ func (a *Agent) handleInbound(m *stun.Message, local Candidate, remote net.Addr)
}

if remoteCandidate == nil {
ip, port, networkType, ok := parseAddr(remote)
if !ok {
a.log.Errorf("Failed to create parse remote net.Addr when creating remote prflx candidate")
ip, port, networkType, err := parseAddr(remote)
if err != nil {
a.log.Errorf("Failed to create parse remote net.Addr when creating remote prflx candidate: %s", err)
return
}

Expand Down
1 change: 1 addition & 0 deletions agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,7 @@ func TestConnectionStateCallback(t *testing.T) {
DisconnectedTimeout: &disconnectedDuration,
FailedTimeout: &failedDuration,
KeepaliveInterval: &KeepaliveInterval,
InterfaceFilter: problematicNetworkInterfaces,
}

aAgent, err := NewAgent(cfg)
Expand Down
2 changes: 1 addition & 1 deletion candidate_base.go
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ func (c *candidateBase) Equal(other Candidate) bool {

// String makes the candidateBase printable
func (c *candidateBase) String() string {
return fmt.Sprintf("%s %s %s%s", c.NetworkType(), c.Type(), net.JoinHostPort(c.Address(), strconv.Itoa(c.Port())), c.relatedAddress)
return fmt.Sprintf("%s %s %s%s (%s)", c.NetworkType(), c.Type(), net.JoinHostPort(c.Address(), strconv.Itoa(c.Port())), c.relatedAddress, c.resolvedAddr)
}

// LastReceived returns a time.Time indicating the last time
Expand Down
Loading

0 comments on commit 7511f91

Please sign in to comment.