mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-20 07:39:56 +00:00
[client] filter CGNAT and CNI addresses from ICE candidates
In Kubernetes environments using Cilium or similar CNI plugins, pod CIDR addresses (e.g. 100.65.x.x) from the RFC 6598 CGNAT range (100.64.0.0/10) were being gathered as valid ICE host candidates. This caused WireGuard endpoints to resolve to non-routable pod IPs, producing overlay-routed connections with degraded latency instead of true P2P paths between hosts. Add three layers of defense: - Expand the default interface blacklist with common Kubernetes CNI interface prefixes (cilium_, lxc, cali, flannel, cni, weave) - Filter local and remote ICE candidates whose addresses fall within the CGNAT range but outside the NetBird WireGuard network - Reject UDP mux writes to CGNAT addresses as a defense-in-depth fallback
This commit is contained in:
@@ -22,6 +22,11 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// cgnatPrefix is the RFC 6598 Carrier-Grade NAT range (100.64.0.0/10).
|
||||||
|
// Addresses in this range are used by CNI plugins (Cilium, Calico, etc.) for pod networking
|
||||||
|
// and are not suitable for direct peer-to-peer connectivity between hosts.
|
||||||
|
var cgnatPrefix = netip.MustParsePrefix("100.64.0.0/10")
|
||||||
|
|
||||||
// FilterFn is a function that filters out candidates based on the address.
|
// FilterFn is a function that filters out candidates based on the address.
|
||||||
// If it returns true, the address is to be filtered. It also returns the prefix of matching route.
|
// If it returns true, the address is to be filtered. It also returns the prefix of matching route.
|
||||||
type FilterFn func(address netip.Addr) (bool, netip.Prefix, error)
|
type FilterFn func(address netip.Addr) (bool, netip.Prefix, error)
|
||||||
@@ -175,6 +180,15 @@ func (u *UDPConn) performFilterCheck(addr net.Addr) error {
|
|||||||
return fmt.Errorf("address %s is part of the NetBird network %s, refusing to write", addr, u.address)
|
return fmt.Errorf("address %s is part of the NetBird network %s, refusing to write", addr, u.address)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter addresses in the RFC 6598 CGNAT range (100.64.0.0/10) that are not part of the
|
||||||
|
// NetBird WireGuard network. These addresses are commonly assigned by Kubernetes CNI plugins
|
||||||
|
// (Cilium, Calico, etc.) for pod networking and are not routable between hosts.
|
||||||
|
if cgnatPrefix.Contains(a) && !u.address.Network.Contains(a) {
|
||||||
|
u.addrCache.Store(addr.String(), true)
|
||||||
|
log.Infof("Address %s is in the CGNAT range (%s), likely a CNI pod address, refusing to write", addr, cgnatPrefix)
|
||||||
|
return fmt.Errorf("address %s is in the CGNAT range (%s), refusing to write", addr, cgnatPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
if isRouted, prefix, err := u.filterFn(a); err != nil {
|
if isRouted, prefix, err := u.filterFn(a); err != nil {
|
||||||
log.Errorf("Failed to check if address %s is routed: %v", addr, err)
|
log.Errorf("Failed to check if address %s is routed: %v", addr, err)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -165,6 +165,10 @@ func (w *WorkerICE) OnRemoteCandidate(candidate ice.Candidate, haRoutes route.HA
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if candidateInCGNAT(candidate, w.config.WgConfig.WgInterface.Address().Network) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := w.agent.AddRemoteCandidate(candidate); err != nil {
|
if err := w.agent.AddRemoteCandidate(candidate); err != nil {
|
||||||
w.log.Errorf("error while handling remote candidate")
|
w.log.Errorf("error while handling remote candidate")
|
||||||
return
|
return
|
||||||
@@ -362,6 +366,10 @@ func (w *WorkerICE) onICECandidate(candidate ice.Candidate) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if candidateInCGNAT(candidate, w.config.WgConfig.WgInterface.Address().Network) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: reported port is incorrect for CandidateTypeHost, makes understanding ICE use via logs confusing as port is ignored
|
// TODO: reported port is incorrect for CandidateTypeHost, makes understanding ICE use via logs confusing as port is ignored
|
||||||
w.log.Debugf("discovered local candidate %s", candidate.String())
|
w.log.Debugf("discovered local candidate %s", candidate.String())
|
||||||
go func() {
|
go func() {
|
||||||
@@ -496,6 +504,11 @@ func extraSrflxCandidate(candidate ice.Candidate) (*ice.CandidateServerReflexive
|
|||||||
return ec, nil
|
return ec, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cgnatPrefix is the RFC 6598 Carrier-Grade NAT range (100.64.0.0/10).
|
||||||
|
// Addresses in this range are used by CNI plugins (Cilium, Calico, etc.) for pod networking
|
||||||
|
// and are not suitable for direct peer-to-peer connectivity between hosts.
|
||||||
|
var cgnatPrefix = netip.MustParsePrefix("100.64.0.0/10")
|
||||||
|
|
||||||
func candidateViaRoutes(candidate ice.Candidate, clientRoutes route.HAMap) bool {
|
func candidateViaRoutes(candidate ice.Candidate, clientRoutes route.HAMap) bool {
|
||||||
addr, err := netip.ParseAddr(candidate.Address())
|
addr, err := netip.ParseAddr(candidate.Address())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -524,6 +537,32 @@ func candidateViaRoutes(candidate ice.Candidate, clientRoutes route.HAMap) bool
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// candidateInCGNAT checks if a candidate address falls within the RFC 6598 CGNAT range (100.64.0.0/10).
|
||||||
|
// These addresses are commonly used by Kubernetes CNI plugins (Cilium, Calico) for pod networking
|
||||||
|
// and are not routable between hosts, making them unsuitable as ICE candidates.
|
||||||
|
// The wgNetwork parameter is the NetBird WireGuard network prefix — if the candidate address is within
|
||||||
|
// this network, it is not filtered here (it's handled separately by the NetBird network check).
|
||||||
|
func candidateInCGNAT(candidate ice.Candidate, wgNetwork netip.Prefix) bool {
|
||||||
|
addr, err := netip.ParseAddr(candidate.Address())
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cgnatPrefix.Contains(addr) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't filter if the address is within the WireGuard network itself —
|
||||||
|
// that's handled by the NetBird network membership check elsewhere.
|
||||||
|
if wgNetwork.IsValid() && wgNetwork.Contains(addr) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("Ignoring candidate [%s], its address %s is in the CGNAT range (%s) likely assigned by a CNI plugin",
|
||||||
|
candidate.String(), addr, cgnatPrefix)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func isRelayCandidate(candidate ice.Candidate) bool {
|
func isRelayCandidate(candidate ice.Candidate) bool {
|
||||||
return candidate.Type() == ice.CandidateTypeRelay
|
return candidate.Type() == ice.CandidateTypeRelay
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ const (
|
|||||||
var DefaultInterfaceBlacklist = []string{
|
var DefaultInterfaceBlacklist = []string{
|
||||||
iface.WgInterfaceDefault, "wt", "utun", "tun0", "zt", "ZeroTier", "wg", "ts",
|
iface.WgInterfaceDefault, "wt", "utun", "tun0", "zt", "ZeroTier", "wg", "ts",
|
||||||
"Tailscale", "tailscale", "docker", "veth", "br-", "lo",
|
"Tailscale", "tailscale", "docker", "veth", "br-", "lo",
|
||||||
|
// Kubernetes CNI interfaces
|
||||||
|
"cilium_", "cilium", "lxc", "cali", "flannel", "cni", "weave",
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConfigInput carries configuration changes to the client
|
// ConfigInput carries configuration changes to the client
|
||||||
|
|||||||
Reference in New Issue
Block a user