mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-04 04:29:06 +02:00
Add transparent proxy inspection engine with envoy sidecar support
This commit is contained in:
@@ -364,6 +364,28 @@ func (m *Manager) SetupEBPFProxyNoTrack(proxyPort, wgPort uint16) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddTProxyRule adds TPROXY redirect rules for the transparent proxy.
|
||||
func (m *Manager) AddTProxyRule(ruleID string, sources []netip.Prefix, dstPorts []uint16, redirectPort uint16) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
return m.router.AddTProxyRule(ruleID, sources, dstPorts, redirectPort)
|
||||
}
|
||||
|
||||
// RemoveTProxyRule removes TPROXY redirect rules by ID.
|
||||
func (m *Manager) RemoveTProxyRule(ruleID string) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
return m.router.RemoveTProxyRule(ruleID)
|
||||
}
|
||||
|
||||
// AddUDPInspectionHook is a no-op for iptables (kernel-mode firewall has no userspace packet hooks).
|
||||
func (m *Manager) AddUDPInspectionHook(_ uint16, _ func([]byte) bool) string { return "" }
|
||||
|
||||
// RemoveUDPInspectionHook is a no-op for iptables.
|
||||
func (m *Manager) RemoveUDPInspectionHook(_ string) {}
|
||||
|
||||
func (m *Manager) initNoTrackChain() error {
|
||||
if err := m.cleanupNoTrackChain(); err != nil {
|
||||
log.Debugf("cleanup notrack chain: %v", err)
|
||||
|
||||
@@ -89,6 +89,8 @@ type router struct {
|
||||
|
||||
stateManager *statemanager.Manager
|
||||
ipFwdState *ipfwdstate.IPForwardingState
|
||||
|
||||
tproxyRules []tproxyRuleEntry
|
||||
}
|
||||
|
||||
func newRouter(iptablesClient *iptables.IPTables, wgIface iFaceMapper, mtu uint16) (*router, error) {
|
||||
@@ -1109,3 +1111,92 @@ func (r *router) addPrefixToIPSet(name string, prefix netip.Prefix) error {
|
||||
func (r *router) destroyIPSet(name string) error {
|
||||
return ipset.Destroy(name)
|
||||
}
|
||||
|
||||
// AddTProxyRule adds iptables nat PREROUTING REDIRECT rules for transparent proxy interception.
|
||||
// Traffic from sources on dstPorts arriving on the WG interface is redirected
|
||||
// to the transparent proxy listener on redirectPort.
|
||||
func (r *router) AddTProxyRule(ruleID string, sources []netip.Prefix, dstPorts []uint16, redirectPort uint16) error {
|
||||
portStr := fmt.Sprintf("%d", redirectPort)
|
||||
|
||||
for _, proto := range []string{"tcp", "udp"} {
|
||||
srcSpecs := r.buildSourceSpecs(sources)
|
||||
|
||||
for _, srcSpec := range srcSpecs {
|
||||
if len(dstPorts) == 0 {
|
||||
rule := append(srcSpec,
|
||||
"-i", r.wgIface.Name(),
|
||||
"-p", proto,
|
||||
"-j", "REDIRECT",
|
||||
"--to-ports", portStr,
|
||||
)
|
||||
if err := r.iptablesClient.AppendUnique(tableNat, chainRTRDR, rule...); err != nil {
|
||||
return fmt.Errorf("add redirect rule %s/%s: %w", ruleID, proto, err)
|
||||
}
|
||||
r.tproxyRules = append(r.tproxyRules, tproxyRuleEntry{
|
||||
ruleID: ruleID,
|
||||
table: tableNat,
|
||||
chain: chainRTRDR,
|
||||
spec: rule,
|
||||
})
|
||||
} else {
|
||||
for _, port := range dstPorts {
|
||||
rule := append(srcSpec,
|
||||
"-i", r.wgIface.Name(),
|
||||
"-p", proto,
|
||||
"--dport", fmt.Sprintf("%d", port),
|
||||
"-j", "REDIRECT",
|
||||
"--to-ports", portStr,
|
||||
)
|
||||
if err := r.iptablesClient.AppendUnique(tableNat, chainRTRDR, rule...); err != nil {
|
||||
return fmt.Errorf("add redirect rule %s/%s/%d: %w", ruleID, proto, port, err)
|
||||
}
|
||||
r.tproxyRules = append(r.tproxyRules, tproxyRuleEntry{
|
||||
ruleID: ruleID,
|
||||
table: tableNat,
|
||||
chain: chainRTRDR,
|
||||
spec: rule,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveTProxyRule removes all iptables REDIRECT rules for the given ruleID.
|
||||
func (r *router) RemoveTProxyRule(ruleID string) error {
|
||||
var remaining []tproxyRuleEntry
|
||||
for _, entry := range r.tproxyRules {
|
||||
if entry.ruleID != ruleID {
|
||||
remaining = append(remaining, entry)
|
||||
continue
|
||||
}
|
||||
if err := r.iptablesClient.DeleteIfExists(entry.table, entry.chain, entry.spec...); err != nil {
|
||||
log.Debugf("remove tproxy rule %s: %v", ruleID, err)
|
||||
}
|
||||
}
|
||||
r.tproxyRules = remaining
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type tproxyRuleEntry struct {
|
||||
ruleID string
|
||||
table string
|
||||
chain string
|
||||
spec []string
|
||||
}
|
||||
|
||||
func (r *router) buildSourceSpecs(sources []netip.Prefix) [][]string {
|
||||
if len(sources) == 0 {
|
||||
return [][]string{{}} // empty spec = match any source
|
||||
}
|
||||
|
||||
specs := make([][]string, 0, len(sources))
|
||||
for _, src := range sources {
|
||||
specs = append(specs, []string{"-s", src.String()})
|
||||
}
|
||||
return specs
|
||||
}
|
||||
|
||||
|
||||
@@ -180,6 +180,22 @@ type Manager interface {
|
||||
// SetupEBPFProxyNoTrack creates static notrack rules for eBPF proxy loopback traffic.
|
||||
// This prevents conntrack from interfering with WireGuard proxy communication.
|
||||
SetupEBPFProxyNoTrack(proxyPort, wgPort uint16) error
|
||||
|
||||
// AddTProxyRule adds TPROXY redirect rules for specific source CIDRs and destination ports.
|
||||
// Traffic from sources on dstPorts is redirected to the transparent proxy on redirectPort.
|
||||
// Empty dstPorts means redirect all ports.
|
||||
AddTProxyRule(ruleID string, sources []netip.Prefix, dstPorts []uint16, redirectPort uint16) error
|
||||
|
||||
// RemoveTProxyRule removes TPROXY redirect rules by ID.
|
||||
RemoveTProxyRule(ruleID string) error
|
||||
|
||||
// AddUDPInspectionHook registers a hook that inspects UDP packets before forwarding.
|
||||
// The hook receives the raw packet and returns true to drop it.
|
||||
// Used for QUIC SNI-based blocking. Returns a hook ID for removal.
|
||||
AddUDPInspectionHook(dstPort uint16, hook func(packet []byte) bool) string
|
||||
|
||||
// RemoveUDPInspectionHook removes a previously registered inspection hook.
|
||||
RemoveUDPInspectionHook(hookID string)
|
||||
}
|
||||
|
||||
func GenKey(format string, pair RouterPair) string {
|
||||
|
||||
@@ -482,6 +482,28 @@ func (m *Manager) SetupEBPFProxyNoTrack(proxyPort, wgPort uint16) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddTProxyRule adds TPROXY redirect rules for the transparent proxy.
|
||||
func (m *Manager) AddTProxyRule(ruleID string, sources []netip.Prefix, dstPorts []uint16, redirectPort uint16) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
return m.router.AddTProxyRule(ruleID, sources, dstPorts, redirectPort)
|
||||
}
|
||||
|
||||
// RemoveTProxyRule removes TPROXY redirect rules by ID.
|
||||
func (m *Manager) RemoveTProxyRule(ruleID string) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
return m.router.RemoveTProxyRule(ruleID)
|
||||
}
|
||||
|
||||
// AddUDPInspectionHook is a no-op for nftables (kernel-mode firewall has no userspace packet hooks).
|
||||
func (m *Manager) AddUDPInspectionHook(_ uint16, _ func([]byte) bool) string { return "" }
|
||||
|
||||
// RemoveUDPInspectionHook is a no-op for nftables.
|
||||
func (m *Manager) RemoveUDPInspectionHook(_ string) {}
|
||||
|
||||
func (m *Manager) initNoTrackChains(table *nftables.Table) error {
|
||||
m.notrackOutputChain = m.rConn.AddChain(&nftables.Chain{
|
||||
Name: chainNameRawOutput,
|
||||
|
||||
@@ -77,6 +77,7 @@ type router struct {
|
||||
ipFwdState *ipfwdstate.IPForwardingState
|
||||
legacyManagement bool
|
||||
mtu uint16
|
||||
|
||||
}
|
||||
|
||||
func newRouter(workTable *nftables.Table, wgIface iFaceMapper, mtu uint16) (*router, error) {
|
||||
@@ -2137,3 +2138,227 @@ func getIpSetExprs(ref refcounter.Ref[*nftables.Set], isSource bool) ([]expr.Any
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// AddTProxyRule adds nftables TPROXY redirect rules in the mangle prerouting chain.
|
||||
// Traffic from sources on dstPorts arriving on the WG interface is redirected to
|
||||
// the transparent proxy listener on redirectPort.
|
||||
// Separate rules are created for TCP and UDP protocols.
|
||||
func (r *router) AddTProxyRule(ruleID string, sources []netip.Prefix, dstPorts []uint16, redirectPort uint16) error {
|
||||
if err := r.refreshRulesMap(); err != nil {
|
||||
return fmt.Errorf(refreshRulesMapError, err)
|
||||
}
|
||||
|
||||
// Use the nat redirect chain for DNAT rules.
|
||||
// TPROXY doesn't work on WG kernel interfaces (socket assignment silently fails),
|
||||
// so we use DNAT to 127.0.0.1:proxy_port instead. The proxy reads the original
|
||||
// destination via SO_ORIGINAL_DST (conntrack).
|
||||
chain := r.chains[chainNameRoutingRdr]
|
||||
if chain == nil {
|
||||
return fmt.Errorf("nat redirect chain not initialized")
|
||||
}
|
||||
|
||||
for _, proto := range []uint8{unix.IPPROTO_TCP, unix.IPPROTO_UDP} {
|
||||
protoName := "tcp"
|
||||
if proto == unix.IPPROTO_UDP {
|
||||
protoName = "udp"
|
||||
}
|
||||
|
||||
ruleKey := fmt.Sprintf("tproxy-%s-%s", ruleID, protoName)
|
||||
|
||||
if existing, ok := r.rules[ruleKey]; ok && existing.Handle != 0 {
|
||||
if err := r.decrementSetCounter(existing); err != nil {
|
||||
log.Debugf("decrement set counter for %s: %v", ruleKey, err)
|
||||
}
|
||||
if err := r.conn.DelRule(existing); err != nil {
|
||||
log.Debugf("remove existing tproxy rule %s: %v", ruleKey, err)
|
||||
}
|
||||
delete(r.rules, ruleKey)
|
||||
}
|
||||
|
||||
exprs, err := r.buildRedirectExprs(proto, sources, dstPorts, redirectPort)
|
||||
if err != nil {
|
||||
return fmt.Errorf("build redirect exprs for %s: %w", protoName, err)
|
||||
}
|
||||
|
||||
r.rules[ruleKey] = r.conn.AddRule(&nftables.Rule{
|
||||
Table: r.workTable,
|
||||
Chain: chain,
|
||||
Exprs: exprs,
|
||||
UserData: []byte(ruleKey),
|
||||
})
|
||||
}
|
||||
|
||||
// Accept redirected packets in the ACL input chain. After REDIRECT, the
|
||||
// destination port becomes the proxy port. Without this rule, the ACL filter
|
||||
// drops the packet. We match on ct state dnat so only REDIRECT'd connections
|
||||
// are accepted: direct connections to the proxy port are blocked.
|
||||
inputAcceptKey := fmt.Sprintf("tproxy-%s-input", ruleID)
|
||||
if _, ok := r.rules[inputAcceptKey]; !ok {
|
||||
inputChain := &nftables.Chain{
|
||||
Name: "netbird-acl-input-rules",
|
||||
Table: r.workTable,
|
||||
}
|
||||
r.rules[inputAcceptKey] = r.conn.InsertRule(&nftables.Rule{
|
||||
Table: r.workTable,
|
||||
Chain: inputChain,
|
||||
Exprs: []expr.Any{
|
||||
// Only accept connections that were REDIRECT'd (ct status dnat)
|
||||
&expr.Ct{Register: 1, Key: expr.CtKeySTATUS},
|
||||
&expr.Bitwise{
|
||||
SourceRegister: 1,
|
||||
DestRegister: 1,
|
||||
Len: 4,
|
||||
Mask: binaryutil.NativeEndian.PutUint32(0x20), // IPS_DST_NAT
|
||||
Xor: binaryutil.NativeEndian.PutUint32(0),
|
||||
},
|
||||
&expr.Cmp{
|
||||
Op: expr.CmpOpNeq,
|
||||
Register: 1,
|
||||
Data: binaryutil.NativeEndian.PutUint32(0),
|
||||
},
|
||||
// Accept both TCP and UDP redirected to the proxy port.
|
||||
&expr.Payload{
|
||||
DestRegister: 1,
|
||||
Base: expr.PayloadBaseTransportHeader,
|
||||
Offset: 2,
|
||||
Len: 2,
|
||||
},
|
||||
&expr.Cmp{
|
||||
Op: expr.CmpOpEq,
|
||||
Register: 1,
|
||||
Data: binaryutil.BigEndian.PutUint16(redirectPort),
|
||||
},
|
||||
&expr.Verdict{Kind: expr.VerdictAccept},
|
||||
},
|
||||
UserData: []byte(inputAcceptKey),
|
||||
})
|
||||
}
|
||||
|
||||
if err := r.conn.Flush(); err != nil {
|
||||
return fmt.Errorf("flush tproxy rules for %s: %w", ruleID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveTProxyRule removes TPROXY redirect rules by ID (both TCP and UDP variants).
|
||||
func (r *router) RemoveTProxyRule(ruleID string) error {
|
||||
if err := r.refreshRulesMap(); err != nil {
|
||||
return fmt.Errorf(refreshRulesMapError, err)
|
||||
}
|
||||
|
||||
var removed int
|
||||
for _, suffix := range []string{"tcp", "udp", "input"} {
|
||||
ruleKey := fmt.Sprintf("tproxy-%s-%s", ruleID, suffix)
|
||||
|
||||
rule, ok := r.rules[ruleKey]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if rule.Handle == 0 {
|
||||
delete(r.rules, ruleKey)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := r.decrementSetCounter(rule); err != nil {
|
||||
log.Debugf("decrement set counter for %s: %v", ruleKey, err)
|
||||
}
|
||||
if err := r.conn.DelRule(rule); err != nil {
|
||||
log.Debugf("delete tproxy rule %s: %v", ruleKey, err)
|
||||
}
|
||||
delete(r.rules, ruleKey)
|
||||
removed++
|
||||
}
|
||||
|
||||
if removed > 0 {
|
||||
if err := r.conn.Flush(); err != nil {
|
||||
return fmt.Errorf("flush tproxy rule removal for %s: %w", ruleID, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildRedirectExprs builds nftables expressions for a REDIRECT rule.
|
||||
// Matches WG interface ingress, source CIDRs, destination ports, then REDIRECTs to the proxy port.
|
||||
func (r *router) buildRedirectExprs(proto uint8, sources []netip.Prefix, dstPorts []uint16, redirectPort uint16) ([]expr.Any, error) {
|
||||
var exprs []expr.Any
|
||||
|
||||
exprs = append(exprs,
|
||||
&expr.Meta{Key: expr.MetaKeyIIFNAME, Register: 1},
|
||||
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: ifname(r.wgIface.Name())},
|
||||
)
|
||||
|
||||
exprs = append(exprs,
|
||||
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
|
||||
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: []byte{proto}},
|
||||
)
|
||||
|
||||
// Source CIDRs use the named ipset shared with route rules.
|
||||
if len(sources) > 0 {
|
||||
srcSet := firewall.NewPrefixSet(sources)
|
||||
srcExprs, err := r.getIpSet(srcSet, sources, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get source ipset: %w", err)
|
||||
}
|
||||
exprs = append(exprs, srcExprs...)
|
||||
}
|
||||
|
||||
if len(dstPorts) == 1 {
|
||||
exprs = append(exprs,
|
||||
&expr.Payload{
|
||||
DestRegister: 1,
|
||||
Base: expr.PayloadBaseTransportHeader,
|
||||
Offset: 2,
|
||||
Len: 2,
|
||||
},
|
||||
&expr.Cmp{
|
||||
Op: expr.CmpOpEq,
|
||||
Register: 1,
|
||||
Data: binaryutil.BigEndian.PutUint16(dstPorts[0]),
|
||||
},
|
||||
)
|
||||
} else if len(dstPorts) > 1 {
|
||||
setElements := make([]nftables.SetElement, len(dstPorts))
|
||||
for i, p := range dstPorts {
|
||||
setElements[i] = nftables.SetElement{Key: binaryutil.BigEndian.PutUint16(p)}
|
||||
}
|
||||
portSet := &nftables.Set{
|
||||
Table: r.workTable,
|
||||
Anonymous: true,
|
||||
Constant: true,
|
||||
KeyType: nftables.TypeInetService,
|
||||
}
|
||||
if err := r.conn.AddSet(portSet, setElements); err != nil {
|
||||
return nil, fmt.Errorf("create port set: %w", err)
|
||||
}
|
||||
exprs = append(exprs,
|
||||
&expr.Payload{
|
||||
DestRegister: 1,
|
||||
Base: expr.PayloadBaseTransportHeader,
|
||||
Offset: 2,
|
||||
Len: 2,
|
||||
},
|
||||
&expr.Lookup{
|
||||
SourceRegister: 1,
|
||||
SetName: portSet.Name,
|
||||
SetID: portSet.ID,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// REDIRECT to local proxy port. Changes the destination to the interface's
|
||||
// primary address + specified port. Conntrack tracks the original destination,
|
||||
// readable via SO_ORIGINAL_DST.
|
||||
exprs = append(exprs,
|
||||
&expr.Immediate{Register: 1, Data: binaryutil.BigEndian.PutUint16(redirectPort)},
|
||||
&expr.Redir{
|
||||
RegisterProtoMin: 1,
|
||||
},
|
||||
)
|
||||
|
||||
return exprs, nil
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -641,6 +641,45 @@ func (m *Manager) SetupEBPFProxyNoTrack(proxyPort, wgPort uint16) error {
|
||||
return m.nativeFirewall.SetupEBPFProxyNoTrack(proxyPort, wgPort)
|
||||
}
|
||||
|
||||
// AddTProxyRule delegates to the native firewall for TPROXY rules.
|
||||
// In userspace mode (no native firewall), this is a no-op since the
|
||||
// forwarder intercepts traffic directly.
|
||||
func (m *Manager) AddTProxyRule(ruleID string, sources []netip.Prefix, dstPorts []uint16, redirectPort uint16) error {
|
||||
if m.nativeFirewall == nil {
|
||||
return nil
|
||||
}
|
||||
return m.nativeFirewall.AddTProxyRule(ruleID, sources, dstPorts, redirectPort)
|
||||
}
|
||||
|
||||
// AddUDPInspectionHook registers a hook for QUIC/UDP inspection via the packet filter.
|
||||
func (m *Manager) AddUDPInspectionHook(dstPort uint16, hook func(packet []byte) bool) string {
|
||||
m.SetUDPPacketHook(netip.Addr{}, dstPort, hook)
|
||||
return "udp-inspection"
|
||||
}
|
||||
|
||||
// RemoveUDPInspectionHook removes a previously registered inspection hook.
|
||||
func (m *Manager) RemoveUDPInspectionHook(_ string) {
|
||||
m.SetUDPPacketHook(netip.Addr{}, 0, nil)
|
||||
}
|
||||
|
||||
// RemoveTProxyRule delegates to the native firewall for TPROXY rules.
|
||||
func (m *Manager) RemoveTProxyRule(ruleID string) error {
|
||||
if m.nativeFirewall == nil {
|
||||
return nil
|
||||
}
|
||||
return m.nativeFirewall.RemoveTProxyRule(ruleID)
|
||||
}
|
||||
|
||||
// IsLocalIP reports whether the given IP belongs to the local machine.
|
||||
func (m *Manager) IsLocalIP(ip netip.Addr) bool {
|
||||
return m.localipmanager.IsLocalIP(ip)
|
||||
}
|
||||
|
||||
// GetForwarder returns the userspace packet forwarder, or nil if not initialized.
|
||||
func (m *Manager) GetForwarder() *forwarder.Forwarder {
|
||||
return m.forwarder.Load()
|
||||
}
|
||||
|
||||
// UpdateSet updates the rule destinations associated with the given set
|
||||
// by merging the existing prefixes with the new ones, then deduplicating.
|
||||
func (m *Manager) UpdateSet(set firewall.Set, prefixes []netip.Prefix) error {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net/netip"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -21,6 +22,7 @@ import (
|
||||
|
||||
"github.com/netbirdio/netbird/client/firewall/uspfilter/common"
|
||||
"github.com/netbirdio/netbird/client/firewall/uspfilter/conntrack"
|
||||
"github.com/netbirdio/netbird/client/inspect"
|
||||
nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log"
|
||||
nftypes "github.com/netbirdio/netbird/client/internal/netflow/types"
|
||||
)
|
||||
@@ -46,6 +48,10 @@ type Forwarder struct {
|
||||
netstack bool
|
||||
hasRawICMPAccess bool
|
||||
pingSemaphore chan struct{}
|
||||
// proxy is the optional inspection engine.
|
||||
// When set, TCP connections are handed to the engine for protocol detection
|
||||
// and rule evaluation. Swapped atomically for lock-free hot-path access.
|
||||
proxy atomic.Pointer[inspect.Proxy]
|
||||
}
|
||||
|
||||
func New(iface common.IFaceMapper, logger *nblog.Logger, flowLogger nftypes.FlowLogger, netstack bool, mtu uint16) (*Forwarder, error) {
|
||||
@@ -79,7 +85,7 @@ func New(iface common.IFaceMapper, logger *nblog.Logger, flowLogger nftypes.Flow
|
||||
}
|
||||
|
||||
if err := s.AddProtocolAddress(nicID, protoAddr, stack.AddressProperties{}); err != nil {
|
||||
return nil, fmt.Errorf("failed to add protocol address: %s", err)
|
||||
return nil, fmt.Errorf("add protocol address: %s", err)
|
||||
}
|
||||
|
||||
defaultSubnet, err := tcpip.NewSubnet(
|
||||
@@ -155,6 +161,13 @@ func (f *Forwarder) InjectIncomingPacket(payload []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetProxy sets the inspection engine. When set, TCP connections are handed
|
||||
// to it for protocol detection and rule evaluation instead of direct relay.
|
||||
// Pass nil to disable inspection.
|
||||
func (f *Forwarder) SetProxy(p *inspect.Proxy) {
|
||||
f.proxy.Store(p)
|
||||
}
|
||||
|
||||
// Stop gracefully shuts down the forwarder
|
||||
func (f *Forwarder) Stop() {
|
||||
f.cancel()
|
||||
@@ -167,6 +180,25 @@ func (f *Forwarder) Stop() {
|
||||
f.stack.Wait()
|
||||
}
|
||||
|
||||
// CheckUDPPacket inspects a UDP payload against proxy rules before injection.
|
||||
// This is called by the filter for QUIC SNI-based blocking.
|
||||
// Returns true if the packet should be allowed, false if it should be dropped.
|
||||
func (f *Forwarder) CheckUDPPacket(payload []byte, srcIP, dstIP netip.Addr, srcPort, dstPort uint16, ruleID []byte) bool {
|
||||
p := f.proxy.Load()
|
||||
if p == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
dst := netip.AddrPortFrom(dstIP, dstPort)
|
||||
src := inspect.SourceInfo{
|
||||
IP: srcIP,
|
||||
PolicyID: inspect.PolicyID(ruleID),
|
||||
}
|
||||
|
||||
action := p.HandleUDPPacket(payload, dst, src)
|
||||
return action != inspect.ActionBlock
|
||||
}
|
||||
|
||||
func (f *Forwarder) determineDialAddr(addr tcpip.Address) net.IP {
|
||||
if f.netstack && f.ip.Equal(addr) {
|
||||
return net.IPv4(127, 0, 0, 1)
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
|
||||
"gvisor.dev/gvisor/pkg/waiter"
|
||||
|
||||
"github.com/netbirdio/netbird/client/inspect"
|
||||
nftypes "github.com/netbirdio/netbird/client/internal/netflow/types"
|
||||
)
|
||||
|
||||
@@ -23,6 +24,86 @@ import (
|
||||
func (f *Forwarder) handleTCP(r *tcp.ForwarderRequest) {
|
||||
id := r.ID()
|
||||
|
||||
// If the inspection engine is configured, accept the connection first and hand it off.
|
||||
if p := f.proxy.Load(); p != nil {
|
||||
f.handleTCPWithInspection(r, id, p)
|
||||
return
|
||||
}
|
||||
|
||||
f.handleTCPDirect(r, id)
|
||||
}
|
||||
|
||||
// handleTCPWithInspection accepts the connection and hands it to the inspection
|
||||
// engine. For allow decisions, the forwarder does its own relay (passthrough).
|
||||
// For block/inspect, the engine handles everything internally.
|
||||
func (f *Forwarder) handleTCPWithInspection(r *tcp.ForwarderRequest, id stack.TransportEndpointID, p *inspect.Proxy) {
|
||||
flowID := uuid.New()
|
||||
f.sendTCPEvent(nftypes.TypeStart, flowID, id, 0, 0, 0, 0)
|
||||
|
||||
wq := waiter.Queue{}
|
||||
ep, epErr := r.CreateEndpoint(&wq)
|
||||
if epErr != nil {
|
||||
f.logger.Error1("forwarder: create TCP endpoint for inspection: %v", epErr)
|
||||
r.Complete(true)
|
||||
f.sendTCPEvent(nftypes.TypeEnd, flowID, id, 0, 0, 0, 0)
|
||||
return
|
||||
}
|
||||
r.Complete(false)
|
||||
|
||||
inConn := gonet.NewTCPConn(&wq, ep)
|
||||
|
||||
srcIP := netip.AddrFrom4(id.RemoteAddress.As4())
|
||||
dstIP := netip.AddrFrom4(id.LocalAddress.As4())
|
||||
dst := netip.AddrPortFrom(dstIP, id.LocalPort)
|
||||
|
||||
var policyID []byte
|
||||
if ruleID, ok := f.getRuleID(srcIP, dstIP, id.RemotePort, id.LocalPort); ok {
|
||||
policyID = ruleID
|
||||
}
|
||||
|
||||
src := inspect.SourceInfo{
|
||||
IP: srcIP,
|
||||
PolicyID: inspect.PolicyID(policyID),
|
||||
}
|
||||
|
||||
f.logger.Trace1("forwarder: handing TCP %v to inspection engine", epID(id))
|
||||
|
||||
go func() {
|
||||
result, err := p.InspectTCP(f.ctx, inConn, dst, src)
|
||||
if err != nil && err != inspect.ErrBlocked {
|
||||
f.logger.Debug2("forwarder: inspection error for %v: %v", epID(id), err)
|
||||
}
|
||||
|
||||
// Passthrough: engine returned allow, forwarder does the relay.
|
||||
if result.PassthroughConn != nil {
|
||||
dialAddr := fmt.Sprintf("%s:%d", f.determineDialAddr(id.LocalAddress), id.LocalPort)
|
||||
outConn, dialErr := (&net.Dialer{}).DialContext(f.ctx, "tcp", dialAddr)
|
||||
if dialErr != nil {
|
||||
f.logger.Trace2("forwarder: passthrough dial error for %v: %v", epID(id), dialErr)
|
||||
if closeErr := result.PassthroughConn.Close(); closeErr != nil {
|
||||
f.logger.Debug1("forwarder: close passthrough conn: %v", closeErr)
|
||||
}
|
||||
ep.Close()
|
||||
f.sendTCPEvent(nftypes.TypeEnd, flowID, id, 0, 0, 0, 0)
|
||||
return
|
||||
}
|
||||
f.proxyTCPPassthrough(id, result.PassthroughConn, outConn, ep, flowID)
|
||||
return
|
||||
}
|
||||
|
||||
// Engine handled it (block/inspect/HTTP). Capture stats and clean up.
|
||||
var rxPackets, txPackets uint64
|
||||
if tcpStats, ok := ep.Stats().(*tcp.Stats); ok {
|
||||
rxPackets = tcpStats.SegmentsSent.Value()
|
||||
txPackets = tcpStats.SegmentsReceived.Value()
|
||||
}
|
||||
ep.Close()
|
||||
f.sendTCPEvent(nftypes.TypeEnd, flowID, id, 0, 0, rxPackets, txPackets)
|
||||
}()
|
||||
}
|
||||
|
||||
// handleTCPDirect handles TCP connections with direct relay (no proxy).
|
||||
func (f *Forwarder) handleTCPDirect(r *tcp.ForwarderRequest, id stack.TransportEndpointID) {
|
||||
flowID := uuid.New()
|
||||
|
||||
f.sendTCPEvent(nftypes.TypeStart, flowID, id, 0, 0, 0, 0)
|
||||
@@ -42,7 +123,6 @@ func (f *Forwarder) handleTCP(r *tcp.ForwarderRequest) {
|
||||
return
|
||||
}
|
||||
|
||||
// Create wait queue for blocking syscalls
|
||||
wq := waiter.Queue{}
|
||||
|
||||
ep, epErr := r.CreateEndpoint(&wq)
|
||||
@@ -55,7 +135,6 @@ func (f *Forwarder) handleTCP(r *tcp.ForwarderRequest) {
|
||||
return
|
||||
}
|
||||
|
||||
// Complete the handshake
|
||||
r.Complete(false)
|
||||
|
||||
inConn := gonet.NewTCPConn(&wq, ep)
|
||||
@@ -73,7 +152,6 @@ func (f *Forwarder) proxyTCP(id stack.TransportEndpointID, inConn *gonet.TCPConn
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
// Close connections and endpoint.
|
||||
if err := inConn.Close(); err != nil && !isClosedError(err) {
|
||||
f.logger.Debug1("forwarder: inConn close error: %v", err)
|
||||
}
|
||||
@@ -132,6 +210,66 @@ func (f *Forwarder) proxyTCP(id stack.TransportEndpointID, inConn *gonet.TCPConn
|
||||
f.sendTCPEvent(nftypes.TypeEnd, flowID, id, uint64(bytesFromOutToIn), uint64(bytesFromInToOut), rxPackets, txPackets)
|
||||
}
|
||||
|
||||
// proxyTCPPassthrough relays traffic between a peeked inbound connection
|
||||
// (from the inspection engine passthrough) and the outbound connection.
|
||||
// It accepts net.Conn for inConn since the inspection engine wraps it in a peekConn.
|
||||
func (f *Forwarder) proxyTCPPassthrough(id stack.TransportEndpointID, inConn net.Conn, outConn net.Conn, ep tcpip.Endpoint, flowID uuid.UUID) {
|
||||
ctx, cancel := context.WithCancel(f.ctx)
|
||||
defer cancel()
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
if err := inConn.Close(); err != nil && !isClosedError(err) {
|
||||
f.logger.Debug1("forwarder: passthrough inConn close: %v", err)
|
||||
}
|
||||
if err := outConn.Close(); err != nil && !isClosedError(err) {
|
||||
f.logger.Debug1("forwarder: passthrough outConn close: %v", err)
|
||||
}
|
||||
ep.Close()
|
||||
}()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
|
||||
var (
|
||||
bytesIn int64
|
||||
bytesOut int64
|
||||
errIn error
|
||||
errOut error
|
||||
)
|
||||
|
||||
go func() {
|
||||
bytesIn, errIn = io.Copy(outConn, inConn)
|
||||
cancel()
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
bytesOut, errOut = io.Copy(inConn, outConn)
|
||||
cancel()
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if errIn != nil && !isClosedError(errIn) {
|
||||
f.logger.Error2("proxyTCPPassthrough: copy error (in→out) for %s: %v", epID(id), errIn)
|
||||
}
|
||||
if errOut != nil && !isClosedError(errOut) {
|
||||
f.logger.Error2("proxyTCPPassthrough: copy error (out→in) for %s: %v", epID(id), errOut)
|
||||
}
|
||||
|
||||
var rxPackets, txPackets uint64
|
||||
if tcpStats, ok := ep.Stats().(*tcp.Stats); ok {
|
||||
rxPackets = tcpStats.SegmentsSent.Value()
|
||||
txPackets = tcpStats.SegmentsReceived.Value()
|
||||
}
|
||||
|
||||
f.logger.Trace5("forwarder: passthrough TCP %s [in: %d Pkts/%d B, out: %d Pkts/%d B]", epID(id), rxPackets, bytesOut, txPackets, bytesIn)
|
||||
|
||||
f.sendTCPEvent(nftypes.TypeEnd, flowID, id, uint64(bytesOut), uint64(bytesIn), rxPackets, txPackets)
|
||||
}
|
||||
|
||||
func (f *Forwarder) sendTCPEvent(typ nftypes.Type, flowID uuid.UUID, id stack.TransportEndpointID, rxBytes, txBytes, rxPackets, txPackets uint64) {
|
||||
srcIp := netip.AddrFrom4(id.RemoteAddress.As4())
|
||||
dstIp := netip.AddrFrom4(id.LocalAddress.As4())
|
||||
|
||||
Reference in New Issue
Block a user