Skip to content

Commit

Permalink
Support for a FQDN as ice host candidate override
Browse files Browse the repository at this point in the history
  • Loading branch information
streamer45 committed Aug 22, 2024
1 parent 1581e70 commit c2eea70
Show file tree
Hide file tree
Showing 8 changed files with 170 additions and 14 deletions.
5 changes: 5 additions & 0 deletions config/config.sample.toml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ ice_host_override = ""
# config to multiple pods in Kubernetes deployments. In that case, each pod should match against one
# local (node) IP and greatly simplify load balancing across multiple nodes.

# Whether or not to perform DNS resolution of the ice_host_override.
# When set to false (Experimental), the override is forwarded to the client unchanged.
# Note: This setting only takes effect if the ice_host_override is a FQDN (i.e. not an IP address).
ice_host_override_resolution = true

# A list of ICE servers (STUN/TURN) to be used by the service. It supports
# advanced configurations.
# Example
Expand Down
1 change: 1 addition & 0 deletions service/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ func (c *Config) SetDefaults() {
c.RTC.ICEPortUDP = 8443
c.RTC.ICEPortTCP = 8443
c.RTC.TURNConfig.CredentialsExpirationMinutes = 1440
c.RTC.ICEHostOverrideResolution = true
c.Store.DataSource = "/tmp/rtcd_db"
c.Logger.EnableConsole = true
c.Logger.ConsoleJSON = false
Expand Down
3 changes: 3 additions & 0 deletions service/rtc/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ type ServerConfig struct {
TURNConfig TURNConfig `toml:"turn"`
// EnableIPv6 specifies whether or not IPv6 should be used.
EnableIPv6 bool `toml:"enable_ipv6"`
// ICEHostOverrideResolution controls whether or not the ICEHostOverride should
// be resolved by the server before forwarding it to the client.
ICEHostOverrideResolution bool `toml:"ice_host_override_resolution"`
}

func (c ServerConfig) IsValid() error {
Expand Down
43 changes: 42 additions & 1 deletion service/rtc/msg.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,51 @@ func newMessage(s *session, msgType MessageType, data []byte) Message {
}
}

func marshalHostCandidate(c *webrtc.ICECandidate) webrtc.ICECandidateInit {
val := c.Foundation
if val == " " {
val = ""
}

val = fmt.Sprintf("%s %d %s %d %s %d typ %s",
val,
c.Component,
c.Protocol,
c.Priority,
c.Address,
c.Port,
c.Typ)

if c.TCPType != "" {
val += fmt.Sprintf(" tcptype %s", c.TCPType)
}

if c.RelatedAddress != "" && c.RelatedPort != 0 {
val = fmt.Sprintf("%s raddr %s rport %d",
val,
c.RelatedAddress,
c.RelatedPort)
}

return webrtc.ICECandidateInit{
Candidate: fmt.Sprintf("candidate:%s", val),
SDPMid: new(string),
SDPMLineIndex: new(uint16),
}
}

func newICEMessage(s *session, c *webrtc.ICECandidate) (Message, error) {
data := make(map[string]interface{})
data["type"] = "candidate"
data["candidate"] = c.ToJSON()

if c.Typ == webrtc.ICECandidateTypeHost && !isIPAddress(c.Address) {
// If the address is not an IP, we assume it's a hostname (FQDN)
// and pass it through as such.
data["candidate"] = marshalHostCandidate(c)
} else {
data["candidate"] = c.ToJSON()
}

js, err := json.Marshal(data)
if err != nil {
return Message{}, err
Expand Down
67 changes: 67 additions & 0 deletions service/rtc/msg_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright (c) 2022-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

package rtc

import (
"testing"

"github.com/pion/webrtc/v3"
"github.com/stretchr/testify/require"
)

func TestNewICEMessage(t *testing.T) {
t.Run("host candidate - ip address", func(t *testing.T) {
msg, err := newICEMessage(&session{
cfg: SessionConfig{
SessionID: "sessionID",
UserID: "userID",
CallID: "callID",
GroupID: "groupID",
},
}, &webrtc.ICECandidate{
Address: "1.1.1.1",
Port: 8443,
Priority: 45,
Typ: webrtc.ICECandidateTypeHost,
Protocol: webrtc.ICEProtocolUDP,
Foundation: "2145320272",
})
require.NoError(t, err)
require.Equal(t, Message{
SessionID: "sessionID",
UserID: "userID",
CallID: "callID",
GroupID: "groupID",
Type: ICEMessage,
Data: []byte(`{"candidate":{"candidate":"candidate:2145320272 0 udp 45 1.1.1.1 8443 typ host","sdpMid":"","sdpMLineIndex":0,"usernameFragment":null},"type":"candidate"}`),
}, msg)
})

t.Run("host candidate - fqdn", func(t *testing.T) {
msg, err := newICEMessage(&session{
cfg: SessionConfig{
SessionID: "sessionID",
UserID: "userID",
CallID: "callID",
GroupID: "groupID",
},
}, &webrtc.ICECandidate{
Address: "example.tld",
Port: 8443,
Priority: 45,
Typ: webrtc.ICECandidateTypeHost,
Protocol: webrtc.ICEProtocolUDP,
Foundation: "2145320272",
})
require.NoError(t, err)
require.Equal(t, Message{
SessionID: "sessionID",
UserID: "userID",
CallID: "callID",
GroupID: "groupID",
Type: ICEMessage,
Data: []byte(`{"candidate":{"candidate":"candidate:2145320272 0 udp 45 example.tld 8443 typ host","sdpMid":"","sdpMLineIndex":0,"usernameFragment":null},"type":"candidate"}`),
}, msg)
})
}
8 changes: 7 additions & 1 deletion service/rtc/sfu.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ func (s *Server) initSettingEngine() (webrtc.SettingEngine, error) {
sEngine.SetDTLSInsecureSkipHelloVerify(true)
}

pairs, err := generateAddrsPairs(s.localIPs, s.publicAddrsMap, s.cfg.ICEHostOverride, s.cfg.EnableIPv6)
pairs, err := generateAddrsPairs(s.localIPs, s.publicAddrsMap, s.cfg.ICEHostOverride,
s.cfg.EnableIPv6, s.cfg.ICEHostOverrideResolution)
if err != nil {
return webrtc.SettingEngine{}, fmt.Errorf("failed to generate addresses pairs: %w", err)
} else if len(pairs) > 0 {
Expand Down Expand Up @@ -278,6 +279,11 @@ func (s *Server) InitSession(cfg SessionConfig, closeCb func() error) error {
}
}

// If the ICE host override is a FQDN and resolution is off, we pass it through to the client unchanged.
if candidate.Typ == webrtc.ICECandidateTypeHost && s.cfg.ICEHostOverride != "" && !isIPAddress(s.cfg.ICEHostOverride) && !s.cfg.ICEHostOverrideResolution {
candidate.Address = s.cfg.ICEHostOverride
}

msg, err := newICEMessage(us, candidate)
if err != nil {
s.log.Error("failed to create ICE message", mlog.Err(err), mlog.String("sessionID", cfg.SessionID))
Expand Down
15 changes: 12 additions & 3 deletions service/rtc/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func getTrackType(kind webrtc.RTPCodecType) string {
return "unknown"
}

func generateAddrsPairs(localIPs []netip.Addr, publicAddrsMap map[netip.Addr]string, hostOverride string, dualStack bool) ([]string, error) {
func generateAddrsPairs(localIPs []netip.Addr, publicAddrsMap map[netip.Addr]string, hostOverride string, dualStack bool, resolveOverride bool) ([]string, error) {
var err error
var pairs []string
var hostOverrideIP string
Expand All @@ -74,12 +74,14 @@ func generateAddrsPairs(localIPs []netip.Addr, publicAddrsMap map[netip.Addr]str
ipNetwork = "ip"
}

// If the override is set we resolve it in case it's a hostname.
if hostOverride != "" {
// If the override is a hostname and server-side resolving is on, we try to resolve it.
if hostOverride != "" && !isIPAddress(hostOverride) && resolveOverride {
hostOverrideIP, err = resolveHost(hostOverride, ipNetwork, time.Second)
if err != nil {
return pairs, fmt.Errorf("failed to resolve host: %w", err)
}
} else if isIPAddress(hostOverride) {
hostOverrideIP = hostOverride
}

// Nothing to do at this point if no local IP was found.
Expand Down Expand Up @@ -172,3 +174,10 @@ func pickRandom[S ~[]*E, E any](s S) *E {
}
return s[rand.Intn(len(s))]
}

func isIPAddress(addr string) bool {
if _, err := netip.ParseAddr(addr); err == nil {
return true
}
return false
}
42 changes: 33 additions & 9 deletions service/rtc/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ import (

func TestGenerateAddrsPairs(t *testing.T) {
t.Run("nil/empty inputs", func(t *testing.T) {
pairs, err := generateAddrsPairs(nil, nil, "", false)
pairs, err := generateAddrsPairs(nil, nil, "", false, false)
require.NoError(t, err)
require.Empty(t, pairs)

pairs, err = generateAddrsPairs([]netip.Addr{}, map[netip.Addr]string{}, "", false)
pairs, err = generateAddrsPairs([]netip.Addr{}, map[netip.Addr]string{}, "", false, false)
require.NoError(t, err)
require.Empty(t, pairs)
})
Expand All @@ -28,7 +28,7 @@ func TestGenerateAddrsPairs(t *testing.T) {
}, map[netip.Addr]string{
netip.MustParseAddr("127.0.0.1"): "",
netip.MustParseAddr("10.1.1.1"): "",
}, "", false)
}, "", false, false)
require.NoError(t, err)
require.Equal(t, []string{"127.0.0.1/127.0.0.1", "10.1.1.1/10.1.1.1"}, pairs)
})
Expand All @@ -37,7 +37,7 @@ func TestGenerateAddrsPairs(t *testing.T) {
pairs, err := generateAddrsPairs([]netip.Addr{
netip.MustParseAddr("127.0.0.1"),
netip.MustParseAddr("10.1.1.1"),
}, map[netip.Addr]string{}, "1.1.1.1/127.0.0.1,1.1.1.1/10.1.1.1", false)
}, map[netip.Addr]string{}, "1.1.1.1/127.0.0.1,1.1.1.1/10.1.1.1", false, false)
require.NoError(t, err)
require.Equal(t, []string{"1.1.1.1/127.0.0.1", "1.1.1.1/10.1.1.1"}, pairs)
})
Expand All @@ -49,7 +49,7 @@ func TestGenerateAddrsPairs(t *testing.T) {
}, map[netip.Addr]string{
netip.MustParseAddr("127.0.0.1"): "",
netip.MustParseAddr("10.1.1.1"): "",
}, "1.1.1.1", false)
}, "1.1.1.1", false, false)
require.NoError(t, err)
require.Equal(t, []string{"127.0.0.1/127.0.0.1", "1.1.1.1/10.1.1.1"}, pairs)
})
Expand All @@ -61,7 +61,7 @@ func TestGenerateAddrsPairs(t *testing.T) {
}, map[netip.Addr]string{
netip.MustParseAddr("127.0.0.1"): "",
netip.MustParseAddr("10.1.1.1"): "1.1.1.1",
}, "", false)
}, "", false, false)
require.NoError(t, err)
require.Equal(t, []string{"127.0.0.1/127.0.0.1", "1.1.1.1/10.1.1.1"}, pairs)
})
Expand All @@ -73,7 +73,7 @@ func TestGenerateAddrsPairs(t *testing.T) {
}, map[netip.Addr]string{
netip.MustParseAddr("127.0.0.1"): "",
netip.MustParseAddr("10.1.1.1"): "1.1.1.1",
}, "", false)
}, "", false, false)
require.NoError(t, err)
require.Equal(t, []string{"127.0.0.1/127.0.0.1", "1.1.1.1/10.1.1.1"}, pairs)
})
Expand All @@ -85,7 +85,7 @@ func TestGenerateAddrsPairs(t *testing.T) {
}, map[netip.Addr]string{
netip.MustParseAddr("127.0.0.1"): "1.1.1.1",
netip.MustParseAddr("10.1.1.1"): "1.1.1.2",
}, "", false)
}, "", false, false)
require.NoError(t, err)
require.Equal(t, []string{"1.1.1.1/127.0.0.1", "1.1.1.2/10.1.1.1"}, pairs)
})
Expand All @@ -99,10 +99,34 @@ func TestGenerateAddrsPairs(t *testing.T) {
}, map[netip.Addr]string{
netip.MustParseAddr("127.0.0.1"): "1.1.1.1",
netip.MustParseAddr("10.1.1.1"): "1.1.1.2",
}, "8.8.8.8", false)
}, "8.8.8.8", false, false)
require.NoError(t, err)
require.Equal(t, []string{"127.0.0.1/127.0.0.1", "8.8.8.8/10.1.1.1"}, pairs)
})

t.Run("ice host override is a FQDN, resolve on", func(t *testing.T) {
pairs, err := generateAddrsPairs([]netip.Addr{
netip.MustParseAddr("127.0.0.1"),
netip.MustParseAddr("10.1.1.1"),
}, map[netip.Addr]string{
netip.MustParseAddr("127.0.0.1"): "",
netip.MustParseAddr("10.1.1.1"): "",
}, "localhost", false, true)
require.NoError(t, err)
require.Equal(t, []string{"127.0.0.1/127.0.0.1", "127.0.0.1/10.1.1.1"}, pairs)
})

t.Run("ice host override is a FQDN, resolve off", func(t *testing.T) {
pairs, err := generateAddrsPairs([]netip.Addr{
netip.MustParseAddr("127.0.0.1"),
netip.MustParseAddr("10.1.1.1"),
}, map[netip.Addr]string{
netip.MustParseAddr("127.0.0.1"): "",
netip.MustParseAddr("10.1.1.1"): "",
}, "localhost", false, false)
require.NoError(t, err)
require.Equal(t, []string{"127.0.0.1/127.0.0.1", "10.1.1.1/10.1.1.1"}, pairs)
})
}

func TestIsValidTrackID(t *testing.T) {
Expand Down

0 comments on commit c2eea70

Please sign in to comment.