Files
ice/external_ip_mapper.go
Joe Turki 7ce2014eae Add AddressRewriter rules
A new API to replace the NAT 1:1
with clear replace/add, and interface, CIDR
level mapping
2025-12-08 18:03:45 +02:00

457 lines
11 KiB
Go

// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package ice
import (
"fmt"
"net"
"strings"
)
// AddressRewriteMode controls whether a rule replaces or appends candidates.
type AddressRewriteMode int
const (
addressRewriteModeUnspecified AddressRewriteMode = iota
AddressRewriteReplace
AddressRewriteAppend
)
// AddressRewriteRule represents a rule for remapping candidate addresses.
type AddressRewriteRule struct {
// External are the 1:1 external addresses to advertise for this rule.
// For replace mode, an empty list is treated as "drop the matched local
// address" (no candidate emitted). For append mode, an empty list is a
// no-op: the original candidate is kept.
// Empty External rules are intentional:
// - Mode AddressRewriteReplace drops the matched candidate (deny-list style).
// - Mode AddressRewriteAppend keeps the original candidate and adds nothing,
// which is useful when you combine a catch-all replace with per-interface
// allow rules.
External []string
// Local optionally pins this rule to a specific local address. When set,
// external IPs map to that address regardless of IP family. When empty,
// External acts as a catch-all for the family implied by the local scope
// (CIDR when set, otherwise the external IP family).
Local string
// Iface is the optional interface name to limit the rule to, empty = any.
Iface string
// CIDR is the optional CIDR to limit the rule to, empty = any.
CIDR string
// AsCandidateType is the candidate type to publish as for this rule. Defaults to host
// when unspecified. Supported values: host, server reflexive, relay.
AsCandidateType CandidateType
// Mode controls whether we replace the original candidate or append extra
// candidates.
//
// If Mode is zero, the default is:
// - CandidateTypeHost -> AddressRewriteReplace
// - CandidateTypeServerReflexive, CandidateTypeRelay -> AddressRewriteAppend
// For replace mode, a match with zero external IPs removes the candidate.
// For append mode, a match with zero external IPs leaves the original
// candidate untouched.
Mode AddressRewriteMode
// Networks is the optional networks to limit the rule to, nil/empty = all.
Networks []NetworkType
}
func validateIPString(ipStr string) (net.IP, bool, error) {
ip := net.ParseIP(ipStr)
if ip == nil {
return nil, false, ErrInvalidNAT1To1IPMapping
}
return ip, (ip.To4() != nil), nil
}
// ipMapping holds the mapping of local and external IP address
//
// for a particular IP family.
type ipMapping struct {
ipSole []net.IP // When non-empty, these are the catch-all external IPs for one local IP family
ipMap map[string][]net.IP // Local-to-external IP mapping (k: local, v: external IPs)
valid bool // If not set any external IP, valid is false
catchAllSet bool
}
func newIPMapping() ipMapping {
return ipMapping{
ipMap: make(map[string][]net.IP),
}
}
func (m *ipMapping) addSoleIP(ip net.IP) {
m.ipSole = append(m.ipSole, ip)
m.valid = true
m.catchAllSet = true
}
func addExternalMappings(
external []string,
ruleMapping *addressRewriteRuleMapping,
hasLocalAddr bool,
localAddr net.IP,
localIsIPv4 bool,
) (bool, error) {
added := false
for _, raw := range external {
extIPStr := strings.TrimSpace(raw)
ipPair := strings.Split(extIPStr, "/")
if len(ipPair) != 1 {
return false, ErrInvalidNAT1To1IPMapping
}
extIP, isExtIPv4, err := validateIPString(ipPair[0])
if err != nil {
return false, err
}
targetLocalIPv4 := isExtIPv4
if hasLocalAddr {
targetLocalIPv4 = localIsIPv4
} else if ruleMapping.cidr != nil {
targetLocalIPv4 = ruleMapping.cidr.IP.To4() != nil
}
if !ruleMapping.isFamilyAllowed(targetLocalIPv4) {
continue
}
ruleMapping.addImplicitMapping(extIP, targetLocalIPv4, hasLocalAddr, localAddr)
added = true
}
return added, nil
}
func maybeMarkEmptyMapping(
ruleMapping *addressRewriteRuleMapping,
added bool,
hasLocalAddr bool,
localIsIPv4 bool,
localAddr net.IP,
) {
if added {
return
}
if hasLocalAddr {
if ruleMapping.isFamilyAllowed(localIsIPv4) {
family := ruleMapping.mappingForFamily(localIsIPv4)
family.ipMap[localAddr.String()] = nil
family.valid = true
}
return
}
if ruleMapping.allowIPv4 {
ruleMapping.ipv4Mapping.valid = true
ruleMapping.ipv4Mapping.catchAllSet = true
}
if ruleMapping.allowIPv6 {
ruleMapping.ipv6Mapping.valid = true
ruleMapping.ipv6Mapping.catchAllSet = true
}
}
func (m *ipMapping) addIPMapping(locIP, extIP net.IP) {
locIPStr := locIP.String()
m.ipMap[locIPStr] = append(m.ipMap[locIPStr], extIP)
m.valid = true
}
func cloneIPs(src []net.IP) []net.IP {
if len(src) == 0 {
return nil
}
cloned := make([]net.IP, 0, len(src))
for _, ip := range src {
if ip == nil {
continue
}
copied := make(net.IP, len(ip))
copy(copied, ip)
cloned = append(cloned, copied)
}
return cloned
}
func (m *ipMapping) findExternalIPs(locIP net.IP) []net.IP {
if !m.valid {
return nil
}
if m.ipMap != nil {
if extIPs, ok := m.ipMap[locIP.String()]; ok && len(extIPs) > 0 {
return cloneIPs(extIPs)
}
}
if len(m.ipSole) > 0 {
return cloneIPs(m.ipSole)
}
return nil
}
type addressRewriteRuleMapping struct {
rule AddressRewriteRule
mode AddressRewriteMode
ipv4Mapping ipMapping
ipv6Mapping ipMapping
cidr *net.IPNet
allowIPv4 bool
allowIPv6 bool
}
func (m *addressRewriteRuleMapping) hasMappings() bool {
return m.ipv4Mapping.valid || m.ipv6Mapping.valid
}
func (m *addressRewriteRuleMapping) mappingForFamily(isIPv4 bool) *ipMapping {
if isIPv4 {
return &m.ipv4Mapping
}
return &m.ipv6Mapping
}
func (m *addressRewriteRuleMapping) isFamilyAllowed(isLocalIPv4 bool) bool {
if isLocalIPv4 {
return m.allowIPv4
}
return m.allowIPv6
}
func (m *addressRewriteRuleMapping) addImplicitMapping(
extIP net.IP,
isLocalIPv4 bool,
hasLocalAddr bool,
localAddr net.IP,
) {
mapping := m.mappingForFamily(isLocalIPv4)
if hasLocalAddr {
mapping.addIPMapping(localAddr, extIP)
} else {
mapping.addSoleIP(extIP)
}
}
type addressRewriteMapper struct {
rulesByCandidateType map[CandidateType][]*addressRewriteRuleMapping
}
//nolint:gocognit,gocyclo,cyclop
func newAddressRewriteMapper(rules []AddressRewriteRule) (*addressRewriteMapper, error) {
if len(rules) == 0 {
return nil, nil //nolint:nilnil
}
mapper := &addressRewriteMapper{
rulesByCandidateType: make(map[CandidateType][]*addressRewriteRuleMapping),
}
for _, rule := range rules {
candidateType := rule.AsCandidateType
if candidateType == CandidateTypeUnspecified {
candidateType = CandidateTypeHost
}
if candidateType == CandidateTypePeerReflexive {
return nil, ErrUnsupportedNAT1To1IPCandidateType
}
mode := rule.Mode
if mode == addressRewriteModeUnspecified {
mode = defaultAddressRewriteMode(candidateType)
}
ruleMapping := &addressRewriteRuleMapping{
rule: rule,
mode: mode,
ipv4Mapping: newIPMapping(),
ipv6Mapping: newIPMapping(),
allowIPv4: true,
allowIPv6: true,
}
if len(rule.Networks) > 0 {
ruleMapping.allowIPv4 = false
ruleMapping.allowIPv6 = false
for _, network := range rule.Networks {
if network.IsIPv4() {
ruleMapping.allowIPv4 = true
}
if network.IsIPv6() {
ruleMapping.allowIPv6 = true
}
}
if !ruleMapping.allowIPv4 && !ruleMapping.allowIPv6 {
continue
}
}
if rule.CIDR != "" {
_, ipNet, err := net.ParseCIDR(rule.CIDR)
if err != nil {
return nil, ErrInvalidNAT1To1IPMapping
}
ruleMapping.cidr = ipNet
}
var (
localAddr net.IP
localIsIPv4 bool
hasLocalAddr bool
err error
)
if trimmedLocal := strings.TrimSpace(rule.Local); trimmedLocal != "" {
localAddr, localIsIPv4, err = validateIPString(trimmedLocal)
if err != nil {
return nil, err
}
hasLocalAddr = true
if ruleMapping.cidr != nil && !ruleMapping.cidr.Contains(localAddr) {
return nil, fmt.Errorf("%w: Invalid local IP is outside CIDR", ErrInvalidNAT1To1IPMapping)
}
}
added, mapErr := addExternalMappings(rule.External, ruleMapping, hasLocalAddr, localAddr, localIsIPv4)
if mapErr != nil {
return nil, mapErr
}
maybeMarkEmptyMapping(ruleMapping, added, hasLocalAddr, localIsIPv4, localAddr)
if ruleMapping.hasMappings() {
mapper.rulesByCandidateType[candidateType] = append(mapper.rulesByCandidateType[candidateType], ruleMapping)
}
}
if len(mapper.rulesByCandidateType) == 0 {
return nil, nil //nolint:nilnil
}
return mapper, nil
}
func (m *addressRewriteMapper) hasCandidateType(candidateType CandidateType) bool {
rules := m.rulesByCandidateType[candidateType]
for _, rule := range rules {
if rule.hasMappings() {
return true
}
}
return false
}
func (m *addressRewriteMapper) shouldReplace(candidateType CandidateType) bool {
for _, rule := range m.rulesByCandidateType[candidateType] {
if rule.mode == AddressRewriteReplace {
return true
}
}
return false
}
func (m *addressRewriteMapper) findExternalIPs(
candidateType CandidateType,
localIPStr string,
iface string,
) ([]net.IP, bool, AddressRewriteMode, error) {
locIP, isLocIPv4, err := validateIPString(localIPStr)
if err != nil {
return nil, false, addressRewriteModeUnspecified, err
}
rules := m.rulesByCandidateType[candidateType]
ips, matched, mode := evaluateRewriteRules(rules, locIP, isLocIPv4, iface)
return ips, matched, mode, nil
}
func ruleMappingForLookup(
rule *addressRewriteRuleMapping,
locIP net.IP,
isLocIPv4 bool,
iface string,
) (*ipMapping, bool) {
if rule.rule.Iface != "" && rule.rule.Iface != iface {
return nil, false
}
if rule.cidr != nil && !rule.cidr.Contains(locIP) {
return nil, false
}
ipMapping := rule.mappingForFamily(isLocIPv4)
if !ipMapping.valid {
return nil, false
}
return ipMapping, true
}
func catchAllSpecificity(rule *addressRewriteRuleMapping, iface string) int {
spec := 0
if rule.rule.Iface != "" {
spec += 2
if rule.cidr != nil {
spec++
}
} else if iface == "" && rule.cidr != nil {
spec++
}
return spec
}
func evaluateRewriteRules(
rules []*addressRewriteRuleMapping,
locIP net.IP,
isLocIPv4 bool,
iface string,
) (ips []net.IP, matched bool, mode AddressRewriteMode) {
var (
catchAll []net.IP
catchAllMode AddressRewriteMode
hasCatchAll bool
bestSpec = -1
)
for _, rule := range rules {
ipMapping, ok := ruleMappingForLookup(rule, locIP, isLocIPv4, iface)
if !ok {
continue
}
if explicit, ok := ipMapping.ipMap[locIP.String()]; ok {
cloned := cloneIPs(explicit)
return cloned, true, rule.mode
}
if ipMapping.catchAllSet {
spec := catchAllSpecificity(rule, iface)
if !hasCatchAll || spec > bestSpec {
catchAll = cloneIPs(ipMapping.ipSole)
catchAllMode = rule.mode
hasCatchAll = true
bestSpec = spec
}
}
}
if hasCatchAll {
return catchAll, true, catchAllMode
}
return nil, false, addressRewriteModeUnspecified
}