mirror of
https://github.com/pion/ice.git
synced 2025-12-24 13:38:03 +08:00
A new API to replace the NAT 1:1 with clear replace/add, and interface, CIDR level mapping
457 lines
11 KiB
Go
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
|
|
}
|