mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-20 17:26:40 +00:00
Add transparent proxy inspection engine with envoy sidecar support
This commit is contained in:
@@ -101,6 +101,7 @@ type Account struct {
|
||||
NameServerGroupsG []nbdns.NameServerGroup `json:"-" gorm:"foreignKey:AccountID;references:id"`
|
||||
DNSSettings DNSSettings `gorm:"embedded;embeddedPrefix:dns_settings_"`
|
||||
PostureChecks []*posture.Checks `gorm:"foreignKey:AccountID;references:id"`
|
||||
InspectionPolicies []*InspectionPolicy `gorm:"foreignKey:AccountID;references:id"`
|
||||
Services []*service.Service `gorm:"foreignKey:AccountID;references:id"`
|
||||
Domains []*proxydomain.Domain `gorm:"foreignKey:AccountID;references:id"`
|
||||
// Settings is a dictionary of Account settings
|
||||
|
||||
@@ -81,6 +81,12 @@ func (a *Account) GetPeerNetworkMapComponents(
|
||||
return nil
|
||||
}
|
||||
|
||||
// Build inspection policies map for the network map builder
|
||||
inspectionPoliciesMap := make(map[string]*InspectionPolicy, len(a.InspectionPolicies))
|
||||
for _, ip := range a.InspectionPolicies {
|
||||
inspectionPoliciesMap[ip.ID] = ip
|
||||
}
|
||||
|
||||
components := &NetworkMapComponents{
|
||||
PeerID: peerID,
|
||||
Network: a.Network.Copy(),
|
||||
@@ -91,6 +97,7 @@ func (a *Account) GetPeerNetworkMapComponents(
|
||||
NetworkResources: make([]*resourceTypes.NetworkResource, 0),
|
||||
PostureFailedPeers: make(map[string]map[string]struct{}, len(a.PostureChecks)),
|
||||
RouterPeers: make(map[string]*nbpeer.Peer),
|
||||
InspectionPolicies: inspectionPoliciesMap,
|
||||
}
|
||||
|
||||
components.AccountSettings = &AccountSettingsInfo{
|
||||
|
||||
158
management/server/types/inspection_policy.go
Normal file
158
management/server/types/inspection_policy.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/xid"
|
||||
|
||||
"github.com/netbirdio/netbird/util/crypt"
|
||||
)
|
||||
|
||||
// InspectionPolicy is a reusable set of L7 inspection rules with proxy configuration.
|
||||
// Referenced by policies via InspectionPolicies field, similar to posture checks.
|
||||
// Contains both what to inspect (rules) and how to inspect (CA, ICAP, mode).
|
||||
type InspectionPolicy struct {
|
||||
ID string `gorm:"primaryKey"`
|
||||
AccountID string `gorm:"index"`
|
||||
Name string
|
||||
Description string
|
||||
Enabled bool
|
||||
Rules []InspectionPolicyRule `gorm:"serializer:json"`
|
||||
|
||||
// Mode is the proxy operation mode: "builtin", "envoy", or "external".
|
||||
Mode string `json:"mode"`
|
||||
// ExternalURL is the upstream proxy URL (HTTP CONNECT or SOCKS5) for external mode.
|
||||
ExternalURL string `json:"external_url"`
|
||||
// DefaultAction applies when no rule matches: "allow", "block", or "inspect".
|
||||
DefaultAction string `json:"default_action"`
|
||||
|
||||
// Redirect ports: which destination ports to intercept at L4.
|
||||
// Empty means all ports.
|
||||
RedirectPorts []int `gorm:"serializer:json" json:"redirect_ports"`
|
||||
|
||||
// MITM CA certificate and key (PEM-encoded)
|
||||
CACertPEM string `json:"ca_cert_pem"`
|
||||
CAKeyPEM string `json:"ca_key_pem"`
|
||||
|
||||
// ICAP configuration for external content scanning
|
||||
ICAP *InspectionICAPConfig `gorm:"serializer:json" json:"icap"`
|
||||
|
||||
// Envoy sidecar configuration (mode "envoy" only)
|
||||
EnvoyBinaryPath string `json:"envoy_binary_path"`
|
||||
EnvoyAdminPort int `json:"envoy_admin_port"`
|
||||
EnvoySnippets *InspectionEnvoySnippets `gorm:"serializer:json" json:"envoy_snippets"`
|
||||
}
|
||||
|
||||
// InspectionEnvoySnippets holds user-provided YAML fragments for envoy config customization.
|
||||
// Only safe snippet types are exposed: filters and clusters. Listeners and bootstrap
|
||||
// overrides are not allowed since the envoy instance is fully managed.
|
||||
type InspectionEnvoySnippets struct {
|
||||
HTTPFilters string `json:"http_filters"`
|
||||
NetworkFilters string `json:"network_filters"`
|
||||
Clusters string `json:"clusters"`
|
||||
}
|
||||
|
||||
// InspectionICAPConfig holds ICAP protocol settings.
|
||||
type InspectionICAPConfig struct {
|
||||
ReqModURL string `json:"reqmod_url"`
|
||||
RespModURL string `json:"respmod_url"`
|
||||
MaxConnections int `json:"max_connections"`
|
||||
}
|
||||
|
||||
// InspectionPolicyRule is an L7 rule within an inspection policy.
|
||||
// No source or network references: sources come from the referencing policy,
|
||||
// the destination network/routing peer is derived from the policy's destination.
|
||||
type InspectionPolicyRule struct {
|
||||
Domains []string `json:"domains"`
|
||||
// Networks restricts this rule to specific destination CIDRs.
|
||||
Networks []string `json:"networks"`
|
||||
// Protocols this rule applies to: "http", "https", "h2", "h3", "websocket", "other".
|
||||
Protocols []string `json:"protocols"`
|
||||
// Paths are URL path patterns: "/api/", "/login", "/admin/*".
|
||||
Paths []string `json:"paths"`
|
||||
Action string `json:"action"`
|
||||
Priority int `json:"priority"`
|
||||
}
|
||||
|
||||
// NewInspectionPolicy creates a new InspectionPolicy with a generated ID.
|
||||
func NewInspectionPolicy(accountID, name, description string, enabled bool) *InspectionPolicy {
|
||||
return &InspectionPolicy{
|
||||
ID: xid.New().String(),
|
||||
AccountID: accountID,
|
||||
Name: name,
|
||||
Description: description,
|
||||
Enabled: enabled,
|
||||
}
|
||||
}
|
||||
|
||||
// Copy returns a deep copy.
|
||||
func (p *InspectionPolicy) Copy() *InspectionPolicy {
|
||||
c := *p
|
||||
c.Rules = make([]InspectionPolicyRule, len(p.Rules))
|
||||
for i, r := range p.Rules {
|
||||
c.Rules[i] = r
|
||||
c.Rules[i].Domains = append([]string{}, r.Domains...)
|
||||
c.Rules[i].Networks = append([]string{}, r.Networks...)
|
||||
c.Rules[i].Protocols = append([]string{}, r.Protocols...)
|
||||
}
|
||||
c.RedirectPorts = append([]int{}, p.RedirectPorts...)
|
||||
if p.ICAP != nil {
|
||||
icap := *p.ICAP
|
||||
c.ICAP = &icap
|
||||
}
|
||||
return &c
|
||||
}
|
||||
|
||||
// EncryptSensitiveData encrypts CA cert and key in place.
|
||||
func (p *InspectionPolicy) EncryptSensitiveData(enc *crypt.FieldEncrypt) error {
|
||||
if enc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
if p.CACertPEM != "" {
|
||||
p.CACertPEM, err = enc.Encrypt(p.CACertPEM)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt ca_cert_pem: %w", err)
|
||||
}
|
||||
}
|
||||
if p.CAKeyPEM != "" {
|
||||
p.CAKeyPEM, err = enc.Encrypt(p.CAKeyPEM)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt ca_key_pem: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DecryptSensitiveData decrypts CA cert and key in place.
|
||||
func (p *InspectionPolicy) DecryptSensitiveData(enc *crypt.FieldEncrypt) error {
|
||||
if enc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
if p.CACertPEM != "" {
|
||||
p.CACertPEM, err = enc.Decrypt(p.CACertPEM)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decrypt ca_cert_pem: %w", err)
|
||||
}
|
||||
}
|
||||
if p.CAKeyPEM != "" {
|
||||
p.CAKeyPEM, err = enc.Decrypt(p.CAKeyPEM)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decrypt ca_key_pem: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HasDomainOnly returns true if this rule matches by domain and has no CIDR destinations.
|
||||
func (r *InspectionPolicyRule) HasDomainOnly() bool {
|
||||
return len(r.Domains) > 0 && len(r.Networks) == 0
|
||||
}
|
||||
|
||||
// HasCIDRDestination returns true if this rule specifies destination CIDRs.
|
||||
func (r *InspectionPolicyRule) HasCIDRDestination() bool {
|
||||
return len(r.Networks) > 0
|
||||
}
|
||||
@@ -37,9 +37,10 @@ type NetworkMap struct {
|
||||
OfflinePeers []*nbpeer.Peer
|
||||
FirewallRules []*FirewallRule
|
||||
RoutesFirewallRules []*RouteFirewallRule
|
||||
ForwardingRules []*ForwardingRule
|
||||
AuthorizedUsers map[string]map[string]struct{}
|
||||
EnableSSH bool
|
||||
ForwardingRules []*ForwardingRule
|
||||
TransparentProxyConfig *TransparentProxyConfig
|
||||
AuthorizedUsers map[string]map[string]struct{}
|
||||
EnableSSH bool
|
||||
}
|
||||
|
||||
func (nm *NetworkMap) Merge(other *NetworkMap) {
|
||||
|
||||
@@ -45,6 +45,13 @@ type NetworkMapComponents struct {
|
||||
PostureFailedPeers map[string]map[string]struct{}
|
||||
|
||||
RouterPeers map[string]*nbpeer.Peer
|
||||
|
||||
// TransparentProxyConfig is the account-level transparent proxy configuration.
|
||||
// Nil if no proxy is configured at account level.
|
||||
TransparentProxyConfig *TransparentProxyConfig
|
||||
|
||||
// InspectionPolicies are reusable inspection rule sets referenced by policies.
|
||||
InspectionPolicies map[string]*InspectionPolicy
|
||||
}
|
||||
|
||||
type AccountSettingsInfo struct {
|
||||
@@ -155,16 +162,21 @@ func (c *NetworkMapComponents) Calculate(ctx context.Context) *NetworkMap {
|
||||
dnsUpdate.NameServerGroups = c.getPeerNSGroupsFromGroups(targetPeerID, peerGroups)
|
||||
}
|
||||
|
||||
// Build transparent proxy config if this peer is a routing peer with inspection enabled.
|
||||
// Falls back to the account-level config if set.
|
||||
tpConfig := c.getTransparentProxyConfig(targetPeerID, isRouter)
|
||||
|
||||
return &NetworkMap{
|
||||
Peers: peersToConnectIncludingRouters,
|
||||
Network: c.Network.Copy(),
|
||||
Routes: append(networkResourcesRoutes, routesUpdate...),
|
||||
DNSConfig: dnsUpdate,
|
||||
OfflinePeers: expiredPeers,
|
||||
FirewallRules: firewallRules,
|
||||
RoutesFirewallRules: append(networkResourcesFirewallRules, routesFirewallRules...),
|
||||
AuthorizedUsers: authorizedUsers,
|
||||
EnableSSH: sshEnabled,
|
||||
Peers: peersToConnectIncludingRouters,
|
||||
Network: c.Network.Copy(),
|
||||
Routes: append(networkResourcesRoutes, routesUpdate...),
|
||||
DNSConfig: dnsUpdate,
|
||||
OfflinePeers: expiredPeers,
|
||||
FirewallRules: firewallRules,
|
||||
RoutesFirewallRules: append(networkResourcesFirewallRules, routesFirewallRules...),
|
||||
TransparentProxyConfig: tpConfig,
|
||||
AuthorizedUsers: authorizedUsers,
|
||||
EnableSSH: sshEnabled,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,7 +538,6 @@ func (c *NetworkMapComponents) getRoutingPeerRoutes(peerID string) (enabledRoute
|
||||
return enabledRoutes, disabledRoutes
|
||||
}
|
||||
|
||||
|
||||
func (c *NetworkMapComponents) filterRoutesByGroups(routes []*route.Route, groupListMap LookupMap) []*route.Route {
|
||||
var filteredRoutes []*route.Route
|
||||
for _, r := range routes {
|
||||
@@ -899,3 +910,198 @@ func (c *NetworkMapComponents) addNetworksRoutingPeers(
|
||||
|
||||
return peersToConnect
|
||||
}
|
||||
|
||||
// getTransparentProxyConfig builds a TransparentProxyConfig for a routing peer
|
||||
// by checking if any ACL policy targeting its networks has inspection policies attached.
|
||||
func (c *NetworkMapComponents) getTransparentProxyConfig(peerID string, isRouter bool) *TransparentProxyConfig {
|
||||
if c.TransparentProxyConfig != nil {
|
||||
return c.TransparentProxyConfig
|
||||
}
|
||||
|
||||
if !isRouter {
|
||||
return nil
|
||||
}
|
||||
|
||||
var networkIDs []string
|
||||
for networkID, routers := range c.RoutersMap {
|
||||
if _, ok := routers[peerID]; ok {
|
||||
networkIDs = append(networkIDs, networkID)
|
||||
}
|
||||
}
|
||||
|
||||
if len(networkIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.buildTransparentProxyFromPolicies(networkIDs, peerID)
|
||||
}
|
||||
|
||||
// buildTransparentProxyFromPolicies builds a TransparentProxyConfig from inspection
|
||||
// policies attached to ACL policies targeting the given networks.
|
||||
// Proxy infra config (CA, ICAP, mode) comes from the first inspection policy found.
|
||||
// Rules are aggregated from all inspection policies.
|
||||
func (c *NetworkMapComponents) buildTransparentProxyFromPolicies(networkIDs []string, peerID string) *TransparentProxyConfig {
|
||||
var config *TransparentProxyConfig
|
||||
|
||||
// Accumulate redirect sources across all networks.
|
||||
allSources := make(map[string]struct{})
|
||||
|
||||
for _, networkID := range networkIDs {
|
||||
networkPolicies := c.getPoliciesForNetwork(networkID)
|
||||
for _, policy := range networkPolicies {
|
||||
for _, ipID := range policy.InspectionPolicies {
|
||||
ip, ok := c.InspectionPolicies[ipID]
|
||||
if !ok || !ip.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
// First inspection policy sets the infra config
|
||||
if config == nil {
|
||||
config = &TransparentProxyConfig{
|
||||
Enabled: true,
|
||||
ExternalURL: ip.ExternalURL,
|
||||
DefaultAction: toTransparentProxyAction(ip.DefaultAction),
|
||||
CACertPEM: []byte(ip.CACertPEM),
|
||||
CAKeyPEM: []byte(ip.CAKeyPEM),
|
||||
}
|
||||
switch ip.Mode {
|
||||
case "envoy":
|
||||
config.Mode = TransparentProxyModeEnvoy
|
||||
case "external":
|
||||
config.Mode = TransparentProxyModeExternal
|
||||
default:
|
||||
config.Mode = TransparentProxyModeBuiltin
|
||||
}
|
||||
for _, p := range ip.RedirectPorts {
|
||||
config.RedirectPorts = append(config.RedirectPorts, uint16(p))
|
||||
}
|
||||
if ip.ICAP != nil {
|
||||
config.ICAP = &TransparentProxyICAPConfig{
|
||||
ReqModURL: ip.ICAP.ReqModURL,
|
||||
RespModURL: ip.ICAP.RespModURL,
|
||||
MaxConnections: ip.ICAP.MaxConnections,
|
||||
}
|
||||
}
|
||||
if ip.Mode == "envoy" {
|
||||
config.EnvoyBinaryPath = ip.EnvoyBinaryPath
|
||||
config.EnvoyAdminPort = uint16(ip.EnvoyAdminPort)
|
||||
if ip.EnvoySnippets != nil {
|
||||
config.EnvoySnippets = &TransparentProxyEnvoySnippets{
|
||||
HTTPFilters: ip.EnvoySnippets.HTTPFilters,
|
||||
NetworkFilters: ip.EnvoySnippets.NetworkFilters,
|
||||
Clusters: ip.EnvoySnippets.Clusters,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Aggregate rules from all inspection policies
|
||||
for _, pr := range ip.Rules {
|
||||
rule := TransparentProxyRule{
|
||||
ID: ip.ID,
|
||||
Domains: pr.Domains,
|
||||
Networks: pr.Networks,
|
||||
Protocols: pr.Protocols,
|
||||
Paths: pr.Paths,
|
||||
Action: toTransparentProxyAction(pr.Action),
|
||||
Priority: pr.Priority,
|
||||
}
|
||||
config.Rules = append(config.Rules, rule)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect sources for this network
|
||||
for _, src := range c.deriveRedirectSourcesFromPolicies(networkID, peerID) {
|
||||
allSources[src] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
if config != nil {
|
||||
config.RedirectSources = make([]string, 0, len(allSources))
|
||||
for src := range allSources {
|
||||
config.RedirectSources = append(config.RedirectSources, src)
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// deriveRedirectSourcesFromPolicies collects source peer IPs from policies
|
||||
// that target the given network and have inspection policies attached.
|
||||
func (c *NetworkMapComponents) deriveRedirectSourcesFromPolicies(networkID, routingPeerID string) []string {
|
||||
sourceSet := make(map[string]struct{})
|
||||
|
||||
for _, policy := range c.getPoliciesForNetwork(networkID) {
|
||||
if len(policy.InspectionPolicies) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
peerIDs := c.getUniquePeerIDsFromGroupsIDs(policy.SourceGroups())
|
||||
for _, peerID := range peerIDs {
|
||||
if peerID == routingPeerID {
|
||||
continue
|
||||
}
|
||||
peer := c.GetPeerInfo(peerID)
|
||||
if peer != nil && peer.IP != nil {
|
||||
sourceSet[peer.IP.String()+"/32"] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sources := make([]string, 0, len(sourceSet))
|
||||
for s := range sourceSet {
|
||||
sources = append(sources, s)
|
||||
}
|
||||
return sources
|
||||
}
|
||||
|
||||
// getPoliciesForNetwork returns all unique policies that have inspection policies attached
|
||||
// and target resources belonging to the given network.
|
||||
func (c *NetworkMapComponents) getPoliciesForNetwork(networkID string) []*Policy {
|
||||
seen := make(map[string]bool)
|
||||
var result []*Policy
|
||||
|
||||
add := func(policy *Policy) {
|
||||
if len(policy.InspectionPolicies) == 0 || seen[policy.ID] {
|
||||
return
|
||||
}
|
||||
seen[policy.ID] = true
|
||||
result = append(result, policy)
|
||||
}
|
||||
|
||||
// Only include policies that target resources in the given network.
|
||||
networkResourceIDs := make(map[string]struct{})
|
||||
for _, resource := range c.NetworkResources {
|
||||
if resource.NetworkID == networkID {
|
||||
networkResourceIDs[resource.ID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
for resourceID, policies := range c.ResourcePoliciesMap {
|
||||
if _, ok := networkResourceIDs[resourceID]; !ok {
|
||||
continue
|
||||
}
|
||||
for _, policy := range policies {
|
||||
add(policy)
|
||||
}
|
||||
}
|
||||
|
||||
// Also check classic policies whose destination groups contain peers in this network.
|
||||
for _, policy := range c.Policies {
|
||||
add(policy)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func toTransparentProxyAction(s string) TransparentProxyAction {
|
||||
switch s {
|
||||
case "allow":
|
||||
return TransparentProxyActionAllow
|
||||
case "inspect":
|
||||
return TransparentProxyActionInspect
|
||||
default:
|
||||
return TransparentProxyActionBlock
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,10 @@ type Policy struct {
|
||||
|
||||
// SourcePostureChecks are ID references to Posture checks for policy source groups
|
||||
SourcePostureChecks []string `gorm:"serializer:json"`
|
||||
|
||||
// InspectionPolicies are ID references to inspection policies applied to traffic matching this policy.
|
||||
// When set, traffic is routed through a transparent proxy on the destination network's routing peers.
|
||||
InspectionPolicies []string `gorm:"serializer:json"`
|
||||
}
|
||||
|
||||
// Copy returns a copy of the policy.
|
||||
@@ -85,11 +89,13 @@ func (p *Policy) Copy() *Policy {
|
||||
Enabled: p.Enabled,
|
||||
Rules: make([]*PolicyRule, len(p.Rules)),
|
||||
SourcePostureChecks: make([]string, len(p.SourcePostureChecks)),
|
||||
InspectionPolicies: make([]string, len(p.InspectionPolicies)),
|
||||
}
|
||||
for i, r := range p.Rules {
|
||||
c.Rules[i] = r.Copy()
|
||||
}
|
||||
copy(c.SourcePostureChecks, p.SourcePostureChecks)
|
||||
copy(c.InspectionPolicies, p.InspectionPolicies)
|
||||
return c
|
||||
}
|
||||
|
||||
|
||||
91
management/server/types/proxy_routes.go
Normal file
91
management/server/types/proxy_routes.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// ProxyRouteSet collects and deduplicates the routes that need to be pushed to
|
||||
// source peers for transparent proxy rules. CIDR rules create specific routes;
|
||||
// domain-only rules require a catch-all (0.0.0.0/0).
|
||||
type ProxyRouteSet struct {
|
||||
// routes is the deduplicated set of destination prefixes to route through the proxy.
|
||||
routes map[netip.Prefix]struct{}
|
||||
// needsCatchAll is true if any rule has domains without CIDRs.
|
||||
needsCatchAll bool
|
||||
}
|
||||
|
||||
// NewProxyRouteSet creates a new route set.
|
||||
func NewProxyRouteSet() *ProxyRouteSet {
|
||||
return &ProxyRouteSet{
|
||||
routes: make(map[netip.Prefix]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// AddFromRule adds route entries derived from a proxy rule's destinations.
|
||||
// - CIDR destinations create specific routes
|
||||
// - Domain-only rules (no CIDRs) trigger a catch-all route
|
||||
// - Rules with neither domains nor CIDRs also trigger catch-all (match all traffic)
|
||||
func (s *ProxyRouteSet) AddFromRule(rule *InspectionPolicyRule) {
|
||||
if rule.HasCIDRDestination() {
|
||||
for _, cidr := range rule.Networks {
|
||||
prefix, err := netip.ParsePrefix(cidr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
s.routes[prefix] = struct{}{}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Domain-only or no destination: need catch-all
|
||||
s.needsCatchAll = true
|
||||
}
|
||||
|
||||
// Routes returns the deduplicated list of prefixes to route through the proxy.
|
||||
// If any rule requires catch-all, returns only ["0.0.0.0/0"] since it subsumes
|
||||
// all specific CIDRs.
|
||||
func (s *ProxyRouteSet) Routes() []netip.Prefix {
|
||||
if s.needsCatchAll {
|
||||
return []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")}
|
||||
}
|
||||
|
||||
result := make([]netip.Prefix, 0, len(s.routes))
|
||||
for prefix := range s.routes {
|
||||
result = append(result, prefix)
|
||||
}
|
||||
|
||||
// Sort for deterministic output
|
||||
slices.SortFunc(result, func(a, b netip.Prefix) int {
|
||||
if c := a.Addr().Compare(b.Addr()); c != 0 {
|
||||
return c
|
||||
}
|
||||
return a.Bits() - b.Bits()
|
||||
})
|
||||
|
||||
// Remove CIDRs that are subsets of larger CIDRs
|
||||
return deduplicatePrefixes(result)
|
||||
}
|
||||
|
||||
// deduplicatePrefixes removes prefixes that are contained within other prefixes.
|
||||
// Input must be sorted.
|
||||
func deduplicatePrefixes(prefixes []netip.Prefix) []netip.Prefix {
|
||||
if len(prefixes) <= 1 {
|
||||
return prefixes
|
||||
}
|
||||
|
||||
var result []netip.Prefix
|
||||
for _, p := range prefixes {
|
||||
subsumed := false
|
||||
for _, existing := range result {
|
||||
if existing.Contains(p.Addr()) && existing.Bits() <= p.Bits() {
|
||||
subsumed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !subsumed {
|
||||
result = append(result, p)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
81
management/server/types/proxy_routes_test.go
Normal file
81
management/server/types/proxy_routes_test.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestProxyRouteSet_CIDROnly(t *testing.T) {
|
||||
s := NewProxyRouteSet()
|
||||
s.AddFromRule(&InspectionPolicyRule{
|
||||
Networks: []string{"10.0.0.0/8", "172.16.0.0/12"},
|
||||
})
|
||||
s.AddFromRule(&InspectionPolicyRule{
|
||||
Networks: []string{"192.168.0.0/16"},
|
||||
})
|
||||
|
||||
routes := s.Routes()
|
||||
require.Len(t, routes, 3)
|
||||
assert.Equal(t, netip.MustParsePrefix("10.0.0.0/8"), routes[0])
|
||||
assert.Equal(t, netip.MustParsePrefix("172.16.0.0/12"), routes[1])
|
||||
assert.Equal(t, netip.MustParsePrefix("192.168.0.0/16"), routes[2])
|
||||
}
|
||||
|
||||
func TestProxyRouteSet_DomainOnlyForceCatchAll(t *testing.T) {
|
||||
s := NewProxyRouteSet()
|
||||
s.AddFromRule(&InspectionPolicyRule{
|
||||
Domains: []string{"*.gambling.com"},
|
||||
})
|
||||
s.AddFromRule(&InspectionPolicyRule{
|
||||
Networks: []string{"10.0.0.0/8"},
|
||||
})
|
||||
|
||||
routes := s.Routes()
|
||||
require.Len(t, routes, 1)
|
||||
assert.Equal(t, netip.MustParsePrefix("0.0.0.0/0"), routes[0])
|
||||
}
|
||||
|
||||
func TestProxyRouteSet_EmptyDestinationForceCatchAll(t *testing.T) {
|
||||
s := NewProxyRouteSet()
|
||||
s.AddFromRule(&InspectionPolicyRule{
|
||||
Action: "block",
|
||||
// No domains, no networks: match all traffic
|
||||
})
|
||||
|
||||
routes := s.Routes()
|
||||
require.Len(t, routes, 1)
|
||||
assert.Equal(t, netip.MustParsePrefix("0.0.0.0/0"), routes[0])
|
||||
}
|
||||
|
||||
func TestProxyRouteSet_DeduplicateSubsets(t *testing.T) {
|
||||
s := NewProxyRouteSet()
|
||||
s.AddFromRule(&InspectionPolicyRule{
|
||||
Networks: []string{"10.0.0.0/8"},
|
||||
})
|
||||
s.AddFromRule(&InspectionPolicyRule{
|
||||
Networks: []string{"10.1.0.0/16"}, // subset of 10.0.0.0/8
|
||||
})
|
||||
s.AddFromRule(&InspectionPolicyRule{
|
||||
Networks: []string{"10.1.2.0/24"}, // subset of both
|
||||
})
|
||||
|
||||
routes := s.Routes()
|
||||
require.Len(t, routes, 1)
|
||||
assert.Equal(t, netip.MustParsePrefix("10.0.0.0/8"), routes[0])
|
||||
}
|
||||
|
||||
func TestProxyRouteSet_DuplicateCIDRs(t *testing.T) {
|
||||
s := NewProxyRouteSet()
|
||||
s.AddFromRule(&InspectionPolicyRule{
|
||||
Networks: []string{"10.0.0.0/8"},
|
||||
})
|
||||
s.AddFromRule(&InspectionPolicyRule{
|
||||
Networks: []string{"10.0.0.0/8"}, // duplicate
|
||||
})
|
||||
|
||||
routes := s.Routes()
|
||||
require.Len(t, routes, 1)
|
||||
}
|
||||
158
management/server/types/transparent_proxy.go
Normal file
158
management/server/types/transparent_proxy.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
proto "github.com/netbirdio/netbird/shared/management/proto"
|
||||
)
|
||||
|
||||
// TransparentProxyAction determines the proxy behavior for matched connections.
|
||||
type TransparentProxyAction int
|
||||
|
||||
const (
|
||||
TransparentProxyActionAllow TransparentProxyAction = 0
|
||||
TransparentProxyActionBlock TransparentProxyAction = 1
|
||||
TransparentProxyActionInspect TransparentProxyAction = 2
|
||||
)
|
||||
|
||||
// TransparentProxyMode selects built-in or external proxy operation.
|
||||
type TransparentProxyMode int
|
||||
|
||||
const (
|
||||
TransparentProxyModeBuiltin TransparentProxyMode = 0
|
||||
TransparentProxyModeExternal TransparentProxyMode = 1
|
||||
TransparentProxyModeEnvoy TransparentProxyMode = 2
|
||||
)
|
||||
|
||||
// TransparentProxyConfig holds the transparent proxy configuration for a routing peer.
|
||||
type TransparentProxyConfig struct {
|
||||
Enabled bool
|
||||
Mode TransparentProxyMode
|
||||
ExternalURL string
|
||||
DefaultAction TransparentProxyAction
|
||||
// RedirectSources is the set of source CIDRs to intercept.
|
||||
RedirectSources []string
|
||||
RedirectPorts []uint16
|
||||
Rules []TransparentProxyRule
|
||||
ICAP *TransparentProxyICAPConfig
|
||||
CACertPEM []byte
|
||||
CAKeyPEM []byte
|
||||
ListenPort uint16
|
||||
|
||||
// Envoy sidecar fields (ModeEnvoy only)
|
||||
EnvoyBinaryPath string
|
||||
EnvoyAdminPort uint16
|
||||
EnvoySnippets *TransparentProxyEnvoySnippets
|
||||
}
|
||||
|
||||
// TransparentProxyEnvoySnippets holds user-provided YAML fragments for envoy config.
|
||||
type TransparentProxyEnvoySnippets struct {
|
||||
HTTPFilters string
|
||||
NetworkFilters string
|
||||
Clusters string
|
||||
}
|
||||
|
||||
// TransparentProxyRule is an L7 inspection rule evaluated by the proxy engine.
|
||||
type TransparentProxyRule struct {
|
||||
ID string
|
||||
// Domains are domain patterns, e.g. "*.example.com".
|
||||
Domains []string
|
||||
// Networks restricts this rule to specific destination CIDRs.
|
||||
Networks []string
|
||||
// Protocols this rule applies to: "http", "https", "h2", "h3", "websocket", "other".
|
||||
Protocols []string
|
||||
// Paths are URL path patterns: "/api/", "/login", "/admin/*".
|
||||
Paths []string
|
||||
Action TransparentProxyAction
|
||||
Priority int
|
||||
}
|
||||
|
||||
// TransparentProxyICAPConfig holds ICAP service configuration.
|
||||
type TransparentProxyICAPConfig struct {
|
||||
ReqModURL string
|
||||
RespModURL string
|
||||
MaxConnections int
|
||||
}
|
||||
|
||||
// ToProto converts the internal config to the proto representation.
|
||||
func (c *TransparentProxyConfig) ToProto() *proto.TransparentProxyConfig {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
pc := &proto.TransparentProxyConfig{
|
||||
Enabled: c.Enabled,
|
||||
Mode: proto.TransparentProxyMode(c.Mode),
|
||||
DefaultAction: proto.TransparentProxyAction(c.DefaultAction),
|
||||
CaCertPem: c.CACertPEM,
|
||||
CaKeyPem: c.CAKeyPEM,
|
||||
ListenPort: uint32(c.ListenPort),
|
||||
}
|
||||
|
||||
if c.ExternalURL != "" {
|
||||
pc.ExternalProxyUrl = c.ExternalURL
|
||||
}
|
||||
|
||||
if c.Mode == TransparentProxyModeEnvoy {
|
||||
pc.EnvoyBinaryPath = c.EnvoyBinaryPath
|
||||
pc.EnvoyAdminPort = uint32(c.EnvoyAdminPort)
|
||||
if c.EnvoySnippets != nil {
|
||||
pc.EnvoySnippets = &proto.TransparentProxyEnvoySnippets{
|
||||
HttpFilters: c.EnvoySnippets.HTTPFilters,
|
||||
NetworkFilters: c.EnvoySnippets.NetworkFilters,
|
||||
Clusters: c.EnvoySnippets.Clusters,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pc.RedirectSources = c.RedirectSources
|
||||
|
||||
redirectPorts := make([]uint32, len(c.RedirectPorts))
|
||||
for i, p := range c.RedirectPorts {
|
||||
redirectPorts[i] = uint32(p)
|
||||
}
|
||||
pc.RedirectPorts = redirectPorts
|
||||
|
||||
for _, r := range c.Rules {
|
||||
pr := &proto.TransparentProxyRule{
|
||||
Id: r.ID,
|
||||
Domains: r.Domains,
|
||||
Networks: r.Networks,
|
||||
Paths: r.Paths,
|
||||
Action: proto.TransparentProxyAction(r.Action),
|
||||
Priority: int32(r.Priority),
|
||||
}
|
||||
for _, p := range r.Protocols {
|
||||
pr.Protocols = append(pr.Protocols, stringToProtoProtocol(p))
|
||||
}
|
||||
pc.Rules = append(pc.Rules, pr)
|
||||
}
|
||||
|
||||
if c.ICAP != nil {
|
||||
pc.Icap = &proto.TransparentProxyICAPConfig{
|
||||
ReqmodUrl: c.ICAP.ReqModURL,
|
||||
RespmodUrl: c.ICAP.RespModURL,
|
||||
MaxConnections: int32(c.ICAP.MaxConnections),
|
||||
}
|
||||
}
|
||||
|
||||
return pc
|
||||
}
|
||||
|
||||
// stringToProtoProtocol maps a protocol string to its proto enum value.
|
||||
func stringToProtoProtocol(s string) proto.TransparentProxyProtocol {
|
||||
switch s {
|
||||
case "http":
|
||||
return proto.TransparentProxyProtocol_TP_PROTO_HTTP
|
||||
case "https":
|
||||
return proto.TransparentProxyProtocol_TP_PROTO_HTTPS
|
||||
case "h2":
|
||||
return proto.TransparentProxyProtocol_TP_PROTO_H2
|
||||
case "h3":
|
||||
return proto.TransparentProxyProtocol_TP_PROTO_H3
|
||||
case "websocket":
|
||||
return proto.TransparentProxyProtocol_TP_PROTO_WEBSOCKET
|
||||
case "other":
|
||||
return proto.TransparentProxyProtocol_TP_PROTO_OTHER
|
||||
default:
|
||||
return proto.TransparentProxyProtocol_TP_PROTO_ALL
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user