From afbddae4729286380895e52a534a4d0a051ace1d Mon Sep 17 00:00:00 2001 From: Viktor Liu Date: Sat, 11 Apr 2026 18:07:46 +0200 Subject: [PATCH] Add transparent proxy inspection engine with envoy sidecar support --- .gitignore | 2 + client/firewall/iptables/manager_linux.go | 22 + client/firewall/iptables/router_linux.go | 91 + client/firewall/manager/firewall.go | 16 + client/firewall/nftables/manager_linux.go | 22 + client/firewall/nftables/router_linux.go | 225 +++ client/firewall/uspfilter/filter.go | 39 + .../firewall/uspfilter/forwarder/forwarder.go | 34 +- client/firewall/uspfilter/forwarder/tcp.go | 144 +- client/inspect/config.go | 212 ++ client/inspect/config_test.go | 93 + client/inspect/dialer_linux.go | 25 + client/inspect/dialer_other.go | 11 + client/inspect/envoy.go | 298 +++ client/inspect/envoy_config.go | 382 ++++ client/inspect/envoy_forward.go | 88 + client/inspect/envoy_signal.go | 13 + client/inspect/envoy_signal_windows.go | 13 + client/inspect/external.go | 229 +++ client/inspect/http.go | 532 +++++ client/inspect/icap.go | 479 +++++ client/inspect/listener.go | 21 + client/inspect/listener_linux.go | 89 + client/inspect/mitm.go | 200 ++ client/inspect/mitm_test.go | 133 ++ client/inspect/peek.go | 109 ++ client/inspect/proxy.go | 482 +++++ client/inspect/quic.go | 388 ++++ client/inspect/quic_test.go | 99 + client/inspect/rules.go | 253 +++ client/inspect/rules_test.go | 338 ++++ client/inspect/sni.go | 287 +++ client/inspect/sni_test.go | 109 ++ client/inspect/tls.go | 287 +++ client/internal/connect.go | 3 + client/internal/engine.go | 16 + client/internal/engine_tproxy.go | 571 ++++++ client/internal/engine_tproxy_test.go | 279 +++ client/internal/profilemanager/config.go | 21 + .../internals/shared/grpc/conversion.go | 4 + management/server/account/manager.go | 4 + management/server/http/handler.go | 2 + .../http/handlers/inspection/handler.go | 288 +++ .../handlers/policies/policies_handler.go | 5 + management/server/inspection_policies.go | 52 + management/server/mock_server/account_mock.go | 20 + .../server/networks/routers/types/router.go | 135 +- management/server/policy.go | 7 + management/server/store/sql_store.go | 60 +- .../store/sql_store_inspection_policy_test.go | 151 ++ management/server/store/store.go | 5 + management/server/store/store_mock.go | 560 +++--- management/server/types/account.go | 1 + management/server/types/account_components.go | 7 + management/server/types/inspection_policy.go | 158 ++ management/server/types/network.go | 7 +- .../server/types/networkmap_components.go | 226 ++- management/server/types/policy.go | 6 + management/server/types/proxy_routes.go | 91 + management/server/types/proxy_routes_test.go | 81 + management/server/types/transparent_proxy.go | 158 ++ shared/management/http/api/openapi.yml | 327 ++++ shared/management/http/api/types.gen.go | 369 +++- shared/management/proto/management.pb.go | 1722 ++++++++++++----- shared/management/proto/management.proto | 90 + 65 files changed, 10428 insertions(+), 763 deletions(-) create mode 100644 client/inspect/config.go create mode 100644 client/inspect/config_test.go create mode 100644 client/inspect/dialer_linux.go create mode 100644 client/inspect/dialer_other.go create mode 100644 client/inspect/envoy.go create mode 100644 client/inspect/envoy_config.go create mode 100644 client/inspect/envoy_forward.go create mode 100644 client/inspect/envoy_signal.go create mode 100644 client/inspect/envoy_signal_windows.go create mode 100644 client/inspect/external.go create mode 100644 client/inspect/http.go create mode 100644 client/inspect/icap.go create mode 100644 client/inspect/listener.go create mode 100644 client/inspect/listener_linux.go create mode 100644 client/inspect/mitm.go create mode 100644 client/inspect/mitm_test.go create mode 100644 client/inspect/peek.go create mode 100644 client/inspect/proxy.go create mode 100644 client/inspect/quic.go create mode 100644 client/inspect/quic_test.go create mode 100644 client/inspect/rules.go create mode 100644 client/inspect/rules_test.go create mode 100644 client/inspect/sni.go create mode 100644 client/inspect/sni_test.go create mode 100644 client/inspect/tls.go create mode 100644 client/internal/engine_tproxy.go create mode 100644 client/internal/engine_tproxy_test.go create mode 100644 management/server/http/handlers/inspection/handler.go create mode 100644 management/server/inspection_policies.go create mode 100644 management/server/store/sql_store_inspection_policy_test.go create mode 100644 management/server/types/inspection_policy.go create mode 100644 management/server/types/proxy_routes.go create mode 100644 management/server/types/proxy_routes_test.go create mode 100644 management/server/types/transparent_proxy.go diff --git a/.gitignore b/.gitignore index a0f128933..28cb2d9f5 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,5 @@ infrastructure_files/setup-*.env vendor/ /netbird client/netbird-electron/ +management/server/types/testdata/comparison/ +management/server/types/testdata/*.json diff --git a/client/firewall/iptables/manager_linux.go b/client/firewall/iptables/manager_linux.go index a1d4467d5..77b0c912f 100644 --- a/client/firewall/iptables/manager_linux.go +++ b/client/firewall/iptables/manager_linux.go @@ -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) diff --git a/client/firewall/iptables/router_linux.go b/client/firewall/iptables/router_linux.go index a7c4f67dd..cefb0a098 100644 --- a/client/firewall/iptables/router_linux.go +++ b/client/firewall/iptables/router_linux.go @@ -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 +} + diff --git a/client/firewall/manager/firewall.go b/client/firewall/manager/firewall.go index d65d717b3..6d17383d8 100644 --- a/client/firewall/manager/firewall.go +++ b/client/firewall/manager/firewall.go @@ -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 { diff --git a/client/firewall/nftables/manager_linux.go b/client/firewall/nftables/manager_linux.go index 0b5b61e04..db507f4b4 100644 --- a/client/firewall/nftables/manager_linux.go +++ b/client/firewall/nftables/manager_linux.go @@ -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, diff --git a/client/firewall/nftables/router_linux.go b/client/firewall/nftables/router_linux.go index 904daf7cb..55d9722fc 100644 --- a/client/firewall/nftables/router_linux.go +++ b/client/firewall/nftables/router_linux.go @@ -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 +} + + diff --git a/client/firewall/uspfilter/filter.go b/client/firewall/uspfilter/filter.go index cb9e1bb0a..41ca0557c 100644 --- a/client/firewall/uspfilter/filter.go +++ b/client/firewall/uspfilter/filter.go @@ -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 { diff --git a/client/firewall/uspfilter/forwarder/forwarder.go b/client/firewall/uspfilter/forwarder/forwarder.go index d17c3cd5c..e41e0a9b9 100644 --- a/client/firewall/uspfilter/forwarder/forwarder.go +++ b/client/firewall/uspfilter/forwarder/forwarder.go @@ -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) diff --git a/client/firewall/uspfilter/forwarder/tcp.go b/client/firewall/uspfilter/forwarder/tcp.go index aef420061..6eafe7c77 100644 --- a/client/firewall/uspfilter/forwarder/tcp.go +++ b/client/firewall/uspfilter/forwarder/tcp.go @@ -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()) diff --git a/client/inspect/config.go b/client/inspect/config.go new file mode 100644 index 000000000..f3c487cf1 --- /dev/null +++ b/client/inspect/config.go @@ -0,0 +1,212 @@ +package inspect + +import ( + "crypto" + "crypto/x509" + "net" + "net/netip" + "net/url" + "strings" + + "github.com/netbirdio/netbird/client/internal/acl/id" + "github.com/netbirdio/netbird/shared/management/domain" +) + +// InspectResult holds the outcome of connection inspection. +type InspectResult struct { + // Action is the rule evaluation result. + Action Action + // PassthroughConn is the client connection with buffered peeked bytes. + // Non-nil only when Action is ActionAllow and the caller should relay + // (TLS passthrough or non-HTTP/TLS protocol). The caller takes ownership + // and is responsible for closing this connection. + PassthroughConn net.Conn +} + +const ( + // DefaultTProxyPort is the default TPROXY listener port for kernel mode. + // Override with NB_TPROXY_PORT environment variable. + DefaultTProxyPort = 22080 +) + +// Action determines how the proxy handles a matched connection. +type Action string + +const ( + // ActionAllow passes the connection through without decryption. + ActionAllow Action = "allow" + // ActionBlock denies the connection. + ActionBlock Action = "block" + // ActionInspect decrypts (MITM) and inspects the connection. + ActionInspect Action = "inspect" +) + +// ProxyMode determines the proxy operating mode. +type ProxyMode string + +const ( + // ModeBuiltin uses the built-in proxy with rules and optional ICAP. + ModeBuiltin ProxyMode = "builtin" + // ModeEnvoy runs a local envoy sidecar for L7 processing. + // Go manages envoy lifecycle, config generation, and rule evaluation. + // USP path forwards via PROXY protocol v2; kernel path uses nftables redirect. + ModeEnvoy ProxyMode = "envoy" + // ModeExternal forwards all traffic to an external proxy. + ModeExternal ProxyMode = "external" +) + +// PolicyID is the management policy identifier associated with a connection. +type PolicyID []byte + +// MatchDomain reports whether target matches the pattern. +// If pattern starts with "*.", it matches any subdomain (but not the base itself). +// Otherwise it requires an exact match. +func MatchDomain(pattern, target domain.Domain) bool { + p := pattern.PunycodeString() + t := target.PunycodeString() + + if strings.HasPrefix(p, "*.") { + base := p[2:] + return strings.HasSuffix(t, "."+base) + } + + return p == t +} + +// SourceInfo carries source identity context for rule evaluation. +// The source may be a direct WireGuard peer or a host behind +// a site-to-site gateway. +type SourceInfo struct { + // IP is the original source address from the packet. + IP netip.Addr + // PolicyID is the management policy that allowed this traffic + // through route ACLs. + PolicyID PolicyID +} + +// ProtoType identifies a protocol handled by the proxy. +type ProtoType string + +const ( + ProtoHTTP ProtoType = "http" + ProtoHTTPS ProtoType = "https" + ProtoH2 ProtoType = "h2" + ProtoH3 ProtoType = "h3" + ProtoWebSocket ProtoType = "websocket" + ProtoOther ProtoType = "other" +) + +// Rule defines a proxy inspection/filtering rule. +type Rule struct { + // ID uniquely identifies this rule. + ID id.RuleID + // Sources are the source CIDRs this rule applies to. + // Includes both direct peer IPs and routed networks behind gateways. + Sources []netip.Prefix + // Domains are the destination domain patterns to match (via SNI or Host header). + // Supports exact match ("example.com") and wildcard ("*.example.com"). + Domains []domain.Domain + // Networks are the destination CIDRs to match. + Networks []netip.Prefix + // Ports are the destination ports to match. Empty means all ports. + Ports []uint16 + // Protocols restricts which protocols this rule applies to. + // Empty means all protocols. + Protocols []ProtoType + // Paths are URL path patterns to match (HTTP only, requires inspect for HTTPS). + // Supports prefix ("/api/"), exact ("/login"), and wildcard ("/admin/*"). + // Empty means all paths. + Paths []string + // Action determines what to do with matched connections. + Action Action + // Priority controls evaluation order. Lower values are evaluated first. + Priority int +} + +// ICAPConfig holds ICAP service configuration. +type ICAPConfig struct { + // ReqModURL is the ICAP REQMOD service URL (e.g., icap://server:1344/reqmod). + ReqModURL *url.URL + // RespModURL is the ICAP RESPMOD service URL (e.g., icap://server:1344/respmod). + RespModURL *url.URL + // MaxConnections is the connection pool size. Zero uses a default. + MaxConnections int +} + +// TLSConfig holds the MITM CA configuration for TLS inspection. +type TLSConfig struct { + // CA is the certificate authority used to sign dynamic certificates. + CA *x509.Certificate + // CAKey is the CA's private key. + CAKey crypto.PrivateKey +} + +// Config holds the transparent proxy configuration. +type Config struct { + // Enabled controls whether the proxy is active. + Enabled bool + // Mode selects built-in or external proxy operation. + Mode ProxyMode + // ExternalURL is the upstream proxy URL for ModeExternal. + // Supports http:// and socks5:// schemes. + ExternalURL *url.URL + + // DefaultAction applies when no rule matches a connection. + DefaultAction Action + + // RedirectSources are the source CIDRs whose traffic should be intercepted. + // Admin decides: "activate for these users/subnets." + // Used for both kernel TPROXY rules and userspace forwarder source filtering. + RedirectSources []netip.Prefix + // RedirectPorts are the destination ports to intercept. Empty means all ports. + RedirectPorts []uint16 + + // Rules are the proxy inspection/filtering rules, evaluated in Priority order. + Rules []Rule + + // ICAP holds ICAP service configuration. Nil disables ICAP. + ICAP *ICAPConfig + // TLS holds the MITM CA. Nil means no MITM capability (ActionInspect rules ignored). + TLS *TLSConfig + + // Envoy configuration (ModeEnvoy only) + Envoy *EnvoyConfig + + // ListenAddr is the TPROXY listen address for kernel mode. + // Zero value disables the TPROXY listener. + ListenAddr netip.AddrPort + // WGNetwork is the WireGuard overlay network prefix. + // The proxy blocks dialing destinations inside this network. + WGNetwork netip.Prefix + // LocalIPChecker reports whether an IP belongs to the routing peer. + // Used to prevent SSRF to local services. May be nil. + LocalIPChecker LocalIPChecker +} + +// EnvoyConfig holds configuration for the envoy sidecar mode. +type EnvoyConfig struct { + // BinaryPath is the path to the envoy binary. + // Empty means search $PATH for "envoy". + BinaryPath string + // AdminPort is the port for envoy's admin API (health checks, stats). + // Zero means auto-assign. + AdminPort uint16 + // Snippets are user-provided config fragments merged into the generated bootstrap. + Snippets *EnvoySnippets +} + +// EnvoySnippets holds user-provided YAML fragments for envoy config customization. +// Only safe snippet types are allowed: filters (HTTP and network) and clusters +// needed as dependencies for filter services. Listeners and bootstrap overrides +// are not exposed since we manage the listener and bootstrap. +type EnvoySnippets struct { + // HTTPFilters is YAML injected into the HCM filter chain before the router filter. + // Used for ext_authz, rate limiting, Lua, Wasm, RBAC, JWT auth, etc. + HTTPFilters string + // NetworkFilters is YAML injected into the TLS filter chain before tcp_proxy. + // Used for network-level RBAC, rate limiting, ext_authz on raw TCP. + NetworkFilters string + // Clusters is YAML for additional upstream clusters referenced by filters. + // Needed when filters call external services (ext_authz backend, rate limit service). + Clusters string +} diff --git a/client/inspect/config_test.go b/client/inspect/config_test.go new file mode 100644 index 000000000..931014986 --- /dev/null +++ b/client/inspect/config_test.go @@ -0,0 +1,93 @@ +package inspect + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/shared/management/domain" +) + +func TestMatchDomain(t *testing.T) { + tests := []struct { + name string + pattern string + target string + want bool + }{ + { + name: "exact match", + pattern: "example.com", + target: "example.com", + want: true, + }, + { + name: "exact no match", + pattern: "example.com", + target: "other.com", + want: false, + }, + { + name: "wildcard matches subdomain", + pattern: "*.example.com", + target: "foo.example.com", + want: true, + }, + { + name: "wildcard matches deep subdomain", + pattern: "*.example.com", + target: "a.b.c.example.com", + want: true, + }, + { + name: "wildcard does not match base", + pattern: "*.example.com", + target: "example.com", + want: false, + }, + { + name: "wildcard does not match unrelated", + pattern: "*.example.com", + target: "foo.other.com", + want: false, + }, + { + name: "case insensitive exact match", + pattern: "Example.COM", + target: "example.com", + want: true, + }, + { + name: "case insensitive wildcard match", + pattern: "*.Example.COM", + target: "FOO.example.com", + want: true, + }, + { + name: "wildcard does not match partial suffix", + pattern: "*.example.com", + target: "notexample.com", + want: false, + }, + { + name: "unicode domain punycode match", + pattern: "*.münchen.de", + target: "sub.xn--mnchen-3ya.de", + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pattern, err := domain.FromString(tt.pattern) + require.NoError(t, err) + + target, err := domain.FromString(tt.target) + require.NoError(t, err) + + got := MatchDomain(pattern, target) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/client/inspect/dialer_linux.go b/client/inspect/dialer_linux.go new file mode 100644 index 000000000..faacf9eeb --- /dev/null +++ b/client/inspect/dialer_linux.go @@ -0,0 +1,25 @@ +package inspect + +import ( + "net" + "syscall" +) + +// newOutboundDialer creates a net.Dialer that clears the socket fwmark. +// In kernel TPROXY mode, accepted connections inherit the TPROXY fwmark. +// Without clearing it, outbound connections from the proxy would match +// the ip rule (fwmark -> local loopback) and loop back to the proxy +// instead of reaching the real destination. +func newOutboundDialer() net.Dialer { + return net.Dialer{ + Control: func(_, _ string, c syscall.RawConn) error { + var sockErr error + if err := c.Control(func(fd uintptr) { + sockErr = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, 0) + }); err != nil { + return err + } + return sockErr + }, + } +} diff --git a/client/inspect/dialer_other.go b/client/inspect/dialer_other.go new file mode 100644 index 000000000..d2787167b --- /dev/null +++ b/client/inspect/dialer_other.go @@ -0,0 +1,11 @@ +//go:build !linux + +package inspect + +import "net" + +// newOutboundDialer returns a plain dialer on non-Linux platforms. +// TPROXY is Linux-only, so no fwmark clearing is needed. +func newOutboundDialer() net.Dialer { + return net.Dialer{} +} diff --git a/client/inspect/envoy.go b/client/inspect/envoy.go new file mode 100644 index 000000000..b25d05343 --- /dev/null +++ b/client/inspect/envoy.go @@ -0,0 +1,298 @@ +package inspect + +import ( + "context" + "fmt" + "io" + "net" + "net/http" + "net/netip" + "os" + "os/exec" + "path/filepath" + "strings" + "sync" + "time" + + log "github.com/sirupsen/logrus" +) + +const ( + envoyStartTimeout = 15 * time.Second + envoyHealthInterval = 500 * time.Millisecond + envoyStopTimeout = 10 * time.Second + envoyDrainTime = 5 +) + +// envoyManager manages the lifecycle of an envoy sidecar process. +type envoyManager struct { + log *log.Entry + cmd *exec.Cmd + configPath string + listenPort uint16 + adminPort uint16 + cancel context.CancelFunc + + blockPagePath string + + mu sync.Mutex + running bool +} + +// startEnvoy finds the envoy binary, generates config, and spawns the process. +// It blocks until envoy reports healthy or the timeout expires. +func startEnvoy(ctx context.Context, logger *log.Entry, config Config) (*envoyManager, error) { + envCfg := config.Envoy + if envCfg == nil { + return nil, fmt.Errorf("envoy config is nil") + } + + binaryPath, err := findEnvoyBinary(envCfg.BinaryPath) + if err != nil { + return nil, fmt.Errorf("find envoy binary: %w", err) + } + + // Pick admin port + adminPort := envCfg.AdminPort + if adminPort == 0 { + p, err := findFreePort() + if err != nil { + return nil, fmt.Errorf("find free admin port: %w", err) + } + adminPort = p + } + + // Pick listener port + listenPort, err := findFreePort() + if err != nil { + return nil, fmt.Errorf("find free listener port: %w", err) + } + + // Use a private temp directory (0700) to prevent local attackers from + // replacing the config file between write and envoy read. + configDir, err := os.MkdirTemp("", "nb-envoy-*") + if err != nil { + return nil, fmt.Errorf("create envoy config directory: %w", err) + } + + // Write the block page HTML for envoy's direct_response to reference. + blockPagePath := filepath.Join(configDir, "block.html") + blockHTML := fmt.Sprintf(blockPageHTML, "blocked domain", "this domain") + if err := os.WriteFile(blockPagePath, []byte(blockHTML), 0600); err != nil { + return nil, fmt.Errorf("write envoy block page: %w", err) + } + + // Generate config with the block page path embedded. + bootstrap, err := generateBootstrap(config, listenPort, adminPort, blockPagePath) + if err != nil { + return nil, fmt.Errorf("generate envoy bootstrap: %w", err) + } + + configPath := filepath.Join(configDir, "bootstrap.yaml") + if err := os.WriteFile(configPath, bootstrap, 0600); err != nil { + return nil, fmt.Errorf("write envoy config: %w", err) + } + + ctx, cancel := context.WithCancel(ctx) + + cmd := exec.CommandContext(ctx, binaryPath, + "-c", configPath, + "--drain-time-s", fmt.Sprintf("%d", envoyDrainTime), + ) + + // Pipe envoy output to our logger. + cmd.Stdout = &logWriter{entry: logger, level: log.DebugLevel} + cmd.Stderr = &logWriter{entry: logger, level: log.WarnLevel} + + if err := cmd.Start(); err != nil { + cancel() + os.Remove(configPath) + return nil, fmt.Errorf("start envoy: %w", err) + } + + mgr := &envoyManager{ + log: logger, + cmd: cmd, + configPath: configPath, + listenPort: listenPort, + adminPort: adminPort, + blockPagePath: blockPagePath, + cancel: cancel, + running: true, + } + + // Wait for envoy to become healthy. + if err := mgr.waitHealthy(ctx); err != nil { + mgr.Stop() + return nil, fmt.Errorf("wait for envoy readiness: %w", err) + } + + logger.Infof("inspect: envoy started (pid=%d, listen=%d, admin=%d)", cmd.Process.Pid, listenPort, adminPort) + + // Monitor process exit in background. + go mgr.monitor() + + return mgr, nil +} + +// ListenAddr returns the address envoy listens on for forwarded connections. +func (m *envoyManager) ListenAddr() netip.AddrPort { + return netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), m.listenPort) +} + +// AdminAddr returns the envoy admin API address. +func (m *envoyManager) AdminAddr() string { + return fmt.Sprintf("127.0.0.1:%d", m.adminPort) +} + +// Reload writes a new config and sends SIGHUP to envoy. +func (m *envoyManager) Reload(config Config) error { + m.mu.Lock() + defer m.mu.Unlock() + + if !m.running { + return fmt.Errorf("envoy is not running") + } + + bootstrap, err := generateBootstrap(config, m.listenPort, m.adminPort, m.blockPagePath) + if err != nil { + return fmt.Errorf("generate envoy bootstrap: %w", err) + } + + if err := os.WriteFile(m.configPath, bootstrap, 0600); err != nil { + return fmt.Errorf("write envoy config: %w", err) + } + + if err := signalReload(m.cmd.Process); err != nil { + return fmt.Errorf("signal envoy reload: %w", err) + } + + m.log.Debugf("inspect: envoy config reloaded") + return nil +} + +// Healthy checks the envoy admin API /ready endpoint. +func (m *envoyManager) Healthy() bool { + resp, err := http.Get(fmt.Sprintf("http://%s/ready", m.AdminAddr())) + if err != nil { + return false + } + defer resp.Body.Close() + return resp.StatusCode == http.StatusOK +} + +// Stop terminates the envoy process and cleans up. +func (m *envoyManager) Stop() { + m.mu.Lock() + defer m.mu.Unlock() + + if !m.running { + return + } + m.running = false + + m.cancel() + + if m.cmd.Process != nil { + done := make(chan struct{}) + go func() { + m.cmd.Wait() + close(done) + }() + + select { + case <-done: + case <-time.After(envoyStopTimeout): + m.log.Warnf("inspect: envoy did not exit in %s, killing", envoyStopTimeout) + m.cmd.Process.Kill() + <-done + } + } + + os.RemoveAll(filepath.Dir(m.configPath)) + m.log.Infof("inspect: envoy stopped") +} + +// waitHealthy polls the admin API until envoy is ready or timeout. +func (m *envoyManager) waitHealthy(ctx context.Context) error { + deadline := time.After(envoyStartTimeout) + ticker := time.NewTicker(envoyHealthInterval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-deadline: + return fmt.Errorf("envoy not ready after %s", envoyStartTimeout) + case <-ticker.C: + if m.Healthy() { + return nil + } + } + } +} + +// monitor watches for unexpected envoy exits. +func (m *envoyManager) monitor() { + err := m.cmd.Wait() + + m.mu.Lock() + wasRunning := m.running + m.running = false + m.mu.Unlock() + + if wasRunning { + m.log.Errorf("inspect: envoy exited unexpectedly: %v", err) + } +} + +// findEnvoyBinary resolves the envoy binary path. +func findEnvoyBinary(configPath string) (string, error) { + if configPath != "" { + if _, err := os.Stat(configPath); err != nil { + return "", fmt.Errorf("envoy binary not found at %s: %w", configPath, err) + } + return configPath, nil + } + + path, err := exec.LookPath("envoy") + if err != nil { + return "", fmt.Errorf("envoy not found in PATH: %w", err) + } + return path, nil +} + +// findFreePort asks the OS for an available TCP port. +func findFreePort() (uint16, error) { + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return 0, err + } + port := uint16(ln.Addr().(*net.TCPAddr).Port) + ln.Close() + return port, nil +} + +// logWriter adapts log.Entry to io.Writer for piping process output. +type logWriter struct { + entry *log.Entry + level log.Level +} + +func (w *logWriter) Write(p []byte) (int, error) { + msg := strings.TrimRight(string(p), "\n\r") + if msg == "" { + return len(p), nil + } + switch w.level { + case log.WarnLevel: + w.entry.Warn(msg) + default: + w.entry.Debug(msg) + } + return len(p), nil +} + +// Ensure logWriter satisfies io.Writer. +var _ io.Writer = (*logWriter)(nil) diff --git a/client/inspect/envoy_config.go b/client/inspect/envoy_config.go new file mode 100644 index 000000000..2e8ac3a9a --- /dev/null +++ b/client/inspect/envoy_config.go @@ -0,0 +1,382 @@ +package inspect + +import ( + "bytes" + "fmt" + "strings" + "text/template" +) + +// envoyBootstrapTmpl generates the full envoy bootstrap with rule translation. +// TLS rules become per-SNI filter chains; HTTP rules become per-domain virtual hosts. +var envoyBootstrapTmpl = template.Must(template.New("bootstrap").Funcs(template.FuncMap{ + "quote": func(s string) string { return fmt.Sprintf("%q", s) }, +}).Parse(`node: + id: netbird-inspect + cluster: netbird +admin: + address: + socket_address: + address: 127.0.0.1 + port_value: {{.AdminPort}} +static_resources: + listeners: + - name: inspect_listener + address: + socket_address: + address: 127.0.0.1 + port_value: {{.ListenPort}} + listener_filters: + - name: envoy.filters.listener.proxy_protocol + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.listener.proxy_protocol.v3.ProxyProtocol + - name: envoy.filters.listener.tls_inspector + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector + filter_chains: +{{- /* TLS filter chains: per-SNI block/allow + default */ -}} +{{- range .TLSChains}} + - filter_chain_match: + transport_protocol: tls +{{- if .ServerNames}} + server_names: +{{- range .ServerNames}} + - {{quote .}} +{{- end}} +{{- end}} + filters: +{{$.NetworkFiltersSnippet}} - name: envoy.filters.network.tcp_proxy + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy + stat_prefix: {{.StatPrefix}} + cluster: original_dst + access_log: + - name: envoy.access_loggers.stderr + typed_config: + "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StderrAccessLog + log_format: + text_format: "[%START_TIME%] tcp %DOWNSTREAM_REMOTE_ADDRESS% -> %UPSTREAM_HOST% %RESPONSE_FLAGS% %DURATION%ms\n" +{{- end}} +{{- /* Plain HTTP filter chain with per-domain virtual hosts */}} + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: inspect_http + access_log: + - name: envoy.access_loggers.stderr + typed_config: + "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StderrAccessLog + log_format: + text_format: "[%START_TIME%] http %DOWNSTREAM_REMOTE_ADDRESS% %REQ(:AUTHORITY)% %REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %RESPONSE_CODE% %RESPONSE_FLAGS% %DURATION%ms\n" + http_filters: +{{.HTTPFiltersSnippet}} - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + route_config: + virtual_hosts: +{{- range .VirtualHosts}} + - name: {{.Name}} + domains: [{{.DomainsStr}}] + routes: +{{- range .Routes}} + - match: + prefix: "{{if .PathPrefix}}{{.PathPrefix}}{{else}}/{{end}}" +{{- if .Block}} + direct_response: + status: 403 + body: + filename: "{{$.BlockPagePath}}" +{{- else}} + route: + cluster: original_dst +{{- end}} +{{- end}} +{{- end}} + clusters: + - name: original_dst + type: ORIGINAL_DST + lb_policy: CLUSTER_PROVIDED + connect_timeout: 10s +{{.ExtraClusters}}`)) + +// tlsChain represents a TLS filter chain entry for the template. +// All TLS chains are passthrough (block decisions happen in Go before envoy). +type tlsChain struct { + // ServerNames restricts this chain to specific SNIs. Empty is catch-all. + ServerNames []string + StatPrefix string +} + +// envoyRoute represents a single route entry within a virtual host. +type envoyRoute struct { + // PathPrefix for envoy prefix match. Empty means catch-all "/". + PathPrefix string + Block bool +} + +// virtualHost represents an HTTP virtual host entry for the template. +type virtualHost struct { + Name string + // DomainsStr is pre-formatted for the template: "a", "b". + DomainsStr string + Routes []envoyRoute +} + +type bootstrapData struct { + AdminPort uint16 + ListenPort uint16 + BlockPagePath string + TLSChains []tlsChain + VirtualHosts []virtualHost + HTTPFiltersSnippet string + NetworkFiltersSnippet string + ExtraClusters string +} + +// generateBootstrap produces the envoy bootstrap YAML from the inspect config. +// Translates inspection rules into envoy-native per-SNI and per-domain routing. +// blockPagePath is the path to the HTML block page file served by direct_response. +func generateBootstrap(config Config, listenPort, adminPort uint16, blockPagePath string) ([]byte, error) { + data := bootstrapData{ + AdminPort: adminPort, + BlockPagePath: blockPagePath, + ListenPort: listenPort, + TLSChains: buildTLSChains(config), + VirtualHosts: buildVirtualHosts(config), + } + + if config.Envoy != nil && config.Envoy.Snippets != nil { + s := config.Envoy.Snippets + data.HTTPFiltersSnippet = indentSnippet(s.HTTPFilters, 18) + data.NetworkFiltersSnippet = indentSnippet(s.NetworkFilters, 12) + data.ExtraClusters = indentSnippet(s.Clusters, 4) + } + + var buf bytes.Buffer + if err := envoyBootstrapTmpl.Execute(&buf, data); err != nil { + return nil, fmt.Errorf("execute bootstrap template: %w", err) + } + + return buf.Bytes(), nil +} + +// buildTLSChains translates inspection rules into envoy TLS filter chains. +// Block rules -> per-SNI chain routing to blackhole. +// Allow rules (when default=block) -> per-SNI chain routing to original_dst. +// Default chain follows DefaultAction. +func buildTLSChains(config Config) []tlsChain { + // TLS block decisions happen in Go before forwarding to envoy, so we only + // generate allow/passthrough chains here. Envoy can't cleanly close a TLS + // connection without completing a handshake, so blocked SNIs never reach envoy. + var allowed []string + + for _, rule := range config.Rules { + if !ruleTouchesProtocol(rule, ProtoHTTPS, ProtoH2) { + continue + } + for _, d := range rule.Domains { + sni := d.PunycodeString() + if rule.Action == ActionAllow || rule.Action == ActionInspect { + allowed = append(allowed, sni) + } + } + } + + var chains []tlsChain + + if len(allowed) > 0 && config.DefaultAction == ActionBlock { + chains = append(chains, tlsChain{ + ServerNames: allowed, + StatPrefix: "tls_allowed", + }) + } + + // Default catch-all: passthrough (blocked SNIs never arrive here) + chains = append(chains, tlsChain{ + StatPrefix: "tls_default", + }) + + return chains +} + +// buildVirtualHosts translates inspection rules into envoy HTTP virtual hosts. +// Groups rules by domain, generates per-path routes within each virtual host. +func buildVirtualHosts(config Config) []virtualHost { + // Group rules by domain for per-domain virtual hosts. + type domainRules struct { + domains []string + routes []envoyRoute + } + + domainRouteMap := make(map[string][]envoyRoute) + + for _, rule := range config.Rules { + if !ruleTouchesProtocol(rule, ProtoHTTP, ProtoWebSocket) { + continue + } + isBlock := rule.Action == ActionBlock + + // Rules without domains or paths are handled by the default action. + if len(rule.Domains) == 0 && len(rule.Paths) == 0 { + continue + } + + // Build routes for this rule's paths + var routes []envoyRoute + if len(rule.Paths) > 0 { + for _, p := range rule.Paths { + // Convert our path patterns to envoy prefix match. + // Strip trailing * for envoy prefix matching. + prefix := strings.TrimSuffix(p, "*") + routes = append(routes, envoyRoute{PathPrefix: prefix, Block: isBlock}) + } + } else { + routes = append(routes, envoyRoute{Block: isBlock}) + } + + if len(rule.Domains) > 0 { + for _, d := range rule.Domains { + host := d.PunycodeString() + domainRouteMap[host] = append(domainRouteMap[host], routes...) + } + } else { + // No domain: applies to all, add to default host + domainRouteMap["*"] = append(domainRouteMap["*"], routes...) + } + } + + var hosts []virtualHost + idx := 0 + + // Per-domain virtual hosts with path routes + for domain, routes := range domainRouteMap { + if domain == "*" { + continue + } + // Add a catch-all route after path-specific routes. + // The catch-all follows the default action. + routes = append(routes, envoyRoute{Block: config.DefaultAction == ActionBlock}) + + hosts = append(hosts, virtualHost{ + Name: fmt.Sprintf("domain_%d", idx), + DomainsStr: fmt.Sprintf("%q", domain), + Routes: routes, + }) + idx++ + } + + // Default virtual host (catch-all for unmatched domains) + defaultRoutes := domainRouteMap["*"] + defaultRoutes = append(defaultRoutes, envoyRoute{Block: config.DefaultAction == ActionBlock}) + hosts = append(hosts, virtualHost{ + Name: "default", + DomainsStr: `"*"`, + Routes: defaultRoutes, + }) + + return hosts +} + +// ruleTouchesProtocol returns true if the rule's protocol list includes any of the given protocols, +// or if the protocol list is empty (matches all). +func ruleTouchesProtocol(rule Rule, protos ...ProtoType) bool { + if len(rule.Protocols) == 0 { + return true + } + for _, rp := range rule.Protocols { + for _, p := range protos { + if rp == p { + return true + } + } + } + return false +} + +// indentSnippet prepends each line of the YAML snippet with the given number of spaces. +// Returns empty string if snippet is empty. +func indentSnippet(snippet string, spaces int) string { + if snippet == "" { + return "" + } + + prefix := make([]byte, spaces) + for i := range prefix { + prefix[i] = ' ' + } + + var buf bytes.Buffer + for i, line := range bytes.Split([]byte(snippet), []byte("\n")) { + if i > 0 { + buf.WriteByte('\n') + } + if len(line) > 0 { + buf.Write(prefix) + buf.Write(line) + } + } + buf.WriteByte('\n') + + return buf.String() +} + +// ValidateSnippets checks that user-provided snippets are safe to inject +// into the envoy config. Returns an error describing the first violation found. +// +// Validation rules: +// - Each snippet must be valid YAML (prevents syntax-level injection) +// - Snippets must not contain YAML document separators (--- or ...) that could +// break out of the indentation context +// - Snippets must only contain list items (starting with "- ") at the top level, +// matching what envoy expects for filters and clusters +func ValidateSnippets(snippets *EnvoySnippets) error { + if snippets == nil { + return nil + } + + fields := []struct { + name string + value string + }{ + {"http_filters", snippets.HTTPFilters}, + {"network_filters", snippets.NetworkFilters}, + {"clusters", snippets.Clusters}, + } + + for _, f := range fields { + if f.value == "" { + continue + } + if err := validateSnippetYAML(f.name, f.value); err != nil { + return err + } + } + + return nil +} + +func validateSnippetYAML(name, snippet string) error { + // Check for YAML document markers that could break template structure. + for _, line := range strings.Split(snippet, "\n") { + trimmed := strings.TrimSpace(line) + if trimmed == "---" || trimmed == "..." { + return fmt.Errorf("snippet %q: YAML document separators (--- or ...) are not allowed", name) + } + } + + // Verify it's valid YAML by checking it doesn't cause template execution issues. + // We can't import yaml.v3 here without adding a dependency, so we do structural checks. + + // Check for null bytes or control characters that could confuse YAML parsers. + for i, b := range []byte(snippet) { + if b == 0 { + return fmt.Errorf("snippet %q: null byte at position %d", name, i) + } + if b < 0x09 || (b > 0x0D && b < 0x20 && b != 0x1B) { + return fmt.Errorf("snippet %q: control character 0x%02x at position %d", name, b, i) + } + } + + return nil +} diff --git a/client/inspect/envoy_forward.go b/client/inspect/envoy_forward.go new file mode 100644 index 000000000..0516e2a2c --- /dev/null +++ b/client/inspect/envoy_forward.go @@ -0,0 +1,88 @@ +package inspect + +import ( + "context" + "encoding/binary" + "fmt" + "net" + "net/netip" +) + +// PROXY protocol v2 constants (RFC 7239 / HAProxy spec) +var proxyV2Signature = [12]byte{ + 0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, + 0x55, 0x49, 0x54, 0x0A, +} + +const ( + proxyV2VersionCommand = 0x21 // version 2, PROXY command + proxyV2FamilyTCP4 = 0x11 // AF_INET, STREAM + proxyV2FamilyTCP6 = 0x21 // AF_INET6, STREAM +) + +// forwardToEnvoy forwards a connection to the given envoy sidecar via PROXY protocol v2. +// The caller provides the envoy manager snapshot to avoid accessing p.envoy without lock. +func (p *Proxy) forwardToEnvoy(ctx context.Context, pconn *peekConn, dst netip.AddrPort, src SourceInfo, em *envoyManager) error { + envoyAddr := em.ListenAddr() + + conn, err := (&net.Dialer{}).DialContext(ctx, "tcp", envoyAddr.String()) + if err != nil { + return fmt.Errorf("dial envoy at %s: %w", envoyAddr, err) + } + defer func() { + if err := conn.Close(); err != nil { + p.log.Debugf("close envoy conn: %v", err) + } + }() + + if err := writeProxyV2Header(conn, src.IP, dst); err != nil { + return fmt.Errorf("write PROXY v2 header: %w", err) + } + + p.log.Tracef("envoy: forwarded %s -> %s via PROXY v2", src.IP, dst) + + return relay(ctx, pconn, conn) +} + +// writeProxyV2Header writes a PROXY protocol v2 header to w. +// The header encodes the original source IP and the destination address:port. +func writeProxyV2Header(w net.Conn, srcIP netip.Addr, dst netip.AddrPort) error { + srcIP = srcIP.Unmap() + dstIP := dst.Addr().Unmap() + + var ( + family byte + addrs []byte + ) + + if srcIP.Is4() && dstIP.Is4() { + family = proxyV2FamilyTCP4 + s4 := srcIP.As4() + d4 := dstIP.As4() + addrs = make([]byte, 12) // 4+4+2+2 + copy(addrs[0:4], s4[:]) + copy(addrs[4:8], d4[:]) + binary.BigEndian.PutUint16(addrs[8:10], 0) // src port unknown + binary.BigEndian.PutUint16(addrs[10:12], dst.Port()) + } else { + family = proxyV2FamilyTCP6 + s16 := srcIP.As16() + d16 := dstIP.As16() + addrs = make([]byte, 36) // 16+16+2+2 + copy(addrs[0:16], s16[:]) + copy(addrs[16:32], d16[:]) + binary.BigEndian.PutUint16(addrs[32:34], 0) // src port unknown + binary.BigEndian.PutUint16(addrs[34:36], dst.Port()) + } + + // Header: signature(12) + ver_cmd(1) + family(1) + len(2) + addrs + header := make([]byte, 16+len(addrs)) + copy(header[0:12], proxyV2Signature[:]) + header[12] = proxyV2VersionCommand + header[13] = family + binary.BigEndian.PutUint16(header[14:16], uint16(len(addrs))) + copy(header[16:], addrs) + + _, err := w.Write(header) + return err +} diff --git a/client/inspect/envoy_signal.go b/client/inspect/envoy_signal.go new file mode 100644 index 000000000..05e8aad13 --- /dev/null +++ b/client/inspect/envoy_signal.go @@ -0,0 +1,13 @@ +//go:build !windows + +package inspect + +import ( + "os" + "syscall" +) + +// signalReload sends SIGHUP to the envoy process to trigger config reload. +func signalReload(p *os.Process) error { + return p.Signal(syscall.SIGHUP) +} diff --git a/client/inspect/envoy_signal_windows.go b/client/inspect/envoy_signal_windows.go new file mode 100644 index 000000000..e984d45a2 --- /dev/null +++ b/client/inspect/envoy_signal_windows.go @@ -0,0 +1,13 @@ +//go:build windows + +package inspect + +import ( + "fmt" + "os" +) + +// signalReload is not supported on Windows. Envoy must be restarted. +func signalReload(_ *os.Process) error { + return fmt.Errorf("envoy config reload via signal not supported on Windows") +} diff --git a/client/inspect/external.go b/client/inspect/external.go new file mode 100644 index 000000000..0e21775c4 --- /dev/null +++ b/client/inspect/external.go @@ -0,0 +1,229 @@ +package inspect + +import ( + "bufio" + "context" + "encoding/base64" + "fmt" + "io" + "net" + "net/http" + "net/netip" + "net/url" + "time" +) + +const ( + externalDialTimeout = 10 * time.Second +) + +// handleExternal forwards the connection to an external proxy. +// For TLS connections, it uses HTTP CONNECT to tunnel through the proxy. +// For HTTP connections, it rewrites the request to use the proxy. +func (p *Proxy) handleExternal(ctx context.Context, pconn *peekConn, dst netip.AddrPort) error { + p.mu.RLock() + proxyURL := p.config.ExternalURL + p.mu.RUnlock() + + if proxyURL == nil { + return fmt.Errorf("external proxy URL not configured") + } + + switch proxyURL.Scheme { + case "http", "https": + return p.externalHTTPProxy(ctx, pconn, dst, proxyURL) + case "socks5": + return p.externalSOCKS5(ctx, pconn, dst, proxyURL) + default: + return fmt.Errorf("unsupported external proxy scheme: %s", proxyURL.Scheme) + } +} + +// externalHTTPProxy tunnels through an HTTP proxy using CONNECT. +func (p *Proxy) externalHTTPProxy(ctx context.Context, pconn *peekConn, dst netip.AddrPort, proxyURL *url.URL) error { + proxyAddr := proxyURL.Host + if _, _, err := net.SplitHostPort(proxyAddr); err != nil { + proxyAddr = net.JoinHostPort(proxyAddr, "8080") + } + + proxyConn, err := (&net.Dialer{Timeout: externalDialTimeout}).DialContext(ctx, "tcp", proxyAddr) + if err != nil { + return fmt.Errorf("dial external proxy %s: %w", proxyAddr, err) + } + defer func() { + if err := proxyConn.Close(); err != nil { + p.log.Debugf("close external proxy conn: %v", err) + } + }() + + connectReq := fmt.Sprintf("CONNECT %s HTTP/1.1\r\nHost: %s\r\n", dst.String(), dst.String()) + if proxyURL.User != nil { + connectReq += "Proxy-Authorization: Basic " + basicAuth(proxyURL.User) + "\r\n" + } + connectReq += "\r\n" + + if _, err := io.WriteString(proxyConn, connectReq); err != nil { + return fmt.Errorf("send CONNECT to proxy: %w", err) + } + + resp, err := http.ReadResponse(bufio.NewReader(proxyConn), nil) + if err != nil { + return fmt.Errorf("read CONNECT response: %w", err) + } + if err := resp.Body.Close(); err != nil { + p.log.Debugf("close CONNECT resp body: %v", err) + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("proxy CONNECT failed: %s", resp.Status) + } + + return relay(ctx, pconn, proxyConn) +} + +// externalSOCKS5 tunnels through a SOCKS5 proxy. +func (p *Proxy) externalSOCKS5(ctx context.Context, pconn *peekConn, dst netip.AddrPort, proxyURL *url.URL) error { + proxyAddr := proxyURL.Host + if _, _, err := net.SplitHostPort(proxyAddr); err != nil { + proxyAddr = net.JoinHostPort(proxyAddr, "1080") + } + + proxyConn, err := (&net.Dialer{Timeout: externalDialTimeout}).DialContext(ctx, "tcp", proxyAddr) + if err != nil { + return fmt.Errorf("dial SOCKS5 proxy %s: %w", proxyAddr, err) + } + defer func() { + if err := proxyConn.Close(); err != nil { + p.log.Debugf("close SOCKS5 proxy conn: %v", err) + } + }() + + if err := socks5Handshake(proxyConn, dst, proxyURL.User); err != nil { + return fmt.Errorf("SOCKS5 handshake: %w", err) + } + + return relay(ctx, pconn, proxyConn) +} + +// socks5Handshake performs the SOCKS5 handshake to connect through the proxy. +func socks5Handshake(conn net.Conn, dst netip.AddrPort, userinfo *url.Userinfo) error { + needAuth := userinfo != nil + + // Greeting + var methods []byte + if needAuth { + methods = []byte{0x00, 0x02} // no auth, username/password + } else { + methods = []byte{0x00} // no auth + } + greeting := append([]byte{0x05, byte(len(methods))}, methods...) + if _, err := conn.Write(greeting); err != nil { + return fmt.Errorf("send greeting: %w", err) + } + + // Server method selection + var methodResp [2]byte + if _, err := io.ReadFull(conn, methodResp[:]); err != nil { + return fmt.Errorf("read method selection: %w", err) + } + if methodResp[0] != 0x05 { + return fmt.Errorf("unexpected SOCKS version: %d", methodResp[0]) + } + + // Handle authentication if selected + if methodResp[1] == 0x02 { + if err := socks5Auth(conn, userinfo); err != nil { + return err + } + } else if methodResp[1] != 0x00 { + return fmt.Errorf("unsupported SOCKS5 auth method: %d", methodResp[1]) + } + + // Connection request + addr := dst.Addr() + var addrBytes []byte + if addr.Is4() { + a4 := addr.As4() + addrBytes = append([]byte{0x01}, a4[:]...) // IPv4 + } else { + a16 := addr.As16() + addrBytes = append([]byte{0x04}, a16[:]...) // IPv6 + } + + port := dst.Port() + connectReq := append([]byte{0x05, 0x01, 0x00}, addrBytes...) + connectReq = append(connectReq, byte(port>>8), byte(port)) + + if _, err := conn.Write(connectReq); err != nil { + return fmt.Errorf("send connect request: %w", err) + } + + // Read response (minimum 10 bytes for IPv4) + var respHeader [4]byte + if _, err := io.ReadFull(conn, respHeader[:]); err != nil { + return fmt.Errorf("read connect response: %w", err) + } + if respHeader[1] != 0x00 { + return fmt.Errorf("SOCKS5 connect failed: status %d", respHeader[1]) + } + + // Skip bound address + switch respHeader[3] { + case 0x01: // IPv4 + var skip [4 + 2]byte + if _, err := io.ReadFull(conn, skip[:]); err != nil { + return fmt.Errorf("read SOCKS5 bound IPv4 address: %w", err) + } + case 0x04: // IPv6 + var skip [16 + 2]byte + if _, err := io.ReadFull(conn, skip[:]); err != nil { + return fmt.Errorf("read SOCKS5 bound IPv6 address: %w", err) + } + case 0x03: // Domain + var dLen [1]byte + if _, err := io.ReadFull(conn, dLen[:]); err != nil { + return fmt.Errorf("read domain length: %w", err) + } + skip := make([]byte, int(dLen[0])+2) + if _, err := io.ReadFull(conn, skip); err != nil { + return fmt.Errorf("read SOCKS5 bound domain address: %w", err) + } + } + + return nil +} + +func socks5Auth(conn net.Conn, userinfo *url.Userinfo) error { + if userinfo == nil { + return fmt.Errorf("SOCKS5 auth required but no credentials provided") + } + + user := userinfo.Username() + pass, _ := userinfo.Password() + + // Username/password auth (RFC 1929) + auth := []byte{0x01, byte(len(user))} + auth = append(auth, []byte(user)...) + auth = append(auth, byte(len(pass))) + auth = append(auth, []byte(pass)...) + + if _, err := conn.Write(auth); err != nil { + return fmt.Errorf("send auth: %w", err) + } + + var resp [2]byte + if _, err := io.ReadFull(conn, resp[:]); err != nil { + return fmt.Errorf("read auth response: %w", err) + } + if resp[1] != 0x00 { + return fmt.Errorf("SOCKS5 auth failed: status %d", resp[1]) + } + + return nil +} + +func basicAuth(userinfo *url.Userinfo) string { + user := userinfo.Username() + pass, _ := userinfo.Password() + return base64.StdEncoding.EncodeToString([]byte(user + ":" + pass)) +} diff --git a/client/inspect/http.go b/client/inspect/http.go new file mode 100644 index 000000000..c3c72c1c5 --- /dev/null +++ b/client/inspect/http.go @@ -0,0 +1,532 @@ +package inspect + +import ( + "bufio" + "context" + "fmt" + "io" + "net" + "net/http" + "net/netip" + "strings" + "sync" + "time" + + "github.com/netbirdio/netbird/shared/management/domain" +) + +const ( + headerUpgrade = "Upgrade" + valueWebSocket = "websocket" +) + +// inspectHTTP runs the HTTP inspection pipeline on decrypted traffic. +// It handles HTTP/1.1 (request-response loop), HTTP/2 (via Go stdlib reverse proxy), +// and WebSocket upgrade detection. +func (p *Proxy) inspectHTTP(ctx context.Context, client, remote net.Conn, dst netip.AddrPort, sni domain.Domain, src SourceInfo, proto string) error { + if proto == "h2" { + return p.inspectH2(ctx, client, remote, dst, sni, src) + } + return p.inspectH1(ctx, client, remote, dst, sni, src) +} + +// inspectH1 handles HTTP/1.1 request-response inspection in a loop. +func (p *Proxy) inspectH1(ctx context.Context, client, remote net.Conn, dst netip.AddrPort, sni domain.Domain, src SourceInfo) error { + clientReader := bufio.NewReader(client) + remoteReader := bufio.NewReader(remote) + + for { + if ctx.Err() != nil { + return ctx.Err() + } + + // Set idle timeout between requests to prevent connection hogging. + if err := client.SetReadDeadline(time.Now().Add(idleTimeout)); err != nil { + return fmt.Errorf("set idle deadline: %w", err) + } + req, err := http.ReadRequest(clientReader) + if err != nil { + if isClosedErr(err) { + return nil + } + return fmt.Errorf("read HTTP request: %w", err) + } + if err := client.SetReadDeadline(time.Time{}); err != nil { + return fmt.Errorf("clear read deadline: %w", err) + } + + // Re-evaluate rules based on Host header if SNI was empty + host := hostFromRequest(req, sni) + + // Domain fronting: Host header doesn't match TLS SNI + if isDomainFronting(req, sni) { + p.log.Debugf("domain fronting detected: SNI=%s Host=%s", sni.PunycodeString(), host.PunycodeString()) + writeBlockResponse(client, req, host) + return ErrBlocked + } + + proto := ProtoHTTP + if isWebSocketUpgrade(req) { + proto = ProtoWebSocket + } + action := p.evaluateAction(src.IP, host, dst, proto, req.URL.Path) + if action == ActionBlock { + p.log.Debugf("block: HTTP %s %s (host=%s)", req.Method, req.URL.Path, host.PunycodeString()) + writeBlockResponse(client, req, host) + return ErrBlocked + } + p.log.Tracef("allow: HTTP %s %s (host=%s, action=%s)", req.Method, req.URL.Path, host.PunycodeString(), action) + + // ICAP REQMOD: send request for inspection. + // Snapshot ICAP client under lock to avoid use-after-close races. + p.mu.RLock() + icap := p.icap + p.mu.RUnlock() + if icap != nil { + modified, err := icap.ReqMod(req) + if err != nil { + p.log.Debugf("ICAP REQMOD error for %s: %v", host.PunycodeString(), err) + // Fail-closed: block on ICAP error + writeBlockResponse(client, req, host) + return fmt.Errorf("ICAP REQMOD: %w", err) + } + req = modified + } + + if isWebSocketUpgrade(req) { + return p.handleWebSocket(ctx, req, client, clientReader, remote, remoteReader) + } + + removeHopByHopHeaders(req.Header) + + if err := req.Write(remote); err != nil { + return fmt.Errorf("forward request: %w", err) + } + + resp, err := http.ReadResponse(remoteReader, req) + if err != nil { + return fmt.Errorf("read HTTP response: %w", err) + } + + // ICAP RESPMOD: send response for inspection + if icap != nil { + modified, err := icap.RespMod(req, resp) + if err != nil { + p.log.Debugf("ICAP RESPMOD error for %s: %v", host.PunycodeString(), err) + if err := resp.Body.Close(); err != nil { + p.log.Debugf("close resp body: %v", err) + } + writeBlockResponse(client, req, host) + return fmt.Errorf("ICAP RESPMOD: %w", err) + } + resp = modified + } + + removeHopByHopHeaders(resp.Header) + + if err := resp.Write(client); err != nil { + if closeErr := resp.Body.Close(); closeErr != nil { + p.log.Debugf("close resp body: %v", closeErr) + } + return fmt.Errorf("forward response: %w", err) + } + if err := resp.Body.Close(); err != nil { + p.log.Debugf("close resp body: %v", err) + } + + // Connection: close means we're done + if resp.Close || req.Close { + return nil + } + } +} + +// inspectH2 proxies HTTP/2 traffic using Go's http stack. +// Client and remote are already-established TLS connections with h2 negotiated. +func (p *Proxy) inspectH2(ctx context.Context, client, remote net.Conn, dst netip.AddrPort, sni domain.Domain, src SourceInfo) error { + // For h2 MITM inspection, we use a local http.Server reading from the client + // connection and an http.Transport writing to the remote connection. + // + // The transport is configured to use the existing TLS connection to the + // real server. The handler inspects each request/response pair. + + transport := &http.Transport{ + DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { + return remote, nil + }, + DialTLSContext: func(_ context.Context, _, _ string) (net.Conn, error) { + return remote, nil + }, + ForceAttemptHTTP2: true, + } + + handler := &h2InspectionHandler{ + proxy: p, + transport: transport, + dst: dst, + sni: sni, + src: src, + } + + server := &http.Server{ + Handler: handler, + } + + // Serve the single client connection. + // ServeConn blocks until the connection is done. + errCh := make(chan error, 1) + go func() { + // http.Server doesn't have a direct ServeConn for h2, + // so we use Serve with a single-connection listener. + ln := &singleConnListener{conn: client} + errCh <- server.Serve(ln) + }() + + select { + case <-ctx.Done(): + if err := server.Close(); err != nil { + p.log.Debugf("close h2 server: %v", err) + } + return ctx.Err() + case err := <-errCh: + if err == http.ErrServerClosed { + return nil + } + return err + } +} + +// h2InspectionHandler inspects each HTTP/2 request/response pair. +type h2InspectionHandler struct { + proxy *Proxy + transport http.RoundTripper + dst netip.AddrPort + sni domain.Domain + src SourceInfo +} + +func (h *h2InspectionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + host := hostFromRequest(req, h.sni) + + if isDomainFronting(req, h.sni) { + h.proxy.log.Debugf("domain fronting detected: SNI=%s Host=%s", h.sni.PunycodeString(), host.PunycodeString()) + writeBlockPage(w, host) + return + } + + action := h.proxy.evaluateAction(h.src.IP, host, h.dst, ProtoH2, req.URL.Path) + if action == ActionBlock { + h.proxy.log.Debugf("block: H2 %s %s (host=%s)", req.Method, req.URL.Path, host.PunycodeString()) + writeBlockPage(w, host) + return + } + + // ICAP REQMOD + if h.proxy.icap != nil { + modified, err := h.proxy.icap.ReqMod(req) + if err != nil { + h.proxy.log.Debugf("ICAP REQMOD error for %s: %v", host.PunycodeString(), err) + writeBlockPage(w, host) + return + } + req = modified + } + + // Forward to upstream + req.URL.Scheme = "https" + req.URL.Host = h.sni.PunycodeString() + req.RequestURI = "" + + resp, err := h.transport.RoundTrip(req) + if err != nil { + h.proxy.log.Debugf("h2 upstream error for %s: %v", host.PunycodeString(), err) + http.Error(w, "Bad Gateway", http.StatusBadGateway) + return + } + defer func() { + if err := resp.Body.Close(); err != nil { + h.proxy.log.Debugf("close h2 resp body: %v", err) + } + }() + + // ICAP RESPMOD + if h.proxy.icap != nil { + modified, err := h.proxy.icap.RespMod(req, resp) + if err != nil { + h.proxy.log.Debugf("ICAP RESPMOD error for %s: %v", host.PunycodeString(), err) + writeBlockPage(w, host) + return + } + resp = modified + } + + // Copy response headers and body + for k, vals := range resp.Header { + for _, v := range vals { + w.Header().Add(k, v) + } + } + w.WriteHeader(resp.StatusCode) + if _, err := io.Copy(w, resp.Body); err != nil { + h.proxy.log.Debugf("h2 response copy error: %v", err) + } +} + +// handleWebSocket completes the WebSocket upgrade and relays frames bidirectionally. +func (p *Proxy) handleWebSocket(ctx context.Context, req *http.Request, client io.ReadWriter, clientReader *bufio.Reader, remote io.ReadWriter, remoteReader *bufio.Reader) error { + if err := req.Write(remote); err != nil { + return fmt.Errorf("forward WebSocket upgrade: %w", err) + } + + resp, err := http.ReadResponse(remoteReader, req) + if err != nil { + return fmt.Errorf("read WebSocket upgrade response: %w", err) + } + + if err := resp.Write(client); err != nil { + if closeErr := resp.Body.Close(); closeErr != nil { + p.log.Debugf("close ws resp body: %v", closeErr) + } + return fmt.Errorf("forward WebSocket upgrade response: %w", err) + } + if err := resp.Body.Close(); err != nil { + p.log.Debugf("close ws resp body: %v", err) + } + + if resp.StatusCode != http.StatusSwitchingProtocols { + return fmt.Errorf("WebSocket upgrade rejected: status %d", resp.StatusCode) + } + + p.log.Tracef("allow: WebSocket upgrade for %s", req.Host) + + // Relay WebSocket frames bidirectionally. + // clientReader/remoteReader may have buffered data. + clientConn := mergeReadWriter(clientReader, client) + remoteConn := mergeReadWriter(remoteReader, remote) + + return relayRW(ctx, clientConn, remoteConn) +} + +// hostFromRequest extracts a domain.Domain from the HTTP request Host header, +// falling back to the SNI if Host is empty or an IP. +func hostFromRequest(req *http.Request, fallback domain.Domain) domain.Domain { + host := req.Host + if host == "" { + return fallback + } + + // Strip port if present + if h, _, err := net.SplitHostPort(host); err == nil { + host = h + } + + // If it's an IP address, use the SNI fallback + if _, err := netip.ParseAddr(host); err == nil { + return fallback + } + + d, err := domain.FromString(host) + if err != nil { + return fallback + } + return d +} + +// isDomainFronting detects domain fronting: the Host header doesn't match the +// SNI used during the TLS handshake. Only meaningful when SNI is non-empty +// (i.e., we're in MITM mode and know the original SNI). +func isDomainFronting(req *http.Request, sni domain.Domain) bool { + if sni == "" { + return false + } + + host := hostFromRequest(req, "") + if host == "" { + return false + } + + // Host should match SNI or be a subdomain of SNI + if host == sni { + return false + } + + // Allow www.example.com when SNI is example.com + sniStr := sni.PunycodeString() + hostStr := host.PunycodeString() + if strings.HasSuffix(hostStr, "."+sniStr) { + return false + } + + return true +} + +func isWebSocketUpgrade(req *http.Request) bool { + return strings.EqualFold(req.Header.Get(headerUpgrade), valueWebSocket) +} + +// writeBlockPage writes the styled HTML block page to an http.ResponseWriter (H2 path). +func writeBlockPage(w http.ResponseWriter, host domain.Domain) { + hostname := host.PunycodeString() + body := fmt.Sprintf(blockPageHTML, hostname, hostname) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Header().Set("Cache-Control", "no-store") + w.WriteHeader(http.StatusForbidden) + io.WriteString(w, body) +} + +func writeBlockResponse(w io.Writer, _ *http.Request, host domain.Domain) { + hostname := host.PunycodeString() + body := fmt.Sprintf(blockPageHTML, hostname, hostname) + + resp := &http.Response{ + StatusCode: http.StatusForbidden, + ProtoMajor: 1, + ProtoMinor: 1, + Header: make(http.Header), + ContentLength: int64(len(body)), + Body: io.NopCloser(strings.NewReader(body)), + } + resp.Header.Set("Content-Type", "text/html; charset=utf-8") + resp.Header.Set("Connection", "close") + resp.Header.Set("Cache-Control", "no-store") + _ = resp.Write(w) +} + +// blockPageHTML is the self-contained HTML block page. +// Uses NetBird dark theme with orange accent. Two format args: page title domain, displayed domain. +const blockPageHTML = ` + + + + +Blocked - %s + + + +
+
+
403 BLOCKED
+

Access Denied

+

This connection to %s has been blocked by your organization's network policy.

+ +
+ +` + +// singleConnListener is a net.Listener that yields a single connection. +type singleConnListener struct { + conn net.Conn + once sync.Once + ch chan struct{} +} + +func (l *singleConnListener) Accept() (net.Conn, error) { + var accepted bool + l.once.Do(func() { + l.ch = make(chan struct{}) + accepted = true + }) + if accepted { + return l.conn, nil + } + // Block until Close + <-l.ch + return nil, net.ErrClosed +} + +func (l *singleConnListener) Close() error { + l.once.Do(func() { + l.ch = make(chan struct{}) + }) + select { + case <-l.ch: + default: + close(l.ch) + } + return nil +} + +func (l *singleConnListener) Addr() net.Addr { + return l.conn.LocalAddr() +} + +type readWriter struct { + io.Reader + io.Writer +} + +func mergeReadWriter(r io.Reader, w io.Writer) io.ReadWriter { + return &readWriter{Reader: r, Writer: w} +} + +// relayRW copies data bidirectionally between two ReadWriters. +func relayRW(ctx context.Context, a, b io.ReadWriter) error { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + errCh := make(chan error, 2) + + go func() { + _, err := io.Copy(b, a) + cancel() + errCh <- err + }() + + go func() { + _, err := io.Copy(a, b) + cancel() + errCh <- err + }() + + var firstErr error + for range 2 { + if err := <-errCh; err != nil && firstErr == nil { + if !isClosedErr(err) { + firstErr = err + } + } + } + + return firstErr +} + +// hopByHopHeaders are HTTP/1.1 headers that apply to a single connection +// and must not be forwarded by a proxy (RFC 7230, Section 6.1). +var hopByHopHeaders = []string{ + "Connection", + "Keep-Alive", + "Proxy-Authenticate", + "Proxy-Authorization", + "TE", + "Trailers", + "Transfer-Encoding", + "Upgrade", +} + +// removeHopByHopHeaders strips hop-by-hop headers from h. +// Also removes headers listed in the Connection header value. +func removeHopByHopHeaders(h http.Header) { + // First, remove any headers named in the Connection header + for _, connHeader := range h["Connection"] { + for _, name := range strings.Split(connHeader, ",") { + h.Del(strings.TrimSpace(name)) + } + } + + for _, name := range hopByHopHeaders { + h.Del(name) + } +} diff --git a/client/inspect/icap.go b/client/inspect/icap.go new file mode 100644 index 000000000..91cf67a4f --- /dev/null +++ b/client/inspect/icap.go @@ -0,0 +1,479 @@ +package inspect + +import ( + "bufio" + "bytes" + "fmt" + "io" + "net" + "net/http" + "net/textproto" + "net/url" + "strconv" + "strings" + "sync" + "time" + + log "github.com/sirupsen/logrus" +) + +const ( + icapVersion = "ICAP/1.0" + icapDefaultPort = "1344" + icapConnTimeout = 30 * time.Second + icapRWTimeout = 60 * time.Second + icapMaxPoolSize = 8 + icapIdleTimeout = 60 * time.Second + icapMaxRespSize = 4 * 1024 * 1024 // 4 MB +) + +// ICAPClient implements an ICAP (RFC 3507) client with persistent connection pooling. +type ICAPClient struct { + reqModURL *url.URL + respModURL *url.URL + pool chan *icapConn + mu sync.Mutex + log *log.Entry + maxPool int +} + +type icapConn struct { + conn net.Conn + reader *bufio.Reader + lastUse time.Time +} + +// NewICAPClient creates an ICAP client. Either or both URLs may be nil +// to disable that mode. +func NewICAPClient(logger *log.Entry, cfg *ICAPConfig) *ICAPClient { + maxPool := cfg.MaxConnections + if maxPool <= 0 { + maxPool = icapMaxPoolSize + } + + return &ICAPClient{ + reqModURL: cfg.ReqModURL, + respModURL: cfg.RespModURL, + pool: make(chan *icapConn, maxPool), + log: logger, + maxPool: maxPool, + } +} + +// ReqMod sends an HTTP request to the ICAP REQMOD service for inspection. +// Returns the (possibly modified) request, or the original if ICAP returns 204. +// Returns nil, nil if REQMOD is not configured. +func (c *ICAPClient) ReqMod(req *http.Request) (*http.Request, error) { + if c.reqModURL == nil { + return req, nil + } + + var reqBuf bytes.Buffer + if err := req.Write(&reqBuf); err != nil { + return nil, fmt.Errorf("serialize request: %w", err) + } + + respBody, err := c.send("REQMOD", c.reqModURL, reqBuf.Bytes(), nil) + if err != nil { + return nil, err + } + + if respBody == nil { + return req, nil + } + + modified, err := http.ReadRequest(bufio.NewReader(bytes.NewReader(respBody))) + if err != nil { + return nil, fmt.Errorf("parse ICAP modified request: %w", err) + } + return modified, nil +} + +// RespMod sends an HTTP response to the ICAP RESPMOD service for inspection. +// Returns the (possibly modified) response, or the original if ICAP returns 204. +// Returns nil, nil if RESPMOD is not configured. +func (c *ICAPClient) RespMod(req *http.Request, resp *http.Response) (*http.Response, error) { + if c.respModURL == nil { + return resp, nil + } + + var reqBuf bytes.Buffer + if err := req.Write(&reqBuf); err != nil { + return nil, fmt.Errorf("serialize request: %w", err) + } + + var respBuf bytes.Buffer + if err := resp.Write(&respBuf); err != nil { + return nil, fmt.Errorf("serialize response: %w", err) + } + + respBody, err := c.send("RESPMOD", c.respModURL, reqBuf.Bytes(), respBuf.Bytes()) + if err != nil { + return nil, err + } + + if respBody == nil { + // 204 No Content: ICAP server didn't modify the response. + // Reconstruct from the buffered copy since resp.Body was consumed by Write. + reconstructed, err := http.ReadResponse(bufio.NewReader(bytes.NewReader(respBuf.Bytes())), req) + if err != nil { + return nil, fmt.Errorf("reconstruct response after ICAP 204: %w", err) + } + return reconstructed, nil + } + + modified, err := http.ReadResponse(bufio.NewReader(bytes.NewReader(respBody)), req) + if err != nil { + return nil, fmt.Errorf("parse ICAP modified response: %w", err) + } + return modified, nil +} + +// Close drains and closes all pooled connections. +func (c *ICAPClient) Close() { + close(c.pool) + for ic := range c.pool { + if err := ic.conn.Close(); err != nil { + c.log.Debugf("close ICAP connection: %v", err) + } + } +} + +// send executes an ICAP request and returns the encapsulated body from the response. +// Returns nil body for 204 No Content (no modification). +// Retries once on stale pooled connection (EOF on read). +func (c *ICAPClient) send(method string, serviceURL *url.URL, reqData, respData []byte) ([]byte, error) { + statusCode, headers, body, err := c.trySend(method, serviceURL, reqData, respData) + if err != nil && isStaleConnErr(err) { + // Retry once with a fresh connection (stale pool entry). + c.log.Debugf("ICAP %s: retrying after stale connection: %v", method, err) + statusCode, headers, body, err = c.trySend(method, serviceURL, reqData, respData) + } + if err != nil { + return nil, err + } + + switch statusCode { + case 204: + return nil, nil + case 200: + return body, nil + default: + c.log.Debugf("ICAP %s returned status %d, headers: %v", method, statusCode, headers) + return nil, fmt.Errorf("ICAP %s: status %d", method, statusCode) + } +} + +func (c *ICAPClient) trySend(method string, serviceURL *url.URL, reqData, respData []byte) (int, textproto.MIMEHeader, []byte, error) { + ic, err := c.getConn(serviceURL) + if err != nil { + return 0, nil, nil, fmt.Errorf("get ICAP connection: %w", err) + } + + if err := c.writeRequest(ic, method, serviceURL, reqData, respData); err != nil { + if closeErr := ic.conn.Close(); closeErr != nil { + c.log.Debugf("close ICAP conn after write error: %v", closeErr) + } + return 0, nil, nil, fmt.Errorf("write ICAP %s: %w", method, err) + } + + statusCode, headers, body, err := c.readResponse(ic) + if err != nil { + if closeErr := ic.conn.Close(); closeErr != nil { + c.log.Debugf("close ICAP conn after read error: %v", closeErr) + } + return 0, nil, nil, fmt.Errorf("read ICAP response: %w", err) + } + + c.putConn(ic) + return statusCode, headers, body, nil +} + +func isStaleConnErr(err error) bool { + if err == nil { + return false + } + s := err.Error() + return strings.Contains(s, "EOF") || strings.Contains(s, "broken pipe") || strings.Contains(s, "connection reset") +} + +func (c *ICAPClient) writeRequest(ic *icapConn, method string, serviceURL *url.URL, reqData, respData []byte) error { + if err := ic.conn.SetWriteDeadline(time.Now().Add(icapRWTimeout)); err != nil { + return fmt.Errorf("set write deadline: %w", err) + } + + // For RESPMOD, split the serialized HTTP response into headers and body. + // The body must be sent chunked per RFC 3507. + var respHdr, respBody []byte + if respData != nil { + if idx := bytes.Index(respData, []byte("\r\n\r\n")); idx >= 0 { + respHdr = respData[:idx+4] // include the \r\n\r\n separator + respBody = respData[idx+4:] + } else { + respHdr = respData + } + } + + var buf bytes.Buffer + + // Request line + fmt.Fprintf(&buf, "%s %s %s\r\n", method, serviceURL.String(), icapVersion) + + // Headers + host := serviceURL.Host + fmt.Fprintf(&buf, "Host: %s\r\n", host) + fmt.Fprintf(&buf, "Connection: keep-alive\r\n") + fmt.Fprintf(&buf, "Allow: 204\r\n") + + // Build Encapsulated header + offset := 0 + var encapParts []string + if reqData != nil { + encapParts = append(encapParts, fmt.Sprintf("req-hdr=%d", offset)) + offset += len(reqData) + } + if respHdr != nil { + encapParts = append(encapParts, fmt.Sprintf("res-hdr=%d", offset)) + offset += len(respHdr) + } + if len(respBody) > 0 { + encapParts = append(encapParts, fmt.Sprintf("res-body=%d", offset)) + } else { + encapParts = append(encapParts, fmt.Sprintf("null-body=%d", offset)) + } + fmt.Fprintf(&buf, "Encapsulated: %s\r\n", strings.Join(encapParts, ", ")) + fmt.Fprintf(&buf, "\r\n") + + // Encapsulated sections + if reqData != nil { + buf.Write(reqData) + } + if respHdr != nil { + buf.Write(respHdr) + } + // Body in chunked encoding (only when there is an actual body section). + // Per RFC 3507 Section 4.4.1, null-body must not include any entity data. + if len(respBody) > 0 { + fmt.Fprintf(&buf, "%x\r\n", len(respBody)) + buf.Write(respBody) + buf.WriteString("\r\n") + buf.WriteString("0\r\n\r\n") + } + + _, err := ic.conn.Write(buf.Bytes()) + return err +} + +func (c *ICAPClient) readResponse(ic *icapConn) (int, textproto.MIMEHeader, []byte, error) { + if err := ic.conn.SetReadDeadline(time.Now().Add(icapRWTimeout)); err != nil { + return 0, nil, nil, fmt.Errorf("set read deadline: %w", err) + } + + tp := textproto.NewReader(ic.reader) + + // Status line: "ICAP/1.0 200 OK" + statusLine, err := tp.ReadLine() + if err != nil { + return 0, nil, nil, fmt.Errorf("read status line: %w", err) + } + + statusCode, err := parseICAPStatus(statusLine) + if err != nil { + return 0, nil, nil, err + } + + // Headers + headers, err := tp.ReadMIMEHeader() + if err != nil { + return statusCode, nil, nil, fmt.Errorf("read ICAP headers: %w", err) + } + + if statusCode == 204 { + return statusCode, headers, nil, nil + } + + // Read encapsulated body based on Encapsulated header + body, err := c.readEncapsulatedBody(ic.reader, headers) + if err != nil { + return statusCode, headers, nil, fmt.Errorf("read encapsulated body: %w", err) + } + + return statusCode, headers, body, nil +} + +func (c *ICAPClient) readEncapsulatedBody(r *bufio.Reader, headers textproto.MIMEHeader) ([]byte, error) { + encap := headers.Get("Encapsulated") + if encap == "" { + return nil, nil + } + + // Find the body offset from the Encapsulated header. + // The last section with a non-zero offset is the body. + // Read everything from the reader as the encapsulated content. + var totalSize int + parts := strings.Split(encap, ",") + for _, part := range parts { + part = strings.TrimSpace(part) + eqIdx := strings.Index(part, "=") + if eqIdx < 0 { + continue + } + offset, err := strconv.Atoi(strings.TrimSpace(part[eqIdx+1:])) + if err != nil { + continue + } + if offset > totalSize { + totalSize = offset + } + } + + // Read all available encapsulated data (headers + body) + // The body section uses chunked encoding per RFC 3507 + var buf bytes.Buffer + if totalSize > 0 { + // Read the header sections (everything before the body offset) + headerBytes := make([]byte, totalSize) + if _, err := io.ReadFull(r, headerBytes); err != nil { + return nil, fmt.Errorf("read encapsulated headers: %w", err) + } + buf.Write(headerBytes) + } + + // Read chunked body + chunked := newChunkedReader(r) + body, err := io.ReadAll(io.LimitReader(chunked, icapMaxRespSize)) + if err != nil { + return nil, fmt.Errorf("read chunked body: %w", err) + } + buf.Write(body) + + return buf.Bytes(), nil +} + +func (c *ICAPClient) getConn(serviceURL *url.URL) (*icapConn, error) { + // Try to get a pooled connection + for { + select { + case ic := <-c.pool: + if time.Since(ic.lastUse) > icapIdleTimeout { + if err := ic.conn.Close(); err != nil { + c.log.Debugf("close idle ICAP connection: %v", err) + } + continue + } + return ic, nil + default: + return c.dialConn(serviceURL) + } + } +} + +func (c *ICAPClient) putConn(ic *icapConn) { + c.mu.Lock() + defer c.mu.Unlock() + + ic.lastUse = time.Now() + select { + case c.pool <- ic: + default: + // Pool full, close connection. + if err := ic.conn.Close(); err != nil { + c.log.Debugf("close excess ICAP connection: %v", err) + } + } +} + +func (c *ICAPClient) dialConn(serviceURL *url.URL) (*icapConn, error) { + host := serviceURL.Host + if _, _, err := net.SplitHostPort(host); err != nil { + host = net.JoinHostPort(host, icapDefaultPort) + } + + conn, err := net.DialTimeout("tcp", host, icapConnTimeout) + if err != nil { + return nil, fmt.Errorf("dial ICAP %s: %w", host, err) + } + + return &icapConn{ + conn: conn, + reader: bufio.NewReader(conn), + lastUse: time.Now(), + }, nil +} + +func parseICAPStatus(line string) (int, error) { + // "ICAP/1.0 200 OK" + parts := strings.SplitN(line, " ", 3) + if len(parts) < 2 { + return 0, fmt.Errorf("malformed ICAP status line: %q", line) + } + code, err := strconv.Atoi(parts[1]) + if err != nil { + return 0, fmt.Errorf("parse ICAP status code %q: %w", parts[1], err) + } + return code, nil +} + +// chunkedReader reads ICAP chunked encoding (same as HTTP chunked, terminated by "0\r\n\r\n"). +type chunkedReader struct { + r *bufio.Reader + remaining int + done bool +} + +func newChunkedReader(r *bufio.Reader) *chunkedReader { + return &chunkedReader{r: r} +} + +func (cr *chunkedReader) Read(p []byte) (int, error) { + if cr.done { + return 0, io.EOF + } + + if cr.remaining == 0 { + // Read chunk size line + line, err := cr.r.ReadString('\n') + if err != nil { + return 0, err + } + line = strings.TrimSpace(line) + + // Strip any chunk extensions + if idx := strings.Index(line, ";"); idx >= 0 { + line = line[:idx] + } + + size, err := strconv.ParseInt(line, 16, 64) + if err != nil { + return 0, fmt.Errorf("parse chunk size %q: %w", line, err) + } + + if size == 0 { + cr.done = true + // Consume trailing \r\n + _, _ = cr.r.ReadString('\n') + return 0, io.EOF + } + + if size < 0 || size > icapMaxRespSize { + return 0, fmt.Errorf("chunk size %d out of range (max %d)", size, icapMaxRespSize) + } + + cr.remaining = int(size) + } + + toRead := len(p) + if toRead > cr.remaining { + toRead = cr.remaining + } + + n, err := cr.r.Read(p[:toRead]) + cr.remaining -= n + + if cr.remaining == 0 { + // Consume chunk-terminating \r\n + _, _ = cr.r.ReadString('\n') + } + + return n, err +} diff --git a/client/inspect/listener.go b/client/inspect/listener.go new file mode 100644 index 000000000..47e7e44fc --- /dev/null +++ b/client/inspect/listener.go @@ -0,0 +1,21 @@ +//go:build !linux + +package inspect + +import ( + "fmt" + "net" + "net/netip" + + log "github.com/sirupsen/logrus" +) + +// newTPROXYListener is not supported on non-Linux platforms. +func newTPROXYListener(_ *log.Entry, addr netip.AddrPort, _ netip.Prefix) (net.Listener, error) { + return nil, fmt.Errorf("TPROXY listener not supported on this platform (requested %s)", addr) +} + +// getOriginalDst is not supported on non-Linux platforms. +func getOriginalDst(_ net.Conn) (netip.AddrPort, error) { + return netip.AddrPort{}, fmt.Errorf("SO_ORIGINAL_DST not supported on this platform") +} diff --git a/client/inspect/listener_linux.go b/client/inspect/listener_linux.go new file mode 100644 index 000000000..a13e6244e --- /dev/null +++ b/client/inspect/listener_linux.go @@ -0,0 +1,89 @@ +package inspect + +import ( + "fmt" + "net" + "net/netip" + "unsafe" + + log "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" +) + +// newTPROXYListener creates a TCP listener for the transparent proxy. +// After nftables REDIRECT, accepted connections have LocalAddr = WG_IP:proxy_port. +// The original destination is retrieved via getsockopt(SO_ORIGINAL_DST). +func newTPROXYListener(logger *log.Entry, addr netip.AddrPort, _ netip.Prefix) (net.Listener, error) { + ln, err := net.Listen("tcp", addr.String()) + if err != nil { + return nil, fmt.Errorf("listen on %s: %w", addr, err) + } + + logger.Infof("inspect: listener started on %s", ln.Addr()) + return ln, nil +} + +// getOriginalDst reads the original destination from conntrack via SO_ORIGINAL_DST. +// This is set by the kernel when the connection was REDIRECT'd/DNAT'd. +// Tries IPv4 first, then falls back to IPv6 (IP6T_SO_ORIGINAL_DST). +func getOriginalDst(conn net.Conn) (netip.AddrPort, error) { + tc, ok := conn.(*net.TCPConn) + if !ok { + return netip.AddrPort{}, fmt.Errorf("not a TCPConn") + } + + raw, err := tc.SyscallConn() + if err != nil { + return netip.AddrPort{}, fmt.Errorf("get syscall conn: %w", err) + } + + var origDst netip.AddrPort + var sockErr error + if err := raw.Control(func(fd uintptr) { + // Try IPv4 first (SO_ORIGINAL_DST = 80) + var sa4 unix.RawSockaddrInet4 + sa4Len := uint32(unsafe.Sizeof(sa4)) + _, _, errno := unix.Syscall6( + unix.SYS_GETSOCKOPT, + fd, + unix.SOL_IP, + 80, // SO_ORIGINAL_DST + uintptr(unsafe.Pointer(&sa4)), + uintptr(unsafe.Pointer(&sa4Len)), + 0, + ) + if errno == 0 { + addr := netip.AddrFrom4(sa4.Addr) + port := uint16(sa4.Port>>8) | uint16(sa4.Port<<8) + origDst = netip.AddrPortFrom(addr.Unmap(), port) + return + } + + // Fall back to IPv6 (IP6T_SO_ORIGINAL_DST = 80 on SOL_IPV6) + var sa6 unix.RawSockaddrInet6 + sa6Len := uint32(unsafe.Sizeof(sa6)) + _, _, errno = unix.Syscall6( + unix.SYS_GETSOCKOPT, + fd, + unix.SOL_IPV6, + 80, // IP6T_SO_ORIGINAL_DST + uintptr(unsafe.Pointer(&sa6)), + uintptr(unsafe.Pointer(&sa6Len)), + 0, + ) + if errno != 0 { + sockErr = fmt.Errorf("getsockopt SO_ORIGINAL_DST (v4 and v6): %w", errno) + return + } + addr := netip.AddrFrom16(sa6.Addr) + port := uint16(sa6.Port>>8) | uint16(sa6.Port<<8) + origDst = netip.AddrPortFrom(addr.Unmap(), port) + }); err != nil { + return netip.AddrPort{}, fmt.Errorf("control raw conn: %w", err) + } + if sockErr != nil { + return netip.AddrPort{}, sockErr + } + + return origDst, nil +} diff --git a/client/inspect/mitm.go b/client/inspect/mitm.go new file mode 100644 index 000000000..cd1f60396 --- /dev/null +++ b/client/inspect/mitm.go @@ -0,0 +1,200 @@ +package inspect + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "fmt" + "math/big" + mrand "math/rand/v2" + "sync" + "time" +) + +const ( + // certCacheSize is the maximum number of cached leaf certificates. + certCacheSize = 1024 + // certTTL is how long generated certificates remain valid. + certTTL = 24 * time.Hour +) + +// certCache is a bounded LRU cache for generated TLS certificates. +type certCache struct { + mu sync.Mutex + entries map[string]*certEntry + // order tracks LRU eviction, most recent at end. + order []string + maxSize int +} + +type certEntry struct { + cert *tls.Certificate + expiresAt time.Time +} + +func newCertCache(maxSize int) *certCache { + return &certCache{ + entries: make(map[string]*certEntry, maxSize), + order: make([]string, 0, maxSize), + maxSize: maxSize, + } +} + +func (c *certCache) get(hostname string) (*tls.Certificate, bool) { + c.mu.Lock() + defer c.mu.Unlock() + + entry, ok := c.entries[hostname] + if !ok { + return nil, false + } + + if time.Now().After(entry.expiresAt) { + c.removeLocked(hostname) + return nil, false + } + + // Move to end (most recently used) + c.touchLocked(hostname) + return entry.cert, true +} + +func (c *certCache) put(hostname string, cert *tls.Certificate) { + c.mu.Lock() + defer c.mu.Unlock() + + // Jitter the TTL by +/- 20% to prevent thundering herd on expiry. + jitter := time.Duration(float64(certTTL) * (0.8 + 0.4*mrand.Float64())) + + if _, exists := c.entries[hostname]; exists { + c.entries[hostname] = &certEntry{ + cert: cert, + expiresAt: time.Now().Add(jitter), + } + c.touchLocked(hostname) + return + } + + // Evict oldest if at capacity + for len(c.entries) >= c.maxSize && len(c.order) > 0 { + c.removeLocked(c.order[0]) + } + + c.entries[hostname] = &certEntry{ + cert: cert, + expiresAt: time.Now().Add(jitter), + } + c.order = append(c.order, hostname) +} + +func (c *certCache) touchLocked(hostname string) { + for i, h := range c.order { + if h == hostname { + c.order = append(c.order[:i], c.order[i+1:]...) + c.order = append(c.order, hostname) + return + } + } +} + +func (c *certCache) removeLocked(hostname string) { + delete(c.entries, hostname) + for i, h := range c.order { + if h == hostname { + c.order = append(c.order[:i], c.order[i+1:]...) + return + } + } +} + +// CertProvider generates TLS certificates on the fly, signed by a CA. +// Generated certificates are cached in an LRU cache. +type CertProvider struct { + ca *x509.Certificate + caKey crypto.PrivateKey + cache *certCache +} + +// NewCertProvider creates a certificate provider using the given CA. +func NewCertProvider(ca *x509.Certificate, caKey crypto.PrivateKey) *CertProvider { + return &CertProvider{ + ca: ca, + caKey: caKey, + cache: newCertCache(certCacheSize), + } +} + +// GetCertificate returns a TLS certificate for the given hostname, +// generating and caching one if necessary. +func (p *CertProvider) GetCertificate(hostname string) (*tls.Certificate, error) { + if cert, ok := p.cache.get(hostname); ok { + return cert, nil + } + + cert, err := p.generateCert(hostname) + if err != nil { + return nil, fmt.Errorf("generate cert for %s: %w", hostname, err) + } + + p.cache.put(hostname, cert) + return cert, nil +} + +// GetTLSConfig returns a tls.Config that dynamically provides certificates +// for any hostname using the MITM CA. +func (p *CertProvider) GetTLSConfig() *tls.Config { + return &tls.Config{ + GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { + return p.GetCertificate(hello.ServerName) + }, + NextProtos: []string{"h2", "http/1.1"}, + MinVersion: tls.VersionTLS12, + } +} + +func (p *CertProvider) generateCert(hostname string) (*tls.Certificate, error) { + serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + if err != nil { + return nil, fmt.Errorf("generate serial number: %w", err) + } + + now := time.Now() + template := &x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + CommonName: hostname, + }, + NotBefore: now.Add(-5 * time.Minute), + NotAfter: now.Add(certTTL), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{ + x509.ExtKeyUsageServerAuth, + }, + DNSNames: []string{hostname}, + } + + leafKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, fmt.Errorf("generate leaf key: %w", err) + } + + certDER, err := x509.CreateCertificate(rand.Reader, template, p.ca, &leafKey.PublicKey, p.caKey) + if err != nil { + return nil, fmt.Errorf("sign leaf certificate: %w", err) + } + + leafCert, err := x509.ParseCertificate(certDER) + if err != nil { + return nil, fmt.Errorf("parse generated certificate: %w", err) + } + + return &tls.Certificate{ + Certificate: [][]byte{certDER, p.ca.Raw}, + PrivateKey: leafKey, + Leaf: leafCert, + }, nil +} diff --git a/client/inspect/mitm_test.go b/client/inspect/mitm_test.go new file mode 100644 index 000000000..dfb5e7f06 --- /dev/null +++ b/client/inspect/mitm_test.go @@ -0,0 +1,133 @@ +package inspect + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "math/big" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func generateTestCA(t *testing.T) (*x509.Certificate, *ecdsa.PrivateKey) { + t.Helper() + + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + template := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + CommonName: "Test CA", + }, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + IsCA: true, + } + + certDER, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) + require.NoError(t, err) + + cert, err := x509.ParseCertificate(certDER) + require.NoError(t, err) + + return cert, key +} + +func TestCertProvider_GetCertificate(t *testing.T) { + ca, caKey := generateTestCA(t) + provider := NewCertProvider(ca, caKey) + + cert, err := provider.GetCertificate("example.com") + require.NoError(t, err) + require.NotNil(t, cert) + + // Verify the leaf certificate + assert.Equal(t, "example.com", cert.Leaf.Subject.CommonName) + assert.Contains(t, cert.Leaf.DNSNames, "example.com") + + // Verify chain: leaf + CA + assert.Len(t, cert.Certificate, 2) + + // Verify leaf is signed by our CA + pool := x509.NewCertPool() + pool.AddCert(ca) + _, err = cert.Leaf.Verify(x509.VerifyOptions{ + Roots: pool, + }) + require.NoError(t, err) +} + +func TestCertProvider_CachesResults(t *testing.T) { + ca, caKey := generateTestCA(t) + provider := NewCertProvider(ca, caKey) + + cert1, err := provider.GetCertificate("cached.example.com") + require.NoError(t, err) + + cert2, err := provider.GetCertificate("cached.example.com") + require.NoError(t, err) + + // Same pointer = cached + assert.Equal(t, cert1, cert2) +} + +func TestCertProvider_DifferentHostsDifferentCerts(t *testing.T) { + ca, caKey := generateTestCA(t) + provider := NewCertProvider(ca, caKey) + + cert1, err := provider.GetCertificate("a.example.com") + require.NoError(t, err) + + cert2, err := provider.GetCertificate("b.example.com") + require.NoError(t, err) + + assert.NotEqual(t, cert1.Leaf.SerialNumber, cert2.Leaf.SerialNumber) +} + +func TestCertProvider_TLSConfigHandshake(t *testing.T) { + ca, caKey := generateTestCA(t) + provider := NewCertProvider(ca, caKey) + + tlsConfig := provider.GetTLSConfig() + require.NotNil(t, tlsConfig) + require.NotNil(t, tlsConfig.GetCertificate) + + // Simulate a ClientHelloInfo + hello := &tls.ClientHelloInfo{ + ServerName: "handshake.example.com", + } + + cert, err := tlsConfig.GetCertificate(hello) + require.NoError(t, err) + assert.Equal(t, "handshake.example.com", cert.Leaf.Subject.CommonName) +} + +func TestCertCache_Eviction(t *testing.T) { + cache := newCertCache(3) + + for i := range 5 { + hostname := string(rune('a'+i)) + ".example.com" + cache.put(hostname, &tls.Certificate{}) + } + + // Only 3 should remain (c, d, e - the most recent) + assert.Len(t, cache.entries, 3) + + _, ok := cache.get("a.example.com") + assert.False(t, ok, "oldest entry should be evicted") + + _, ok = cache.get("b.example.com") + assert.False(t, ok, "second oldest should be evicted") + + _, ok = cache.get("e.example.com") + assert.True(t, ok, "newest entry should exist") +} diff --git a/client/inspect/peek.go b/client/inspect/peek.go new file mode 100644 index 000000000..1eb41adc0 --- /dev/null +++ b/client/inspect/peek.go @@ -0,0 +1,109 @@ +package inspect + +import ( + "bytes" + "fmt" + "io" + "net" +) + +// peekConn wraps a net.Conn with a buffer that allows reading ahead +// without consuming data. Subsequent Read calls return the buffered +// bytes first, then read from the underlying connection. +type peekConn struct { + net.Conn + buf bytes.Buffer + // peeked holds the raw bytes that were peeked, available for replay. + peeked []byte +} + +// newPeekConn wraps conn for peek-ahead reading. +func newPeekConn(conn net.Conn) *peekConn { + return &peekConn{Conn: conn} +} + +// Peek reads exactly n bytes from the connection without consuming them. +// The peeked bytes are replayed on subsequent Read calls. +// Peek may only be called once; calling it again returns an error. +func (c *peekConn) Peek(n int) ([]byte, error) { + if c.peeked != nil { + return nil, fmt.Errorf("peek already called") + } + + buf := make([]byte, n) + if _, err := io.ReadFull(c.Conn, buf); err != nil { + return nil, fmt.Errorf("peek %d bytes: %w", n, err) + } + + c.peeked = buf + c.buf.Write(buf) + + return buf, nil +} + +// PeekAll reads up to n bytes, returning whatever is available. +// Unlike Peek, it does not require exactly n bytes. +func (c *peekConn) PeekAll(n int) ([]byte, error) { + if c.peeked != nil { + return nil, fmt.Errorf("peek already called") + } + + buf := make([]byte, n) + nr, err := c.Conn.Read(buf) + if nr > 0 { + c.peeked = buf[:nr] + c.buf.Write(c.peeked) + } + if err != nil && nr == 0 { + return nil, fmt.Errorf("peek: %w", err) + } + + return c.peeked, nil +} + +// PeekMore extends the peeked buffer to at least n total bytes. +// The buffer is reset and refilled with the extended data. +// The returned slice is the internal peeked buffer; callers must not +// retain references from prior Peek/PeekMore calls after calling this. +func (c *peekConn) PeekMore(n int) ([]byte, error) { + if len(c.peeked) >= n { + return c.peeked[:n], nil + } + + remaining := n - len(c.peeked) + extra := make([]byte, remaining) + if _, err := io.ReadFull(c.Conn, extra); err != nil { + return nil, fmt.Errorf("peek more %d bytes: %w", remaining, err) + } + + // Pre-allocate to avoid reallocation detaching previously returned slices. + combined := make([]byte, 0, n) + combined = append(combined, c.peeked...) + combined = append(combined, extra...) + c.peeked = combined + c.buf.Reset() + c.buf.Write(c.peeked) + + return c.peeked, nil +} + +// Peeked returns the bytes that were peeked so far, or nil if Peek hasn't been called. +func (c *peekConn) Peeked() []byte { + return c.peeked +} + +// Read returns buffered peek data first, then reads from the underlying connection. +func (c *peekConn) Read(p []byte) (int, error) { + if c.buf.Len() > 0 { + return c.buf.Read(p) + } + return c.Conn.Read(p) +} + +// reader returns an io.Reader that replays buffered bytes then reads from conn. +func (c *peekConn) reader() io.Reader { + if c.buf.Len() > 0 { + return io.MultiReader(&c.buf, c.Conn) + } + return c.Conn +} diff --git a/client/inspect/proxy.go b/client/inspect/proxy.go new file mode 100644 index 000000000..0d0c11925 --- /dev/null +++ b/client/inspect/proxy.go @@ -0,0 +1,482 @@ +package inspect + +import ( + "context" + "errors" + "fmt" + "net" + "net/netip" + "sync" + "time" + + log "github.com/sirupsen/logrus" +) + +// ErrBlocked is returned when a connection is denied by proxy policy. +var ErrBlocked = errors.New("connection blocked by proxy policy") + +const ( + // headerReadTimeout is the deadline for reading the initial protocol header. + // Prevents slow loris attacks where a client opens a connection but sends data slowly. + headerReadTimeout = 10 * time.Second + + // idleTimeout is the deadline for idle connections between HTTP requests. + idleTimeout = 120 * time.Second +) + +// Proxy is the inspection engine for traffic passing through a NetBird +// routing peer. It handles protocol detection, rule evaluation, MITM TLS +// decryption, ICAP delegation, and external proxy forwarding. +type Proxy struct { + config Config + rules *RuleEngine + certs *CertProvider + icap *ICAPClient + // envoy is nil unless mode is ModeEnvoy. + envoy *envoyManager + // dialer is the outbound dialer (with SO_MARK cleared on Linux). + dialer net.Dialer + log *log.Entry + // wgNetwork is the WG overlay prefix; dial targets inside it are blocked. + wgNetwork netip.Prefix + // localIPs reports the routing peer's own IPs; dial targets are blocked. + localIPs LocalIPChecker + // listener is the TPROXY/REDIRECT listener for kernel mode. + listener net.Listener + + mu sync.RWMutex + ctx context.Context + cancel context.CancelFunc +} + +// LocalIPChecker reports whether an IP belongs to the local machine. +type LocalIPChecker interface { + IsLocalIP(netip.Addr) bool +} + +// New creates a transparent proxy with the given configuration. +func New(ctx context.Context, logger *log.Entry, config Config) (*Proxy, error) { + ctx, cancel := context.WithCancel(ctx) + + p := &Proxy{ + config: config, + rules: NewRuleEngine(logger, config.DefaultAction), + dialer: newOutboundDialer(), + log: logger, + wgNetwork: config.WGNetwork, + localIPs: config.LocalIPChecker, + ctx: ctx, + cancel: cancel, + } + + p.rules.UpdateRules(config.Rules, config.DefaultAction) + + // Initialize MITM certificate provider + if config.TLS != nil { + p.certs = NewCertProvider(config.TLS.CA, config.TLS.CAKey) + } + + // Initialize ICAP client + if config.ICAP != nil { + p.icap = NewICAPClient(logger, config.ICAP) + } + + // Start envoy sidecar if configured + if config.Mode == ModeEnvoy { + envoyLog := logger.WithField("sidecar", "envoy") + em, err := startEnvoy(ctx, envoyLog, config) + if err != nil { + cancel() + return nil, fmt.Errorf("start envoy sidecar: %w", err) + } + p.envoy = em + } + + // Start TPROXY listener for kernel mode + if config.ListenAddr.IsValid() { + ln, err := newTPROXYListener(logger, config.ListenAddr, netip.Prefix{}) + if err != nil { + cancel() + return nil, fmt.Errorf("start TPROXY listener on %s: %w", config.ListenAddr, err) + } + p.listener = ln + go p.acceptLoop(ln) + } + + return p, nil +} + +// HandleTCP is the entry point for TCP connections from the userspace forwarder. +// It determines the protocol (TLS or plaintext HTTP), evaluates rules, +// and either blocks, passes through, inspects, or forwards to an external proxy. +func (p *Proxy) HandleTCP(ctx context.Context, clientConn net.Conn, dst netip.AddrPort, src SourceInfo) error { + defer func() { + if err := clientConn.Close(); err != nil { + p.log.Debugf("close client conn: %v", err) + } + }() + + p.mu.RLock() + mode := p.config.Mode + p.mu.RUnlock() + + if mode == ModeExternal { + pconn := newPeekConn(clientConn) + return p.handleExternal(ctx, pconn, dst) + } + + // Envoy and builtin modes both peek the protocol header for rule evaluation. + // Envoy mode forwards non-blocked traffic to envoy; builtin mode handles all locally. + // TLS blocks are handled by Go (instant close) since envoy can't cleanly RST a TLS connection. + + // Built-in and envoy mode: peek 5 bytes (TLS record header size) to determine protocol. + // Set a read deadline to prevent slow loris attacks. + if err := clientConn.SetReadDeadline(time.Now().Add(headerReadTimeout)); err != nil { + return fmt.Errorf("set read deadline: %w", err) + } + pconn := newPeekConn(clientConn) + header, err := pconn.Peek(5) + if err != nil { + return fmt.Errorf("peek protocol header: %w", err) + } + if err := clientConn.SetReadDeadline(time.Time{}); err != nil { + return fmt.Errorf("clear read deadline: %w", err) + } + + if isTLSHandshake(header[0]) { + return p.handleTLS(ctx, pconn, dst, src) + } + + if isHTTPMethod(header) { + return p.handlePlainHTTP(ctx, pconn, dst, src) + } + + // Not TLS and not HTTP: evaluate rules with ProtoOther. + // If no rule explicitly allows "other", this falls through to the default action. + action := p.rules.Evaluate(src.IP, "", dst.Addr(), dst.Port(), ProtoOther, "") + if action == ActionAllow { + remote, err := p.dialTCP(ctx, dst) + if err != nil { + return fmt.Errorf("dial for passthrough: %w", err) + } + defer func() { + if err := remote.Close(); err != nil { + p.log.Debugf("close remote conn: %v", err) + } + }() + return relay(ctx, pconn, remote) + } + + p.log.Debugf("block: non-HTTP/TLS to %s (action=%s, first bytes: %x)", dst, action, header) + return ErrBlocked +} + +// InspectTCP evaluates rules for a TCP connection and returns the result. +// Unlike HandleTCP, it can return early for allow decisions, letting the caller +// handle the relay (USP forwarder passthrough optimization). +// +// When InspectResult.PassthroughConn is non-nil, ownership transfers to the caller: +// the caller must close the connection and relay traffic. The engine does not close it. +// +// When PassthroughConn is nil, the engine handled everything internally +// (block, inspect/MITM, or plain HTTP inspection) and closed the connection. +func (p *Proxy) InspectTCP(ctx context.Context, clientConn net.Conn, dst netip.AddrPort, src SourceInfo) (InspectResult, error) { + p.mu.RLock() + mode := p.config.Mode + envoy := p.envoy + p.mu.RUnlock() + + // External mode: handle internally, engine owns the connection. + if mode == ModeExternal { + defer func() { + if err := clientConn.Close(); err != nil { + p.log.Debugf("close client conn: %v", err) + } + }() + pconn := newPeekConn(clientConn) + err := p.handleExternal(ctx, pconn, dst) + return InspectResult{Action: ActionAllow}, err + } + + // Peek protocol header. + if err := clientConn.SetReadDeadline(time.Now().Add(headerReadTimeout)); err != nil { + clientConn.Close() + return InspectResult{}, fmt.Errorf("set read deadline: %w", err) + } + pconn := newPeekConn(clientConn) + header, err := pconn.Peek(5) + if err != nil { + clientConn.Close() + return InspectResult{}, fmt.Errorf("peek protocol header: %w", err) + } + if err := clientConn.SetReadDeadline(time.Time{}); err != nil { + clientConn.Close() + return InspectResult{}, fmt.Errorf("clear read deadline: %w", err) + } + + // TLS: may return passthrough for allow. + if isTLSHandshake(header[0]) { + result, err := p.inspectTLS(ctx, pconn, dst, src) + if err != nil && result.PassthroughConn == nil { + clientConn.Close() + return result, err + } + // Envoy mode: forward allowed TLS to envoy instead of returning passthrough. + if result.PassthroughConn != nil && envoy != nil { + defer clientConn.Close() + envoyErr := p.forwardToEnvoy(ctx, pconn, dst, src, envoy) + return InspectResult{Action: ActionAllow}, envoyErr + } + return result, err + } + + // Plain HTTP: in envoy mode, forward to envoy for L7 processing. + // In builtin mode, inspect per-request locally. + if isHTTPMethod(header) { + defer func() { + if err := clientConn.Close(); err != nil { + p.log.Debugf("close client conn: %v", err) + } + }() + if envoy != nil { + err := p.forwardToEnvoy(ctx, pconn, dst, src, envoy) + return InspectResult{Action: ActionAllow}, err + } + err := p.handlePlainHTTP(ctx, pconn, dst, src) + return InspectResult{Action: ActionInspect}, err + } + + // Other protocol: evaluate rules. + action := p.rules.Evaluate(src.IP, "", dst.Addr(), dst.Port(), ProtoOther, "") + if action == ActionAllow { + // Envoy mode: forward to envoy. + if envoy != nil { + defer clientConn.Close() + err := p.forwardToEnvoy(ctx, pconn, dst, src, envoy) + return InspectResult{Action: ActionAllow}, err + } + return InspectResult{Action: ActionAllow, PassthroughConn: pconn}, nil + } + + p.log.Debugf("block: non-HTTP/TLS to %s (action=%s, first bytes: %x)", dst, action, header) + clientConn.Close() + return InspectResult{Action: ActionBlock}, ErrBlocked +} + +// HandleUDPPacket inspects a UDP packet for QUIC Initial packets. +// Returns the action to take: ActionAllow to continue normal forwarding, +// ActionBlock to drop the packet. +// Non-QUIC packets always return ActionAllow. +func (p *Proxy) HandleUDPPacket(data []byte, dst netip.AddrPort, src SourceInfo) Action { + if len(data) < 5 { + return ActionAllow + } + + // Check for QUIC Long Header + if data[0]&0x80 == 0 { + return ActionAllow + } + + sni, err := ExtractQUICSNI(data) + if err != nil { + // Can't parse QUIC, allow through (could be non-QUIC UDP) + p.log.Tracef("QUIC SNI extraction failed for %s: %v", dst, err) + return ActionAllow + } + + if sni == "" { + return ActionAllow + } + + action := p.rules.Evaluate(src.IP, sni, dst.Addr(), dst.Port(), ProtoH3, "") + + if action == ActionBlock { + p.log.Debugf("block: QUIC to %s (SNI=%s)", dst, sni.PunycodeString()) + return ActionBlock + } + + // QUIC can't be MITMed, treat Inspect as Allow + if action == ActionInspect { + p.log.Debugf("allow: QUIC to %s (SNI=%s), MITM not supported for QUIC", dst, sni.PunycodeString()) + } else { + p.log.Tracef("allow: QUIC to %s (SNI=%s)", dst, sni.PunycodeString()) + } + + return ActionAllow +} + +// handlePlainHTTP handles plaintext HTTP connections. +func (p *Proxy) handlePlainHTTP(ctx context.Context, pconn *peekConn, dst netip.AddrPort, src SourceInfo) error { + remote, err := p.dialTCP(ctx, dst) + if err != nil { + return fmt.Errorf("dial %s: %w", dst, err) + } + defer func() { + if err := remote.Close(); err != nil { + p.log.Debugf("close remote for %s: %v", dst, err) + } + }() + + // For plaintext HTTP, always inspect (we can see the traffic) + return p.inspectHTTP(ctx, pconn, remote, dst, "", src, "http/1.1") +} + +// UpdateConfig replaces the inspection engine configuration at runtime. +func (p *Proxy) UpdateConfig(config Config) { + p.log.Debugf("config update: mode=%s rules=%d default=%s has_tls=%v has_icap=%v", + config.Mode, len(config.Rules), config.DefaultAction, config.TLS != nil, config.ICAP != nil) + + p.mu.Lock() + + p.config = config + p.rules.UpdateRules(config.Rules, config.DefaultAction) + + // Update MITM provider + if config.TLS != nil { + p.certs = NewCertProvider(config.TLS.CA, config.TLS.CAKey) + } else { + p.certs = nil + } + + // Swap ICAP client under lock, close the old one outside to avoid blocking. + var oldICAP *ICAPClient + if config.ICAP != nil { + oldICAP = p.icap + p.icap = NewICAPClient(p.log, config.ICAP) + } else { + oldICAP = p.icap + p.icap = nil + } + + // If switching away from envoy mode, clear and stop the old envoy. + var oldEnvoy *envoyManager + if config.Mode != ModeEnvoy && p.envoy != nil { + oldEnvoy = p.envoy + p.envoy = nil + } + + envoy := p.envoy + + p.mu.Unlock() + + if oldICAP != nil { + oldICAP.Close() + } + + if oldEnvoy != nil { + oldEnvoy.Stop() + } + + // Reload envoy config if still in envoy mode. + if envoy != nil && config.Mode == ModeEnvoy { + if err := envoy.Reload(config); err != nil { + p.log.Errorf("inspect: envoy config reload: %v", err) + } + } +} + +// Mode returns the current proxy operating mode. +func (p *Proxy) Mode() ProxyMode { + p.mu.RLock() + defer p.mu.RUnlock() + return p.config.Mode +} + +// ListenPort returns the port to use for kernel-mode nftables REDIRECT. +// For builtin mode: the TPROXY listener port. +// For envoy mode: the envoy listener port (nftables redirects directly to envoy). +// Returns 0 if no listener is active. +func (p *Proxy) ListenPort() uint16 { + p.mu.RLock() + envoy := p.envoy + p.mu.RUnlock() + + if envoy != nil { + return envoy.listenPort + } + if p.listener == nil { + return 0 + } + tcpAddr, ok := p.listener.Addr().(*net.TCPAddr) + if !ok { + return 0 + } + return uint16(tcpAddr.Port) +} + +// Close shuts down the proxy and releases resources. +func (p *Proxy) Close() error { + p.cancel() + + p.mu.Lock() + envoy := p.envoy + p.envoy = nil + icap := p.icap + p.icap = nil + p.mu.Unlock() + + if envoy != nil { + envoy.Stop() + } + + if p.listener != nil { + if err := p.listener.Close(); err != nil { + p.log.Debugf("close TPROXY listener: %v", err) + } + } + + if icap != nil { + icap.Close() + } + + return nil +} + +// acceptLoop accepts connections from the redirected listener (kernel mode). +// Connections arrive via nftables REDIRECT; original destination is read from conntrack. +func (p *Proxy) acceptLoop(ln net.Listener) { + for { + conn, err := ln.Accept() + if err != nil { + if p.ctx.Err() != nil { + return + } + p.log.Debugf("accept error: %v", err) + continue + } + + go func() { + // Read original destination from conntrack (SO_ORIGINAL_DST). + // nftables REDIRECT changes dst to the local WG IP:proxy_port, + // but conntrack preserves the real destination. + dstAddr, err := getOriginalDst(conn) + if err != nil { + p.log.Debugf("get original dst: %v", err) + if closeErr := conn.Close(); closeErr != nil { + p.log.Debugf("close conn: %v", closeErr) + } + return + } + + p.log.Tracef("accepted: %s -> %s (original dst %s)", + conn.RemoteAddr(), conn.LocalAddr(), dstAddr) + + srcAddr, err := netip.ParseAddrPort(conn.RemoteAddr().String()) + if err != nil { + p.log.Debugf("parse source: %v", err) + if closeErr := conn.Close(); closeErr != nil { + p.log.Debugf("close conn: %v", closeErr) + } + return + } + + src := SourceInfo{ + IP: srcAddr.Addr().Unmap(), + } + + if err := p.HandleTCP(p.ctx, conn, dstAddr, src); err != nil && !errors.Is(err, ErrBlocked) { + p.log.Debugf("connection to %s: %v", dstAddr, err) + } + }() + } +} diff --git a/client/inspect/quic.go b/client/inspect/quic.go new file mode 100644 index 000000000..2a03f2462 --- /dev/null +++ b/client/inspect/quic.go @@ -0,0 +1,388 @@ +package inspect + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/sha256" + "encoding/binary" + "fmt" + "io" + + "golang.org/x/crypto/hkdf" + + "github.com/netbirdio/netbird/shared/management/domain" +) + +// QUIC version constants +const ( + quicV1Version uint32 = 0x00000001 + quicV2Version uint32 = 0x6b3343cf +) + +// quicV1Salt is the initial salt for QUIC v1 (RFC 9001 Section 5.2). +var quicV1Salt = []byte{ + 0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, + 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, + 0xcc, 0xbb, 0x7f, 0x0a, +} + +// quicV2Salt is the initial salt for QUIC v2 (RFC 9369). +var quicV2Salt = []byte{ + 0x0d, 0xed, 0xe3, 0xde, 0xf7, 0x00, 0xa6, 0xdb, + 0x81, 0x93, 0x81, 0xbe, 0x6e, 0x26, 0x9d, 0xcb, + 0xf9, 0xbd, 0x2e, 0xd9, +} + +// ExtractQUICSNI extracts the SNI from a QUIC Initial packet. +// The Initial packet's encryption uses well-known keys derived from the +// Destination Connection ID, so any observer can decrypt it (by design). +func ExtractQUICSNI(data []byte) (domain.Domain, error) { + if len(data) < 5 { + return "", fmt.Errorf("packet too short") + } + + // Check for QUIC Long Header (form bit set) + if data[0]&0x80 == 0 { + return "", fmt.Errorf("not a QUIC long header packet") + } + + // Version + version := binary.BigEndian.Uint32(data[1:5]) + + var salt []byte + var initialLabel, keyLabel, ivLabel, hpLabel string + + switch version { + case quicV1Version: + salt = quicV1Salt + initialLabel = "client in" + keyLabel = "quic key" + ivLabel = "quic iv" + hpLabel = "quic hp" + case quicV2Version: + salt = quicV2Salt + initialLabel = "client in" + keyLabel = "quicv2 key" + ivLabel = "quicv2 iv" + hpLabel = "quicv2 hp" + default: + return "", fmt.Errorf("unsupported QUIC version: 0x%08x", version) + } + + // Parse Long Header + if len(data) < 6 { + return "", fmt.Errorf("packet too short for DCID length") + } + dcidLen := int(data[5]) + if len(data) < 6+dcidLen+1 { + return "", fmt.Errorf("packet too short for DCID") + } + dcid := data[6 : 6+dcidLen] + + scidLenOff := 6 + dcidLen + scidLen := int(data[scidLenOff]) + tokenLenOff := scidLenOff + 1 + scidLen + + if tokenLenOff >= len(data) { + return "", fmt.Errorf("packet too short for token length") + } + + // Token length is a variable-length integer + tokenLen, tokenLenSize, err := readVarInt(data[tokenLenOff:]) + if err != nil { + return "", fmt.Errorf("read token length: %w", err) + } + + payloadLenOff := tokenLenOff + tokenLenSize + int(tokenLen) + if payloadLenOff >= len(data) { + return "", fmt.Errorf("packet too short for payload length") + } + + // Payload length is a variable-length integer + payloadLen, payloadLenSize, err := readVarInt(data[payloadLenOff:]) + if err != nil { + return "", fmt.Errorf("read payload length: %w", err) + } + + pnOffset := payloadLenOff + payloadLenSize + if pnOffset+4 > len(data) { + return "", fmt.Errorf("packet too short for packet number") + } + + // Derive initial keys + clientKey, clientIV, clientHP, err := deriveInitialKeys(dcid, salt, initialLabel, keyLabel, ivLabel, hpLabel) + if err != nil { + return "", fmt.Errorf("derive initial keys: %w", err) + } + + // Remove header protection + sampleOffset := pnOffset + 4 // sample starts 4 bytes after pn offset + if sampleOffset+16 > len(data) { + return "", fmt.Errorf("packet too short for HP sample") + } + sample := data[sampleOffset : sampleOffset+16] + + hpBlock, err := aes.NewCipher(clientHP) + if err != nil { + return "", fmt.Errorf("create HP cipher: %w", err) + } + + mask := make([]byte, 16) + hpBlock.Encrypt(mask, sample) + + // Unmask header byte + header := make([]byte, len(data)) + copy(header, data) + header[0] ^= mask[0] & 0x0f // Long header: low 4 bits + + // Determine packet number length + pnLen := int(header[0]&0x03) + 1 + + // Unmask packet number + for i := 0; i < pnLen; i++ { + header[pnOffset+i] ^= mask[1+i] + } + + // Reconstruct packet number + var pn uint32 + for i := 0; i < pnLen; i++ { + pn = (pn << 8) | uint32(header[pnOffset+i]) + } + + // Build nonce + nonce := make([]byte, len(clientIV)) + copy(nonce, clientIV) + for i := 0; i < 4; i++ { + nonce[len(nonce)-1-i] ^= byte(pn >> (8 * i)) + } + + // Decrypt payload + block, err := aes.NewCipher(clientKey) + if err != nil { + return "", fmt.Errorf("create AES cipher: %w", err) + } + + aead, err := cipher.NewGCM(block) + if err != nil { + return "", fmt.Errorf("create AEAD: %w", err) + } + + encryptedPayload := header[pnOffset+pnLen : pnOffset+int(payloadLen)] + aad := header[:pnOffset+pnLen] + + plaintext, err := aead.Open(nil, nonce, encryptedPayload, aad) + if err != nil { + return "", fmt.Errorf("decrypt QUIC payload: %w", err) + } + + // Parse CRYPTO frames to extract ClientHello + clientHello, err := extractCryptoFrames(plaintext) + if err != nil { + return "", fmt.Errorf("extract CRYPTO frames: %w", err) + } + + info, err := parseHelloBody(clientHello) + return info.SNI, err +} + +// deriveInitialKeys derives the client's initial encryption keys from the DCID. +func deriveInitialKeys(dcid, salt []byte, initialLabel, keyLabel, ivLabel, hpLabel string) (key, iv, hp []byte, err error) { + // initial_secret = HKDF-Extract(salt, DCID) + initialSecret := hkdf.Extract(sha256.New, dcid, salt) + + // client_initial_secret = HKDF-Expand-Label(initial_secret, initialLabel, "", 32) + clientSecret, err := hkdfExpandLabel(initialSecret, initialLabel, nil, 32) + if err != nil { + return nil, nil, nil, fmt.Errorf("derive client secret: %w", err) + } + + // client_key = HKDF-Expand-Label(client_secret, keyLabel, "", 16) + key, err = hkdfExpandLabel(clientSecret, keyLabel, nil, 16) + if err != nil { + return nil, nil, nil, fmt.Errorf("derive key: %w", err) + } + + // client_iv = HKDF-Expand-Label(client_secret, ivLabel, "", 12) + iv, err = hkdfExpandLabel(clientSecret, ivLabel, nil, 12) + if err != nil { + return nil, nil, nil, fmt.Errorf("derive IV: %w", err) + } + + // client_hp = HKDF-Expand-Label(client_secret, hpLabel, "", 16) + hp, err = hkdfExpandLabel(clientSecret, hpLabel, nil, 16) + if err != nil { + return nil, nil, nil, fmt.Errorf("derive HP key: %w", err) + } + + return key, iv, hp, nil +} + +// hkdfExpandLabel implements TLS 1.3 HKDF-Expand-Label. +func hkdfExpandLabel(secret []byte, label string, context []byte, length int) ([]byte, error) { + // HkdfLabel = struct { + // uint16 length; + // opaque label<7..255> = "tls13 " + Label; + // opaque context<0..255> = Context; + // } + fullLabel := "tls13 " + label + + hkdfLabel := make([]byte, 2+1+len(fullLabel)+1+len(context)) + binary.BigEndian.PutUint16(hkdfLabel[0:2], uint16(length)) + hkdfLabel[2] = byte(len(fullLabel)) + copy(hkdfLabel[3:], fullLabel) + hkdfLabel[3+len(fullLabel)] = byte(len(context)) + if len(context) > 0 { + copy(hkdfLabel[4+len(fullLabel):], context) + } + + expander := hkdf.Expand(sha256.New, secret, hkdfLabel) + out := make([]byte, length) + if _, err := io.ReadFull(expander, out); err != nil { + return nil, err + } + return out, nil +} + +// maxCryptoFrameSize limits total CRYPTO frame data to prevent memory exhaustion. +const maxCryptoFrameSize = 64 * 1024 + +// extractCryptoFrames reassembles CRYPTO frame data from QUIC frames. +func extractCryptoFrames(frames []byte) ([]byte, error) { + var result []byte + pos := 0 + + for pos < len(frames) { + frameType := frames[pos] + + switch { + case frameType == 0x00: + // PADDING frame + pos++ + + case frameType == 0x06: + // CRYPTO frame + pos++ + + offset, n, err := readVarInt(frames[pos:]) + if err != nil { + return nil, fmt.Errorf("read crypto offset: %w", err) + } + pos += n + _ = offset // We assume ordered, offset 0 for Initial + + dataLen, n, err := readVarInt(frames[pos:]) + if err != nil { + return nil, fmt.Errorf("read crypto data length: %w", err) + } + pos += n + + end := pos + int(dataLen) + if end > len(frames) { + return nil, fmt.Errorf("CRYPTO frame data truncated") + } + + result = append(result, frames[pos:end]...) + if len(result) > maxCryptoFrameSize { + return nil, fmt.Errorf("CRYPTO frame data exceeds %d bytes", maxCryptoFrameSize) + } + pos = end + + case frameType == 0x01: + // PING frame + pos++ + + case frameType == 0x02 || frameType == 0x03: + // ACK frame - skip + pos++ + // Largest Acknowledged + _, n, err := readVarInt(frames[pos:]) + if err != nil { + return nil, fmt.Errorf("read ACK: %w", err) + } + pos += n + // ACK Delay + _, n, err = readVarInt(frames[pos:]) + if err != nil { + return nil, fmt.Errorf("read ACK delay: %w", err) + } + pos += n + // ACK Range Count + rangeCount, n, err := readVarInt(frames[pos:]) + if err != nil { + return nil, fmt.Errorf("read ACK range count: %w", err) + } + pos += n + // First ACK Range + _, n, err = readVarInt(frames[pos:]) + if err != nil { + return nil, fmt.Errorf("read first ACK range: %w", err) + } + pos += n + // Additional ranges + for i := uint64(0); i < rangeCount; i++ { + _, n, err = readVarInt(frames[pos:]) + if err != nil { + return nil, fmt.Errorf("read ACK gap: %w", err) + } + pos += n + _, n, err = readVarInt(frames[pos:]) + if err != nil { + return nil, fmt.Errorf("read ACK range: %w", err) + } + pos += n + } + // ECN counts for type 0x03 + if frameType == 0x03 { + for range 3 { + _, n, err = readVarInt(frames[pos:]) + if err != nil { + return nil, fmt.Errorf("read ECN count: %w", err) + } + pos += n + } + } + + default: + // Unknown frame type, stop parsing + if len(result) > 0 { + return result, nil + } + return nil, fmt.Errorf("unknown QUIC frame type: 0x%02x at offset %d", frameType, pos) + } + } + + if len(result) == 0 { + return nil, fmt.Errorf("no CRYPTO frames found") + } + + return result, nil +} + +// readVarInt reads a QUIC variable-length integer. +// Returns (value, bytes consumed, error). +func readVarInt(data []byte) (uint64, int, error) { + if len(data) == 0 { + return 0, 0, fmt.Errorf("empty data for varint") + } + + prefix := data[0] >> 6 + length := 1 << prefix + + if len(data) < length { + return 0, 0, fmt.Errorf("varint truncated: need %d, have %d", length, len(data)) + } + + var val uint64 + switch length { + case 1: + val = uint64(data[0] & 0x3f) + case 2: + val = uint64(binary.BigEndian.Uint16(data[:2])) & 0x3fff + case 4: + val = uint64(binary.BigEndian.Uint32(data[:4])) & 0x3fffffff + case 8: + val = binary.BigEndian.Uint64(data[:8]) & 0x3fffffffffffffff + } + + return val, length, nil +} diff --git a/client/inspect/quic_test.go b/client/inspect/quic_test.go new file mode 100644 index 000000000..1290c0cf1 --- /dev/null +++ b/client/inspect/quic_test.go @@ -0,0 +1,99 @@ +package inspect + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestReadVarInt(t *testing.T) { + tests := []struct { + name string + data []byte + want uint64 + n int + }{ + { + name: "1 byte value", + data: []byte{0x25}, + want: 37, + n: 1, + }, + { + name: "2 byte value", + data: []byte{0x7b, 0xbd}, + want: 15293, + n: 2, + }, + { + name: "4 byte value", + data: []byte{0x9d, 0x7f, 0x3e, 0x7d}, + want: 494878333, + n: 4, + }, + { + name: "zero", + data: []byte{0x00}, + want: 0, + n: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + val, n, err := readVarInt(tt.data) + require.NoError(t, err) + assert.Equal(t, tt.want, val) + assert.Equal(t, tt.n, n) + }) + } +} + +func TestReadVarInt_Empty(t *testing.T) { + _, _, err := readVarInt(nil) + require.Error(t, err) +} + +func TestReadVarInt_Truncated(t *testing.T) { + // 2-byte prefix but only 1 byte + _, _, err := readVarInt([]byte{0x40}) + require.Error(t, err) +} + +func TestExtractQUICSNI_NotLongHeader(t *testing.T) { + // Short header packet (form bit not set) + data := make([]byte, 100) + data[0] = 0x40 // short header + + _, err := ExtractQUICSNI(data) + require.Error(t, err) + assert.Contains(t, err.Error(), "not a QUIC long header") +} + +func TestExtractQUICSNI_UnsupportedVersion(t *testing.T) { + data := make([]byte, 100) + data[0] = 0xC0 // long header + // Version 0xdeadbeef + data[1] = 0xde + data[2] = 0xad + data[3] = 0xbe + data[4] = 0xef + + _, err := ExtractQUICSNI(data) + require.Error(t, err) + assert.Contains(t, err.Error(), "unsupported QUIC version") +} + +func TestExtractQUICSNI_TooShort(t *testing.T) { + _, err := ExtractQUICSNI([]byte{0xC0, 0x00}) + require.Error(t, err) +} + +func TestHkdfExpandLabel(t *testing.T) { + // Smoke test: ensure it returns the right length and doesn't error + secret := make([]byte, 32) + result, err := hkdfExpandLabel(secret, "quic key", nil, 16) + require.NoError(t, err) + assert.Len(t, result, 16) +} diff --git a/client/inspect/rules.go b/client/inspect/rules.go new file mode 100644 index 000000000..39a2faf60 --- /dev/null +++ b/client/inspect/rules.go @@ -0,0 +1,253 @@ +package inspect + +import ( + "net/netip" + "slices" + "sort" + "strings" + "sync" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/internal/acl/id" + "github.com/netbirdio/netbird/shared/management/domain" +) + +// RuleEngine evaluates proxy rules against connection metadata. +// It is safe for concurrent use. +type RuleEngine struct { + mu sync.RWMutex + rules []Rule + // defaultAction applies when no rule matches. + defaultAction Action + log *log.Entry +} + +// NewRuleEngine creates a rule engine with the given default action. +func NewRuleEngine(logger *log.Entry, defaultAction Action) *RuleEngine { + return &RuleEngine{ + defaultAction: defaultAction, + log: logger, + } +} + +// UpdateRules replaces the rule set and default action. Rules are sorted by priority. +func (e *RuleEngine) UpdateRules(rules []Rule, defaultAction Action) { + sorted := make([]Rule, len(rules)) + copy(sorted, rules) + sort.Slice(sorted, func(i, j int) bool { + return sorted[i].Priority < sorted[j].Priority + }) + + e.mu.Lock() + e.rules = sorted + e.defaultAction = defaultAction + e.mu.Unlock() +} + +// EvalResult holds the outcome of a rule evaluation. +type EvalResult struct { + Action Action + RuleID id.RuleID +} + +// Evaluate determines the action for a connection based on the rule set. +// Pass empty path for connection-level evaluation (TLS/SNI), non-empty for request-level (HTTP). +func (e *RuleEngine) Evaluate(src netip.Addr, dstDomain domain.Domain, dstAddr netip.Addr, dstPort uint16, proto ProtoType, path string) Action { + r := e.EvaluateWithResult(src, dstDomain, dstAddr, dstPort, proto, path) + return r.Action +} + +// EvaluateWithResult is like Evaluate but also returns the matched rule ID. +func (e *RuleEngine) EvaluateWithResult(src netip.Addr, dstDomain domain.Domain, dstAddr netip.Addr, dstPort uint16, proto ProtoType, path string) EvalResult { + e.mu.RLock() + defer e.mu.RUnlock() + + for i := range e.rules { + rule := &e.rules[i] + if e.ruleMatches(rule, src, dstDomain, dstAddr, dstPort, proto, path) { + e.log.Tracef("rule %s matched: action=%s src=%s domain=%s dst=%s:%d proto=%s path=%s", + rule.ID, rule.Action, src, dstDomain.SafeString(), dstAddr, dstPort, proto, path) + return EvalResult{Action: rule.Action, RuleID: rule.ID} + } + } + + e.log.Tracef("no rule matched, default=%s: src=%s domain=%s dst=%s:%d proto=%s path=%s", + e.defaultAction, src, dstDomain.SafeString(), dstAddr, dstPort, proto, path) + return EvalResult{Action: e.defaultAction} +} + +// HasPathRulesForDomain returns true if any rule matching the domain has non-empty Paths. +// Used to force MITM inspection when path-level rules exist (paths are only visible after decryption). +func (e *RuleEngine) HasPathRulesForDomain(dstDomain domain.Domain) bool { + e.mu.RLock() + defer e.mu.RUnlock() + + for i := range e.rules { + if len(e.rules[i].Paths) > 0 && e.matchDomain(&e.rules[i], dstDomain) { + return true + } + } + return false +} + +// ruleMatches checks whether all non-empty fields of a rule match. +// Empty fields are treated as "match any". +// All specified fields must match (AND logic). +func (e *RuleEngine) ruleMatches(rule *Rule, src netip.Addr, dstDomain domain.Domain, dstAddr netip.Addr, dstPort uint16, proto ProtoType, path string) bool { + if !e.matchSource(rule, src) { + return false + } + + if !e.matchDomain(rule, dstDomain) { + return false + } + + if !e.matchNetwork(rule, dstAddr) { + return false + } + + if !e.matchPort(rule, dstPort) { + return false + } + + if !e.matchProtocol(rule, proto) { + return false + } + + if !e.matchPaths(rule, path) { + return false + } + + return true +} + +// matchSource returns true if src matches any of the rule's source CIDRs, +// or if no source CIDRs are specified (match any). +func (e *RuleEngine) matchSource(rule *Rule, src netip.Addr) bool { + if len(rule.Sources) == 0 { + return true + } + + for _, prefix := range rule.Sources { + if prefix.Contains(src) { + return true + } + } + + return false +} + +// matchDomain returns true if dstDomain matches any of the rule's domain patterns, +// or if no domain patterns are specified (match any). +func (e *RuleEngine) matchDomain(rule *Rule, dstDomain domain.Domain) bool { + if len(rule.Domains) == 0 { + return true + } + + // If we have domain rules but no domain to match against (e.g., raw IP connection), + // the domain condition does not match. + if dstDomain == "" { + return false + } + + for _, pattern := range rule.Domains { + if MatchDomain(pattern, dstDomain) { + return true + } + } + + return false +} + +// matchNetwork returns true if dstAddr is within any of the rule's destination CIDRs, +// or if no destination CIDRs are specified (match any). +func (e *RuleEngine) matchNetwork(rule *Rule, dstAddr netip.Addr) bool { + if len(rule.Networks) == 0 { + return true + } + + for _, prefix := range rule.Networks { + if prefix.Contains(dstAddr) { + return true + } + } + + return false +} + +// matchProtocol returns true if proto matches any of the rule's protocols, +// or if no protocols are specified (match any). +func (e *RuleEngine) matchProtocol(rule *Rule, proto ProtoType) bool { + if len(rule.Protocols) == 0 { + return true + } + + for _, p := range rule.Protocols { + if p == proto { + return true + } + } + + return false +} + +// matchPort returns true if dstPort matches any of the rule's destination ports, +// or if no ports are specified (match any). +func (e *RuleEngine) matchPort(rule *Rule, dstPort uint16) bool { + if len(rule.Ports) == 0 { + return true + } + + return slices.Contains(rule.Ports, dstPort) +} + +// matchPaths returns true if path matches any of the rule's path patterns, +// or if no paths are specified (match any). Empty path (connection-level eval) matches all. +func (e *RuleEngine) matchPaths(rule *Rule, path string) bool { + if len(rule.Paths) == 0 { + return true + } + // Connection-level (path=""): rules with paths don't match at connection level. + // HasPathRulesForDomain forces the connection to inspect, so paths are + // checked per-request once the HTTP request is visible. + if path == "" { + return false + } + for _, pattern := range rule.Paths { + if matchPath(pattern, path) { + return true + } + } + return false +} + +// matchPath checks if a URL path matches a pattern. +// Supports: exact ("/login"), prefix with wildcard ("/api/*"), +// and contains ("*/admin/*"). A bare "*" matches everything. +func matchPath(pattern, path string) bool { + if pattern == "*" { + return true + } + + hasLeadingStar := strings.HasPrefix(pattern, "*") + hasTrailingStar := strings.HasSuffix(pattern, "*") + + switch { + case hasLeadingStar && hasTrailingStar: + // */admin/* = contains + middle := strings.Trim(pattern, "*") + return strings.Contains(path, middle) + case hasTrailingStar: + // /api/* = prefix + prefix := strings.TrimSuffix(pattern, "*") + return strings.HasPrefix(path, prefix) + case hasLeadingStar: + // *.json = suffix + suffix := strings.TrimPrefix(pattern, "*") + return strings.HasSuffix(path, suffix) + default: + // exact + return path == pattern + } +} diff --git a/client/inspect/rules_test.go b/client/inspect/rules_test.go new file mode 100644 index 000000000..265b8e383 --- /dev/null +++ b/client/inspect/rules_test.go @@ -0,0 +1,338 @@ +package inspect + +import ( + "net/netip" + "testing" + + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/client/internal/acl/id" + "github.com/netbirdio/netbird/shared/management/domain" +) + +func testLogger() *log.Entry { + return log.WithField("test", true) +} + +func mustDomain(t *testing.T, s string) domain.Domain { + t.Helper() + d, err := domain.FromString(s) + require.NoError(t, err) + return d +} + +func TestRuleEngine_Evaluate(t *testing.T) { + tests := []struct { + name string + rules []Rule + defaultAction Action + src netip.Addr + dstDomain domain.Domain + dstAddr netip.Addr + dstPort uint16 + want Action + }{ + { + name: "no rules returns default allow", + defaultAction: ActionAllow, + src: netip.MustParseAddr("10.0.0.1"), + dstAddr: netip.MustParseAddr("1.2.3.4"), + dstPort: 443, + want: ActionAllow, + }, + { + name: "no rules returns default block", + defaultAction: ActionBlock, + src: netip.MustParseAddr("10.0.0.1"), + dstAddr: netip.MustParseAddr("1.2.3.4"), + dstPort: 443, + want: ActionBlock, + }, + { + name: "domain exact match blocks", + defaultAction: ActionAllow, + rules: []Rule{ + { + ID: id.RuleID("r1"), + Domains: []domain.Domain{mustDomain(t, "malware.example.com")}, + Action: ActionBlock, + }, + }, + src: netip.MustParseAddr("10.0.0.1"), + dstDomain: mustDomain(t, "malware.example.com"), + dstAddr: netip.MustParseAddr("1.2.3.4"), + dstPort: 443, + want: ActionBlock, + }, + { + name: "domain wildcard match blocks", + defaultAction: ActionAllow, + rules: []Rule{ + { + ID: id.RuleID("r1"), + Domains: []domain.Domain{mustDomain(t, "*.evil.com")}, + Action: ActionBlock, + }, + }, + src: netip.MustParseAddr("10.0.0.1"), + dstDomain: mustDomain(t, "phishing.evil.com"), + dstAddr: netip.MustParseAddr("1.2.3.4"), + dstPort: 443, + want: ActionBlock, + }, + { + name: "domain wildcard does not match base", + defaultAction: ActionAllow, + rules: []Rule{ + { + ID: id.RuleID("r1"), + Domains: []domain.Domain{mustDomain(t, "*.evil.com")}, + Action: ActionBlock, + }, + }, + src: netip.MustParseAddr("10.0.0.1"), + dstDomain: mustDomain(t, "evil.com"), + dstAddr: netip.MustParseAddr("1.2.3.4"), + dstPort: 443, + want: ActionAllow, + }, + { + name: "case insensitive domain match", + defaultAction: ActionAllow, + rules: []Rule{ + { + ID: id.RuleID("r1"), + Domains: []domain.Domain{mustDomain(t, "Example.COM")}, + Action: ActionBlock, + }, + }, + src: netip.MustParseAddr("10.0.0.1"), + dstDomain: mustDomain(t, "EXAMPLE.com"), + dstAddr: netip.MustParseAddr("1.2.3.4"), + dstPort: 443, + want: ActionBlock, + }, + { + name: "source CIDR match", + defaultAction: ActionAllow, + rules: []Rule{ + { + ID: id.RuleID("r1"), + Sources: []netip.Prefix{netip.MustParsePrefix("192.168.1.0/24")}, + Action: ActionInspect, + }, + }, + src: netip.MustParseAddr("192.168.1.50"), + dstAddr: netip.MustParseAddr("1.2.3.4"), + dstPort: 443, + want: ActionInspect, + }, + { + name: "source CIDR no match", + defaultAction: ActionAllow, + rules: []Rule{ + { + ID: id.RuleID("r1"), + Sources: []netip.Prefix{netip.MustParsePrefix("192.168.1.0/24")}, + Action: ActionBlock, + }, + }, + src: netip.MustParseAddr("10.0.0.5"), + dstAddr: netip.MustParseAddr("1.2.3.4"), + dstPort: 443, + want: ActionAllow, + }, + { + name: "destination network match", + defaultAction: ActionAllow, + rules: []Rule{ + { + ID: id.RuleID("r1"), + Networks: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/8")}, + Action: ActionInspect, + }, + }, + src: netip.MustParseAddr("192.168.1.1"), + dstAddr: netip.MustParseAddr("10.50.0.1"), + dstPort: 80, + want: ActionInspect, + }, + { + name: "port match", + defaultAction: ActionAllow, + rules: []Rule{ + { + ID: id.RuleID("r1"), + Ports: []uint16{443, 8443}, + Action: ActionInspect, + }, + }, + src: netip.MustParseAddr("10.0.0.1"), + dstAddr: netip.MustParseAddr("1.2.3.4"), + dstPort: 443, + want: ActionInspect, + }, + { + name: "port no match", + defaultAction: ActionAllow, + rules: []Rule{ + { + ID: id.RuleID("r1"), + Ports: []uint16{443, 8443}, + Action: ActionBlock, + }, + }, + src: netip.MustParseAddr("10.0.0.1"), + dstAddr: netip.MustParseAddr("1.2.3.4"), + dstPort: 22, + want: ActionAllow, + }, + { + name: "priority ordering first match wins", + defaultAction: ActionAllow, + rules: []Rule{ + { + ID: id.RuleID("allow-internal"), + Domains: []domain.Domain{mustDomain(t, "*.internal.corp")}, + Action: ActionAllow, + Priority: 1, + }, + { + ID: id.RuleID("inspect-all"), + Action: ActionInspect, + Priority: 10, + }, + }, + src: netip.MustParseAddr("10.0.0.1"), + dstDomain: mustDomain(t, "api.internal.corp"), + dstAddr: netip.MustParseAddr("10.1.0.5"), + dstPort: 443, + want: ActionAllow, + }, + { + name: "all fields must match (AND logic)", + defaultAction: ActionAllow, + rules: []Rule{ + { + ID: id.RuleID("r1"), + Sources: []netip.Prefix{netip.MustParsePrefix("192.168.1.0/24")}, + Domains: []domain.Domain{mustDomain(t, "*.evil.com")}, + Ports: []uint16{443}, + Action: ActionBlock, + }, + }, + // Source matches, domain matches, but port doesn't + src: netip.MustParseAddr("192.168.1.10"), + dstDomain: mustDomain(t, "phish.evil.com"), + dstAddr: netip.MustParseAddr("1.2.3.4"), + dstPort: 8080, + want: ActionAllow, + }, + { + name: "empty domain with domain rule does not match", + defaultAction: ActionAllow, + rules: []Rule{ + { + ID: id.RuleID("r1"), + Domains: []domain.Domain{mustDomain(t, "example.com")}, + Action: ActionBlock, + }, + }, + src: netip.MustParseAddr("10.0.0.1"), + dstDomain: "", // raw IP connection, no SNI + dstAddr: netip.MustParseAddr("1.2.3.4"), + dstPort: 443, + want: ActionAllow, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + engine := NewRuleEngine(testLogger(), tt.defaultAction) + engine.UpdateRules(tt.rules, tt.defaultAction) + + got := engine.Evaluate(tt.src, tt.dstDomain, tt.dstAddr, tt.dstPort, "", "") + assert.Equal(t, tt.want, got) + }) + } +} + +func TestRuleEngine_ProtocolMatching(t *testing.T) { + engine := NewRuleEngine(testLogger(), ActionAllow) + engine.UpdateRules([]Rule{ + { + ID: "block-websocket", + Protocols: []ProtoType{ProtoWebSocket}, + Action: ActionBlock, + Priority: 1, + }, + { + ID: "inspect-h2", + Protocols: []ProtoType{ProtoH2}, + Action: ActionInspect, + Priority: 2, + }, + }, ActionAllow) + + src := netip.MustParseAddr("10.0.0.1") + dst := netip.MustParseAddr("1.2.3.4") + + // WebSocket: blocked by rule + assert.Equal(t, ActionBlock, engine.Evaluate(src, "", dst, 443, ProtoWebSocket, "")) + + // HTTP/2: inspected by rule + assert.Equal(t, ActionInspect, engine.Evaluate(src, "", dst, 443, ProtoH2, "")) + + // Plain HTTP: no protocol rule matches, default allow + assert.Equal(t, ActionAllow, engine.Evaluate(src, "", dst, 80, ProtoHTTP, "")) + + // HTTPS: no protocol rule matches, default allow + assert.Equal(t, ActionAllow, engine.Evaluate(src, "", dst, 443, ProtoHTTPS, "")) + + // QUIC/H3: no protocol rule matches, default allow + assert.Equal(t, ActionAllow, engine.Evaluate(src, "", dst, 443, ProtoH3, "")) + + // Empty protocol (unknown): no protocol rule matches, default allow + assert.Equal(t, ActionAllow, engine.Evaluate(src, "", dst, 443, "", "")) +} + +func TestRuleEngine_EmptyProtocolsMatchAll(t *testing.T) { + engine := NewRuleEngine(testLogger(), ActionAllow) + engine.UpdateRules([]Rule{ + { + ID: "block-all-protos", + Action: ActionBlock, + // No Protocols field = match all protocols + Priority: 1, + }, + }, ActionAllow) + + src := netip.MustParseAddr("10.0.0.1") + dst := netip.MustParseAddr("1.2.3.4") + + assert.Equal(t, ActionBlock, engine.Evaluate(src, "", dst, 443, ProtoHTTP, "")) + assert.Equal(t, ActionBlock, engine.Evaluate(src, "", dst, 443, ProtoHTTPS, "")) + assert.Equal(t, ActionBlock, engine.Evaluate(src, "", dst, 443, ProtoWebSocket, "")) + assert.Equal(t, ActionBlock, engine.Evaluate(src, "", dst, 443, ProtoH2, "")) + assert.Equal(t, ActionBlock, engine.Evaluate(src, "", dst, 443, "", "")) +} + +func TestRuleEngine_UpdateRulesSortsByPriority(t *testing.T) { + engine := NewRuleEngine(testLogger(), ActionAllow) + + engine.UpdateRules([]Rule{ + {ID: "c", Priority: 30, Action: ActionBlock}, + {ID: "a", Priority: 10, Action: ActionInspect}, + {ID: "b", Priority: 20, Action: ActionAllow}, + }, ActionAllow) + + engine.mu.RLock() + defer engine.mu.RUnlock() + + require.Len(t, engine.rules, 3) + assert.Equal(t, id.RuleID("a"), engine.rules[0].ID) + assert.Equal(t, id.RuleID("b"), engine.rules[1].ID) + assert.Equal(t, id.RuleID("c"), engine.rules[2].ID) +} diff --git a/client/inspect/sni.go b/client/inspect/sni.go new file mode 100644 index 000000000..1ba3a83cf --- /dev/null +++ b/client/inspect/sni.go @@ -0,0 +1,287 @@ +package inspect + +import ( + "encoding/binary" + "fmt" + "io" + + "github.com/netbirdio/netbird/shared/management/domain" +) + +const ( + recordTypeHandshake = 0x16 + handshakeTypeClientHello = 0x01 + extensionTypeSNI = 0x0000 + extensionTypeALPN = 0x0010 + sniTypeHostName = 0x00 + + // maxClientHelloSize is the maximum ClientHello size we'll read. + // Real-world ClientHellos are typically under 1KB but can reach ~16KB with + // many extensions (post-quantum key shares, etc.). + maxClientHelloSize = 16384 +) + +// ClientHelloInfo holds data extracted from a TLS ClientHello. +type ClientHelloInfo struct { + SNI domain.Domain + ALPN []string +} + +// isTLSHandshake reports whether the first byte indicates a TLS handshake record. +func isTLSHandshake(b byte) bool { + return b == recordTypeHandshake +} + +// httpMethods lists the first bytes of valid HTTP method tokens. +var httpMethods = [][]byte{ + []byte("GET "), + []byte("POST"), + []byte("PUT "), + []byte("DELE"), + []byte("HEAD"), + []byte("OPTI"), + []byte("PATC"), + []byte("CONN"), + []byte("TRAC"), +} + +// isHTTPMethod reports whether the peeked bytes look like the start of an HTTP request. +func isHTTPMethod(b []byte) bool { + if len(b) < 4 { + return false + } + for _, m := range httpMethods { + if b[0] == m[0] && b[1] == m[1] && b[2] == m[2] && b[3] == m[3] { + return true + } + } + return false +} + +// parseClientHello reads a TLS ClientHello from r and returns SNI and ALPN. +func parseClientHello(r io.Reader) (ClientHelloInfo, error) { + // TLS record header: type(1) + version(2) + length(2) + var recordHeader [5]byte + if _, err := io.ReadFull(r, recordHeader[:]); err != nil { + return ClientHelloInfo{}, fmt.Errorf("read TLS record header: %w", err) + } + + if recordHeader[0] != recordTypeHandshake { + return ClientHelloInfo{}, fmt.Errorf("not a TLS handshake record (type=%d)", recordHeader[0]) + } + + recordLen := int(binary.BigEndian.Uint16(recordHeader[3:5])) + if recordLen < 4 || recordLen > maxClientHelloSize { + return ClientHelloInfo{}, fmt.Errorf("invalid TLS record length: %d", recordLen) + } + + // Read the full handshake message + msg := make([]byte, recordLen) + if _, err := io.ReadFull(r, msg); err != nil { + return ClientHelloInfo{}, fmt.Errorf("read handshake message: %w", err) + } + + return parseClientHelloMsg(msg) +} + +// extractSNI reads a TLS ClientHello from r and returns the SNI hostname. +// Returns empty domain if no SNI extension is present. +func extractSNI(r io.Reader) (domain.Domain, error) { + info, err := parseClientHello(r) + return info.SNI, err +} + +// extractSNIFromBytes parses SNI from raw bytes that start with the TLS record header. +func extractSNIFromBytes(data []byte) (domain.Domain, error) { + info, err := parseClientHelloFromBytes(data) + return info.SNI, err +} + +// parseClientHelloFromBytes parses a ClientHello from raw bytes starting with the TLS record header. +func parseClientHelloFromBytes(data []byte) (ClientHelloInfo, error) { + if len(data) < 5 { + return ClientHelloInfo{}, fmt.Errorf("data too short for TLS record header") + } + + if data[0] != recordTypeHandshake { + return ClientHelloInfo{}, fmt.Errorf("not a TLS handshake record (type=%d)", data[0]) + } + + recordLen := int(binary.BigEndian.Uint16(data[3:5])) + if recordLen < 4 { + return ClientHelloInfo{}, fmt.Errorf("invalid TLS record length: %d", recordLen) + } + + end := 5 + recordLen + if end > len(data) { + return ClientHelloInfo{}, fmt.Errorf("TLS record truncated: need %d, have %d", end, len(data)) + } + + return parseClientHelloMsg(data[5:end]) +} + +// parseClientHelloMsg extracts SNI and ALPN from a raw ClientHello handshake message. +// msg starts at the handshake type byte. +func parseClientHelloMsg(msg []byte) (ClientHelloInfo, error) { + if len(msg) < 4 { + return ClientHelloInfo{}, fmt.Errorf("handshake message too short") + } + + if msg[0] != handshakeTypeClientHello { + return ClientHelloInfo{}, fmt.Errorf("not a ClientHello (type=%d)", msg[0]) + } + + // Handshake header: type(1) + length(3) + helloLen := int(msg[1])<<16 | int(msg[2])<<8 | int(msg[3]) + if helloLen+4 > len(msg) { + return ClientHelloInfo{}, fmt.Errorf("ClientHello truncated") + } + + hello := msg[4 : 4+helloLen] + return parseHelloBody(hello) +} + +// parseHelloBody parses the ClientHello body (after handshake header) +// and extracts SNI and ALPN. +func parseHelloBody(hello []byte) (ClientHelloInfo, error) { + // ClientHello structure: + // version(2) + random(32) + session_id_len(1) + session_id(var) + // + cipher_suites_len(2) + cipher_suites(var) + // + compression_len(1) + compression(var) + // + extensions_len(2) + extensions(var) + + var info ClientHelloInfo + + if len(hello) < 35 { + return info, fmt.Errorf("ClientHello body too short") + } + + pos := 2 + 32 // skip version + random + + // Skip session ID + if pos >= len(hello) { + return info, fmt.Errorf("ClientHello truncated at session ID") + } + sessionIDLen := int(hello[pos]) + pos += 1 + sessionIDLen + + // Skip cipher suites + if pos+2 > len(hello) { + return info, fmt.Errorf("ClientHello truncated at cipher suites") + } + cipherLen := int(binary.BigEndian.Uint16(hello[pos : pos+2])) + pos += 2 + cipherLen + + // Skip compression methods + if pos >= len(hello) { + return info, fmt.Errorf("ClientHello truncated at compression") + } + compLen := int(hello[pos]) + pos += 1 + compLen + + // Extensions + if pos+2 > len(hello) { + return info, nil + } + + extLen := int(binary.BigEndian.Uint16(hello[pos : pos+2])) + pos += 2 + + extEnd := pos + extLen + if extEnd > len(hello) { + return info, fmt.Errorf("extensions block truncated") + } + + // Walk extensions looking for SNI and ALPN + for pos+4 <= extEnd { + extType := binary.BigEndian.Uint16(hello[pos : pos+2]) + extDataLen := int(binary.BigEndian.Uint16(hello[pos+2 : pos+4])) + pos += 4 + + if pos+extDataLen > extEnd { + return info, fmt.Errorf("extension data truncated") + } + + switch extType { + case extensionTypeSNI: + sni, err := parseSNIExtension(hello[pos : pos+extDataLen]) + if err != nil { + return info, err + } + info.SNI = sni + case extensionTypeALPN: + info.ALPN = parseALPNExtension(hello[pos : pos+extDataLen]) + } + + pos += extDataLen + } + + return info, nil +} + +// parseALPNExtension parses the ALPN extension data and returns protocol names. +// ALPN extension: list_length(2) + entries (each: len(1) + protocol_name(var)) +func parseALPNExtension(data []byte) []string { + if len(data) < 2 { + return nil + } + + listLen := int(binary.BigEndian.Uint16(data[0:2])) + if listLen+2 > len(data) { + return nil + } + + var protocols []string + pos := 2 + end := 2 + listLen + + for pos < end { + if pos >= len(data) { + break + } + nameLen := int(data[pos]) + pos++ + if pos+nameLen > end { + break + } + protocols = append(protocols, string(data[pos:pos+nameLen])) + pos += nameLen + } + + return protocols +} + +// parseSNIExtension parses the SNI extension data and returns the hostname. +func parseSNIExtension(data []byte) (domain.Domain, error) { + // SNI extension: list_length(2) + entries + if len(data) < 2 { + return "", fmt.Errorf("SNI extension too short") + } + + listLen := int(binary.BigEndian.Uint16(data[0:2])) + if listLen+2 > len(data) { + return "", fmt.Errorf("SNI list truncated") + } + + pos := 2 + end := 2 + listLen + + for pos+3 <= end { + nameType := data[pos] + nameLen := int(binary.BigEndian.Uint16(data[pos+1 : pos+3])) + pos += 3 + + if pos+nameLen > end { + return "", fmt.Errorf("SNI name truncated") + } + + if nameType == sniTypeHostName { + hostname := string(data[pos : pos+nameLen]) + return domain.FromString(hostname) + } + + pos += nameLen + } + + return "", nil +} diff --git a/client/inspect/sni_test.go b/client/inspect/sni_test.go new file mode 100644 index 000000000..a90d86c18 --- /dev/null +++ b/client/inspect/sni_test.go @@ -0,0 +1,109 @@ +package inspect + +import ( + "bytes" + "crypto/tls" + "net" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestExtractSNI(t *testing.T) { + tests := []struct { + name string + sni string + wantSNI string + wantErr bool + }{ + { + name: "standard domain", + sni: "example.com", + wantSNI: "example.com", + }, + { + name: "subdomain", + sni: "api.staging.example.com", + wantSNI: "api.staging.example.com", + }, + { + name: "mixed case normalized to lowercase", + sni: "Example.COM", + wantSNI: "example.com", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + clientHello := buildClientHello(t, tt.sni) + + sni, err := extractSNI(bytes.NewReader(clientHello)) + if tt.wantErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.wantSNI, sni.PunycodeString()) + }) + } +} + +func TestExtractSNI_NotTLS(t *testing.T) { + // HTTP request instead of TLS + data := []byte("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") + _, err := extractSNI(bytes.NewReader(data)) + require.Error(t, err) + assert.Contains(t, err.Error(), "not a TLS handshake") +} + +func TestExtractSNI_Truncated(t *testing.T) { + // Just the record header, no body + data := []byte{0x16, 0x03, 0x01, 0x00, 0x05} + _, err := extractSNI(bytes.NewReader(data)) + require.Error(t, err) +} + +func TestExtractSNIFromBytes(t *testing.T) { + clientHello := buildClientHello(t, "test.example.com") + + sni, err := extractSNIFromBytes(clientHello) + require.NoError(t, err) + assert.Equal(t, "test.example.com", sni.PunycodeString()) +} + +// buildClientHello generates a real TLS ClientHello with the given SNI. +func buildClientHello(t *testing.T, serverName string) []byte { + t.Helper() + + // Use a pipe to capture the ClientHello bytes + clientConn, serverConn := net.Pipe() + + done := make(chan []byte, 1) + go func() { + buf := make([]byte, 4096) + n, _ := serverConn.Read(buf) + done <- buf[:n] + serverConn.Close() + }() + + tlsConn := tls.Client(clientConn, &tls.Config{ + ServerName: serverName, + InsecureSkipVerify: true, + }) + + // Trigger the handshake (will fail since server isn't TLS, but we capture the ClientHello) + go func() { + _ = tlsConn.Handshake() + tlsConn.Close() + }() + + clientHello := <-done + clientConn.Close() + + require.True(t, len(clientHello) > 5, "ClientHello too short") + require.Equal(t, byte(0x16), clientHello[0], "not a TLS handshake record") + + return clientHello +} diff --git a/client/inspect/tls.go b/client/inspect/tls.go new file mode 100644 index 000000000..05bade734 --- /dev/null +++ b/client/inspect/tls.go @@ -0,0 +1,287 @@ +package inspect + +import ( + "context" + "crypto/tls" + "fmt" + "io" + "net" + "net/netip" + + "github.com/netbirdio/netbird/shared/management/domain" +) + +// handleTLS processes a TLS connection for the kernel-mode path: extracts SNI, +// evaluates rules, and handles the connection internally. +// In envoy mode, allowed connections are forwarded to envoy instead of direct relay. +func (p *Proxy) handleTLS(ctx context.Context, pconn *peekConn, dst netip.AddrPort, src SourceInfo) error { + result, err := p.inspectTLS(ctx, pconn, dst, src) + if err != nil { + return err + } + + if result.PassthroughConn != nil { + p.mu.RLock() + envoy := p.envoy + p.mu.RUnlock() + + if envoy != nil { + return p.forwardToEnvoy(ctx, pconn, dst, src, envoy) + } + return p.tlsPassthrough(ctx, pconn, dst, "") + } + + return nil +} + +// inspectTLS extracts SNI, evaluates rules, and returns the result. +// For ActionAllow: returns the peekConn as PassthroughConn (caller relays). +// For ActionBlock/ActionInspect: handles internally and returns nil PassthroughConn. +func (p *Proxy) inspectTLS(ctx context.Context, pconn *peekConn, dst netip.AddrPort, src SourceInfo) (InspectResult, error) { + // The first 5 bytes (TLS record header) are already peeked. + // Extend to read the full TLS record so bytes remain in the buffer for passthrough. + peeked := pconn.Peeked() + recordLen := int(peeked[3])<<8 | int(peeked[4]) + if _, err := pconn.PeekMore(5 + recordLen); err != nil { + return InspectResult{}, fmt.Errorf("read TLS record: %w", err) + } + + hello, err := parseClientHelloFromBytes(pconn.Peeked()) + if err != nil { + return InspectResult{}, fmt.Errorf("parse ClientHello: %w", err) + } + + sni := hello.SNI + proto := protoFromALPN(hello.ALPN) + // Connection-level evaluation: pass empty path. + action := p.evaluateAction(src.IP, sni, dst, proto, "") + + // If any rule for this domain has path patterns, force inspect so paths can + // be checked per-request after MITM decryption. + if action == ActionAllow && p.rules.HasPathRulesForDomain(sni) { + p.log.Debugf("upgrading to inspect for %s (path rules exist)", sni.PunycodeString()) + action = ActionInspect + } + + // Snapshot cert provider under lock for use in this connection. + p.mu.RLock() + certs := p.certs + p.mu.RUnlock() + + switch action { + case ActionBlock: + p.log.Debugf("block: TLS to %s (SNI=%s)", dst, sni.PunycodeString()) + if certs != nil { + return InspectResult{Action: ActionBlock}, p.tlsBlockPage(ctx, pconn, sni, certs) + } + return InspectResult{Action: ActionBlock}, ErrBlocked + + case ActionAllow: + p.log.Tracef("allow: TLS passthrough to %s (SNI=%s)", dst, sni.PunycodeString()) + return InspectResult{Action: ActionAllow, PassthroughConn: pconn}, nil + + case ActionInspect: + if certs == nil { + p.log.Warnf("allow: %s (inspect requested but no MITM CA configured)", sni.PunycodeString()) + return InspectResult{Action: ActionAllow, PassthroughConn: pconn}, nil + } + err := p.tlsMITM(ctx, pconn, dst, sni, src, certs) + return InspectResult{Action: ActionInspect}, err + + default: + p.log.Warnf("block: unknown action %q for %s", action, sni.PunycodeString()) + return InspectResult{Action: ActionBlock}, ErrBlocked + } +} + +// tlsBlockPage completes a MITM TLS handshake with the client using a dynamic +// certificate, then serves an HTTP 403 block page so the user sees a clear +// message instead of a cryptic SSL error. +func (p *Proxy) tlsBlockPage(ctx context.Context, pconn *peekConn, sni domain.Domain, certs *CertProvider) error { + hostname := sni.PunycodeString() + + // Force HTTP/1.1 only: block pages are simple responses, no need for h2 + tlsCfg := certs.GetTLSConfig() + tlsCfg.NextProtos = []string{"http/1.1"} + clientTLS := tls.Server(pconn, tlsCfg) + if err := clientTLS.HandshakeContext(ctx); err != nil { + // Client may not trust our CA, handshake fails. That's expected. + return fmt.Errorf("block page TLS handshake for %s: %w", hostname, err) + } + defer func() { + if err := clientTLS.Close(); err != nil { + p.log.Debugf("close block page TLS for %s: %v", hostname, err) + } + }() + + writeBlockResponse(clientTLS, nil, sni) + return ErrBlocked +} + +// tlsPassthrough connects to the destination and relays encrypted traffic +// without decryption. The peeked ClientHello bytes are replayed. +func (p *Proxy) tlsPassthrough(ctx context.Context, pconn *peekConn, dst netip.AddrPort, sni domain.Domain) error { + remote, err := p.dialTCP(ctx, dst) + if err != nil { + return fmt.Errorf("dial %s: %w", dst, err) + } + defer func() { + if err := remote.Close(); err != nil { + p.log.Debugf("close remote for %s: %v", dst, err) + } + }() + + p.log.Tracef("allow: TLS passthrough to %s (SNI=%s)", dst, sni.PunycodeString()) + + return relay(ctx, pconn, remote) +} + +// tlsMITM terminates the client TLS connection with a dynamic certificate, +// establishes a new TLS connection to the real destination, and runs the +// HTTP inspection pipeline on the decrypted traffic. +func (p *Proxy) tlsMITM(ctx context.Context, pconn *peekConn, dst netip.AddrPort, sni domain.Domain, src SourceInfo, certs *CertProvider) error { + hostname := sni.PunycodeString() + + // TLS handshake with client using dynamic cert + clientTLS := tls.Server(pconn, certs.GetTLSConfig()) + if err := clientTLS.HandshakeContext(ctx); err != nil { + return fmt.Errorf("client TLS handshake for %s: %w", hostname, err) + } + defer func() { + if err := clientTLS.Close(); err != nil { + p.log.Debugf("close client TLS for %s: %v", hostname, err) + } + }() + + // TLS connection to real destination + remoteTLS, err := p.dialTLS(ctx, dst, hostname) + if err != nil { + return fmt.Errorf("dial TLS %s (%s): %w", dst, hostname, err) + } + defer func() { + if err := remoteTLS.Close(); err != nil { + p.log.Debugf("close remote TLS for %s: %v", hostname, err) + } + }() + + negotiatedProto := clientTLS.ConnectionState().NegotiatedProtocol + p.log.Tracef("inspect: MITM established for %s (proto=%s)", hostname, negotiatedProto) + + return p.inspectHTTP(ctx, clientTLS, remoteTLS, dst, sni, src, negotiatedProto) +} + +// dialTLS connects to the destination with TLS, verifying the real server certificate. +func (p *Proxy) dialTLS(ctx context.Context, dst netip.AddrPort, serverName string) (net.Conn, error) { + rawConn, err := p.dialTCP(ctx, dst) + if err != nil { + return nil, err + } + + tlsConn := tls.Client(rawConn, &tls.Config{ + ServerName: serverName, + NextProtos: []string{"h2", "http/1.1"}, + MinVersion: tls.VersionTLS12, + }) + + if err := tlsConn.HandshakeContext(ctx); err != nil { + if closeErr := rawConn.Close(); closeErr != nil { + p.log.Debugf("close raw conn after TLS handshake failure: %v", closeErr) + } + return nil, fmt.Errorf("TLS handshake with %s: %w", serverName, err) + } + + return tlsConn, nil +} + +// protoFromALPN maps TLS ALPN protocol names to proxy ProtoType. +// Falls back to ProtoHTTPS when no recognized ALPN is present. +func protoFromALPN(alpn []string) ProtoType { + for _, p := range alpn { + switch p { + case "h2": + return ProtoH2 + case "h3": // unlikely in TLS, but handle anyway + return ProtoH3 + } + } + // No ALPN or only "http/1.1": treat as HTTPS + return ProtoHTTPS +} + +// relay copies data bidirectionally between client and remote until one +// side closes or the context is cancelled. +func relay(ctx context.Context, client, remote net.Conn) error { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + errCh := make(chan error, 2) + + go func() { + _, err := io.Copy(remote, client) + cancel() + errCh <- err + }() + + go func() { + _, err := io.Copy(client, remote) + cancel() + errCh <- err + }() + + var firstErr error + for range 2 { + if err := <-errCh; err != nil && firstErr == nil { + if !isClosedErr(err) { + firstErr = err + } + } + } + + return firstErr +} + +// evaluateAction runs rule evaluation and resolves the effective action. +// Pass empty path for connection-level (TLS), non-empty for request-level (HTTP). +func (p *Proxy) evaluateAction(src netip.Addr, sni domain.Domain, dst netip.AddrPort, proto ProtoType, path string) Action { + return p.rules.Evaluate(src, sni, dst.Addr(), dst.Port(), proto, path) +} + +// dialTCP dials the destination, blocking connections to loopback, link-local, +// multicast, and WG overlay network addresses. +func (p *Proxy) dialTCP(ctx context.Context, dst netip.AddrPort) (net.Conn, error) { + ip := dst.Addr().Unmap() + if err := p.validateDialTarget(ip); err != nil { + return nil, fmt.Errorf("dial %s: %w", dst, err) + } + return p.dialer.DialContext(ctx, "tcp", dst.String()) +} + +// validateDialTarget blocks destinations that should never be dialed by the proxy. +// Mirrors the route validation in systemops.validateRoute. +func (p *Proxy) validateDialTarget(addr netip.Addr) error { + switch { + case !addr.IsValid(): + return fmt.Errorf("invalid address") + case addr.IsLoopback(): + return fmt.Errorf("loopback address not allowed") + case addr.IsLinkLocalUnicast(), addr.IsLinkLocalMulticast(), addr.IsInterfaceLocalMulticast(): + return fmt.Errorf("link-local address not allowed") + case addr.IsMulticast(): + return fmt.Errorf("multicast address not allowed") + case p.wgNetwork.IsValid() && p.wgNetwork.Contains(addr): + return fmt.Errorf("overlay network address not allowed") + case p.localIPs != nil && p.localIPs.IsLocalIP(addr): + return fmt.Errorf("local address not allowed") + } + return nil +} + +func isClosedErr(err error) bool { + if err == nil { + return false + } + return err == io.EOF || + err == io.ErrClosedPipe || + err == net.ErrClosed || + err == context.Canceled +} diff --git a/client/internal/connect.go b/client/internal/connect.go index bc2bd84d9..2ec7952d7 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -562,6 +562,9 @@ func createEngineConfig(key wgtypes.Key, config *profilemanager.Config, peerConf MTU: selectMTU(config.MTU, peerConfig.Mtu), LogPath: logPath, + InspectionCACertPath: config.InspectionCACertPath, + InspectionCAKeyPath: config.InspectionCAKeyPath, + ProfileConfig: config, } diff --git a/client/internal/engine.go b/client/internal/engine.go index be2d8bbf3..b4645ea0a 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -31,6 +31,7 @@ import ( "github.com/netbirdio/netbird/client/iface/device" nbnetstack "github.com/netbirdio/netbird/client/iface/netstack" "github.com/netbirdio/netbird/client/iface/udpmux" + "github.com/netbirdio/netbird/client/inspect" "github.com/netbirdio/netbird/client/internal/acl" "github.com/netbirdio/netbird/client/internal/debug" "github.com/netbirdio/netbird/client/internal/dns" @@ -136,6 +137,12 @@ type EngineConfig struct { MTU uint16 + // InspectionCACertPath is a local CA cert for transparent proxy MITM. + // Takes priority over management-pushed CA. + InspectionCACertPath string + // InspectionCAKeyPath is the corresponding private key. + InspectionCAKeyPath string + // for debug bundle generation ProfileConfig *profilemanager.Config @@ -222,6 +229,10 @@ type Engine struct { latestSyncResponse *mgmProto.SyncResponse flowManager nftypes.FlowManager + // transparentProxy is the transparent forward proxy for traffic inspection. + transparentProxy *inspect.Proxy + udpInspectionHookID string + // auto-update updateManager *updater.Manager @@ -1272,6 +1283,9 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error { fwdEntries := toRouteDomains(e.config.WgPrivateKey.PublicKey().String(), routes) e.updateDNSForwarder(dnsRouteFeatureFlag, fwdEntries) + // Transparent proxy + e.updateTransparentProxy(networkMap.GetTransparentProxyConfig()) + // Ingress forward rules forwardingRules, err := e.updateForwardRules(networkMap.GetForwardingRules()) if err != nil { @@ -1695,6 +1709,8 @@ func (e *Engine) parseNATExternalIPMappings() []string { func (e *Engine) close() { log.Debugf("removing Netbird interface %s", e.config.WgIfaceName) + e.stopTransparentProxy() + if e.wgInterface != nil { if err := e.wgInterface.Close(); err != nil { log.Errorf("failed closing Netbird interface %s %v", e.config.WgIfaceName, err) diff --git a/client/internal/engine_tproxy.go b/client/internal/engine_tproxy.go new file mode 100644 index 000000000..9bb9b7d75 --- /dev/null +++ b/client/internal/engine_tproxy.go @@ -0,0 +1,571 @@ +package internal + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "math/big" + "net/netip" + "net/url" + "os" + "strconv" + "time" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/firewall/uspfilter/forwarder" + "github.com/netbirdio/netbird/client/inspect" + "github.com/netbirdio/netbird/client/internal/acl/id" + "github.com/netbirdio/netbird/shared/management/domain" + mgmProto "github.com/netbirdio/netbird/shared/management/proto" +) + +// updateTransparentProxy processes transparent proxy configuration from the network map. +func (e *Engine) updateTransparentProxy(cfg *mgmProto.TransparentProxyConfig) { + if cfg == nil || !cfg.Enabled { + if cfg == nil { + log.Tracef("inspect: config is nil") + } else { + log.Tracef("inspect: config disabled") + } + // Only stop if explicitly disabled. Don't stop on nil config to avoid + // a gap during policy edits where management briefly pushes empty config. + if cfg != nil && !cfg.Enabled { + e.stopTransparentProxy() + } + return + } + + log.Debugf("inspect: config received: enabled=%v mode=%v default_action=%v rules=%d has_ca=%v", + cfg.Enabled, cfg.Mode, cfg.DefaultAction, len(cfg.Rules), len(cfg.CaCertPem) > 0) + + // BlockInbound prevents adding TPROXY rules since kernel TPROXY bypasses ACLs. + // The userspace forwarder path still works as it operates within the forwarder hook. + if e.config.BlockInbound { + log.Warnf("inspect: BlockInbound is set, skipping redirect rules (userspace path still active)") + } + + proxyConfig, err := toProxyConfig(cfg) + if err != nil { + log.Errorf("inspect: parse config: %v", err) + e.stopTransparentProxy() + return + } + + // CA priority: local config > management-pushed > auto-generated self-signed. + // Local wins over mgmt to prevent compromised management from injecting a CA. + e.resolveInspectionCA(&proxyConfig) + + if e.transparentProxy != nil { + // Mode change requires full recreate (envoy lifecycle, listener changes). + if proxyConfig.Mode != e.transparentProxy.Mode() { + log.Infof("inspect: mode changed to %s, recreating engine", proxyConfig.Mode) + e.stopTransparentProxy() + } else { + e.transparentProxy.UpdateConfig(proxyConfig) + e.syncTProxyRules(proxyConfig) + e.syncUDPInspectionHook() + return + } + } + + if e.wgInterface != nil { + proxyConfig.WGNetwork = e.wgInterface.Address().Network + proxyConfig.ListenAddr = netip.AddrPortFrom( + e.wgInterface.Address().IP.Unmap(), + proxyConfig.ListenAddr.Port(), + ) + } + + // Pass local IP checker for SSRF prevention + if checker, ok := e.firewall.(inspect.LocalIPChecker); ok { + proxyConfig.LocalIPChecker = checker + } + + p, err := inspect.New(e.ctx, log.WithField("component", "inspect"), proxyConfig) + if err != nil { + log.Errorf("inspect: start engine: %v", err) + return + } + e.transparentProxy = p + + e.attachProxyToForwarder(p) + e.syncTProxyRules(proxyConfig) + e.syncUDPInspectionHook() + + log.Infof("inspect: engine started (mode=%s, rules=%d)", proxyConfig.Mode, len(proxyConfig.Rules)) +} + +// stopTransparentProxy shuts down the transparent proxy and removes interception. +func (e *Engine) stopTransparentProxy() { + if e.transparentProxy == nil { + return + } + + e.attachProxyToForwarder(nil) + e.removeTProxyRule() + e.removeUDPInspectionHook() + + if err := e.transparentProxy.Close(); err != nil { + log.Debugf("inspect: close engine: %v", err) + } + e.transparentProxy = nil + + log.Info("inspect: engine stopped") +} + +const tproxyRuleID = "tproxy-redirect" + +// syncTProxyRules adds a TPROXY rule via the firewall manager to intercept +// matching traffic on the WG interface and redirect it to the proxy socket. +func (e *Engine) syncTProxyRules(config inspect.Config) { + if e.config.BlockInbound { + e.removeTProxyRule() + return + } + + var listenPort uint16 + if e.transparentProxy != nil { + listenPort = e.transparentProxy.ListenPort() + } + if listenPort == 0 { + e.removeTProxyRule() + return + } + + if e.firewall == nil { + return + } + + dstPorts := make([]uint16, len(config.RedirectPorts)) + copy(dstPorts, config.RedirectPorts) + + log.Debugf("inspect: syncing redirect rules: listen port %d, redirect ports %v, sources %v", + listenPort, dstPorts, config.RedirectSources) + + if err := e.firewall.AddTProxyRule(tproxyRuleID, config.RedirectSources, dstPorts, listenPort); err != nil { + log.Errorf("inspect: add redirect rule: %v", err) + return + } +} + +// removeTProxyRule removes the TPROXY redirect rule. +func (e *Engine) removeTProxyRule() { + if e.firewall == nil { + return + } + if err := e.firewall.RemoveTProxyRule(tproxyRuleID); err != nil { + log.Debugf("inspect: remove redirect rule: %v", err) + } +} + +// syncUDPInspectionHook registers a UDP packet hook on port 443 for QUIC SNI blocking. +// The hook is called by the USP filter for each UDP packet matching the port, +// allowing the inspection engine to extract QUIC SNI and block by domain. +func (e *Engine) syncUDPInspectionHook() { + e.removeUDPInspectionHook() + + if e.firewall == nil || e.transparentProxy == nil { + return + } + + p := e.transparentProxy + hookID := e.firewall.AddUDPInspectionHook(443, func(packet []byte) bool { + srcIP, dstIP, dstPort, udpPayload, ok := parseUDPPacket(packet) + if !ok { + return false + } + + src := inspect.SourceInfo{IP: srcIP} + dst := netip.AddrPortFrom(dstIP, dstPort) + action := p.HandleUDPPacket(udpPayload, dst, src) + return action == inspect.ActionBlock + }) + + e.udpInspectionHookID = hookID + log.Debugf("inspect: registered UDP inspection hook on port 443 (id=%s)", hookID) +} + +// removeUDPInspectionHook removes the QUIC inspection hook. +func (e *Engine) removeUDPInspectionHook() { + if e.udpInspectionHookID == "" || e.firewall == nil { + return + } + e.firewall.RemoveUDPInspectionHook(e.udpInspectionHookID) + e.udpInspectionHookID = "" +} + +// parseUDPPacket extracts source/destination IP, destination port, and UDP +// payload from a raw IP packet. Supports both IPv4 and IPv6. +func parseUDPPacket(packet []byte) (srcIP, dstIP netip.Addr, dstPort uint16, payload []byte, ok bool) { + if len(packet) < 1 { + return srcIP, dstIP, 0, nil, false + } + + version := packet[0] >> 4 + + var udpOffset int + switch version { + case 4: + if len(packet) < 20 { + return srcIP, dstIP, 0, nil, false + } + ihl := int(packet[0]&0x0f) * 4 + if len(packet) < ihl+8 { + return srcIP, dstIP, 0, nil, false + } + var srcOK, dstOK bool + srcIP, srcOK = netip.AddrFromSlice(packet[12:16]) + dstIP, dstOK = netip.AddrFromSlice(packet[16:20]) + if !srcOK || !dstOK { + return srcIP, dstIP, 0, nil, false + } + udpOffset = ihl + + case 6: + // IPv6 fixed header is 40 bytes. Next header must be UDP (17). + if len(packet) < 48 { // 40 header + 8 UDP + return srcIP, dstIP, 0, nil, false + } + nextHeader := packet[6] + if nextHeader != 17 { // not UDP (may have extension headers) + return srcIP, dstIP, 0, nil, false + } + var srcOK, dstOK bool + srcIP, srcOK = netip.AddrFromSlice(packet[8:24]) + dstIP, dstOK = netip.AddrFromSlice(packet[24:40]) + if !srcOK || !dstOK { + return srcIP, dstIP, 0, nil, false + } + udpOffset = 40 + + default: + return srcIP, dstIP, 0, nil, false + } + + srcIP = srcIP.Unmap() + dstIP = dstIP.Unmap() + dstPort = uint16(packet[udpOffset+2])<<8 | uint16(packet[udpOffset+3]) + payload = packet[udpOffset+8:] + + return srcIP, dstIP, dstPort, payload, true +} + +// attachProxyToForwarder sets or clears the proxy on the userspace forwarder. +func (e *Engine) attachProxyToForwarder(p *inspect.Proxy) { + type forwarderGetter interface { + GetForwarder() *forwarder.Forwarder + } + + if fg, ok := e.firewall.(forwarderGetter); ok { + if fwd := fg.GetForwarder(); fwd != nil { + fwd.SetProxy(p) + } + } +} + +// toProxyConfig converts a proto TransparentProxyConfig to the inspect.Config type. +func toProxyConfig(cfg *mgmProto.TransparentProxyConfig) (inspect.Config, error) { + config := inspect.Config{ + Enabled: cfg.Enabled, + DefaultAction: toProxyAction(cfg.DefaultAction), + } + + switch cfg.Mode { + case mgmProto.TransparentProxyMode_TP_MODE_ENVOY: + config.Mode = inspect.ModeEnvoy + case mgmProto.TransparentProxyMode_TP_MODE_EXTERNAL: + config.Mode = inspect.ModeExternal + default: + config.Mode = inspect.ModeBuiltin + } + + if cfg.ExternalProxyUrl != "" { + u, err := url.Parse(cfg.ExternalProxyUrl) + if err != nil { + return inspect.Config{}, fmt.Errorf("parse external proxy URL: %w", err) + } + config.ExternalURL = u + } + + for _, s := range cfg.RedirectSources { + prefix, err := netip.ParsePrefix(s) + if err != nil { + return inspect.Config{}, fmt.Errorf("parse redirect source %q: %w", s, err) + } + config.RedirectSources = append(config.RedirectSources, prefix) + } + + for _, p := range cfg.RedirectPorts { + config.RedirectPorts = append(config.RedirectPorts, uint16(p)) + } + + // TPROXY listen port: fixed default, overridable via env var. + if config.Mode == inspect.ModeBuiltin { + port := uint16(inspect.DefaultTProxyPort) + if v := os.Getenv("NB_TPROXY_PORT"); v != "" { + if p, err := strconv.ParseUint(v, 10, 16); err == nil { + port = uint16(p) + } else { + log.Warnf("invalid NB_TPROXY_PORT %q, using default %d", v, inspect.DefaultTProxyPort) + } + } + config.ListenAddr = netip.AddrPortFrom(netip.IPv4Unspecified(), port) + } + + for _, r := range cfg.Rules { + rule, err := toProxyRule(r) + if err != nil { + return inspect.Config{}, fmt.Errorf("parse rule %q: %w", r.Id, err) + } + config.Rules = append(config.Rules, rule) + } + + if cfg.Icap != nil { + icapCfg, err := toICAPConfig(cfg.Icap) + if err != nil { + return inspect.Config{}, fmt.Errorf("parse ICAP config: %w", err) + } + config.ICAP = icapCfg + } + + if len(cfg.CaCertPem) > 0 && len(cfg.CaKeyPem) > 0 { + tlsCfg, err := parseTLSConfig(cfg.CaCertPem, cfg.CaKeyPem) + if err != nil { + return inspect.Config{}, fmt.Errorf("parse TLS config: %w", err) + } + config.TLS = tlsCfg + } + + if config.Mode == inspect.ModeEnvoy { + envCfg := &inspect.EnvoyConfig{ + BinaryPath: cfg.EnvoyBinaryPath, + AdminPort: uint16(cfg.EnvoyAdminPort), + } + if cfg.EnvoySnippets != nil { + envCfg.Snippets = &inspect.EnvoySnippets{ + HTTPFilters: cfg.EnvoySnippets.HttpFilters, + NetworkFilters: cfg.EnvoySnippets.NetworkFilters, + Clusters: cfg.EnvoySnippets.Clusters, + } + } + config.Envoy = envCfg + } + + return config, nil +} + +func toProxyRule(r *mgmProto.TransparentProxyRule) (inspect.Rule, error) { + rule := inspect.Rule{ + ID: id.RuleID(r.Id), + Action: toProxyAction(r.Action), + Priority: int(r.Priority), + } + + for _, d := range r.Domains { + dom, err := domain.FromString(d) + if err != nil { + return inspect.Rule{}, fmt.Errorf("parse domain %q: %w", d, err) + } + rule.Domains = append(rule.Domains, dom) + } + + for _, n := range r.Networks { + prefix, err := netip.ParsePrefix(n) + if err != nil { + return inspect.Rule{}, fmt.Errorf("parse network %q: %w", n, err) + } + rule.Networks = append(rule.Networks, prefix) + } + + for _, p := range r.Ports { + rule.Ports = append(rule.Ports, uint16(p)) + } + + for _, proto := range r.Protocols { + rule.Protocols = append(rule.Protocols, toProxyProtoType(proto)) + } + + rule.Paths = r.Paths + + return rule, nil +} + +func toProxyProtoType(p mgmProto.TransparentProxyProtocol) inspect.ProtoType { + switch p { + case mgmProto.TransparentProxyProtocol_TP_PROTO_HTTP: + return inspect.ProtoHTTP + case mgmProto.TransparentProxyProtocol_TP_PROTO_HTTPS: + return inspect.ProtoHTTPS + case mgmProto.TransparentProxyProtocol_TP_PROTO_H2: + return inspect.ProtoH2 + case mgmProto.TransparentProxyProtocol_TP_PROTO_H3: + return inspect.ProtoH3 + case mgmProto.TransparentProxyProtocol_TP_PROTO_WEBSOCKET: + return inspect.ProtoWebSocket + case mgmProto.TransparentProxyProtocol_TP_PROTO_OTHER: + return inspect.ProtoOther + default: + return "" + } +} + +func toProxyAction(a mgmProto.TransparentProxyAction) inspect.Action { + switch a { + case mgmProto.TransparentProxyAction_TP_ACTION_BLOCK: + return inspect.ActionBlock + case mgmProto.TransparentProxyAction_TP_ACTION_INSPECT: + return inspect.ActionInspect + default: + return inspect.ActionAllow + } +} + +func toICAPConfig(cfg *mgmProto.TransparentProxyICAPConfig) (*inspect.ICAPConfig, error) { + icap := &inspect.ICAPConfig{ + MaxConnections: int(cfg.MaxConnections), + } + + if cfg.ReqmodUrl != "" { + u, err := url.Parse(cfg.ReqmodUrl) + if err != nil { + return nil, fmt.Errorf("parse ICAP reqmod URL: %w", err) + } + icap.ReqModURL = u + } + + if cfg.RespmodUrl != "" { + u, err := url.Parse(cfg.RespmodUrl) + if err != nil { + return nil, fmt.Errorf("parse ICAP respmod URL: %w", err) + } + icap.RespModURL = u + } + + return icap, nil +} + +func parseTLSConfig(certPEM, keyPEM []byte) (*inspect.TLSConfig, error) { + block, _ := pem.Decode(certPEM) + if block == nil { + return nil, fmt.Errorf("decode CA certificate PEM") + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, fmt.Errorf("parse CA certificate: %w", err) + } + + keyBlock, _ := pem.Decode(keyPEM) + if keyBlock == nil { + return nil, fmt.Errorf("decode CA key PEM") + } + + key, err := x509.ParseECPrivateKey(keyBlock.Bytes) + if err != nil { + // Try PKCS8 as fallback + pkcs8Key, pkcs8Err := x509.ParsePKCS8PrivateKey(keyBlock.Bytes) + if pkcs8Err != nil { + return nil, fmt.Errorf("parse CA private key (tried EC and PKCS8): %w", err) + } + return &inspect.TLSConfig{CA: cert, CAKey: pkcs8Key}, nil + } + + return &inspect.TLSConfig{CA: cert, CAKey: key}, nil +} + +// resolveInspectionCA sets the TLS config on the proxy config using priority: +// 1. Local config file CA (InspectionCACertPath/InspectionCAKeyPath) +// 2. Management-pushed CA (already parsed in toProxyConfig) +// 3. Auto-generated self-signed CA (ephemeral, for testing) +// Local always wins to prevent a compromised management server from injecting a CA. +func (e *Engine) resolveInspectionCA(config *inspect.Config) { + // 1. Local CA from config file or env vars + certPath := e.config.InspectionCACertPath + keyPath := e.config.InspectionCAKeyPath + if certPath == "" { + certPath = os.Getenv("NB_INSPECTION_CA_CERT") + } + if keyPath == "" { + keyPath = os.Getenv("NB_INSPECTION_CA_KEY") + } + if certPath != "" && keyPath != "" { + certPEM, err := os.ReadFile(certPath) + if err != nil { + log.Errorf("read local inspection CA cert %s: %v", certPath, err) + return + } + keyPEM, err := os.ReadFile(keyPath) + if err != nil { + log.Errorf("read local inspection CA key %s: %v", keyPath, err) + return + } + tlsCfg, err := parseTLSConfig(certPEM, keyPEM) + if err != nil { + log.Errorf("parse local inspection CA: %v", err) + return + } + log.Infof("inspect: using local CA from %s", certPath) + config.TLS = tlsCfg + return + } + + // 2. Management-pushed CA (already set by toProxyConfig) + if config.TLS != nil { + log.Infof("inspect: using management-pushed CA") + return + } + + // 3. Auto-generate self-signed CA for testing / accept-cert UX + tlsCfg, err := generateSelfSignedCA() + if err != nil { + log.Errorf("generate self-signed inspection CA: %v", err) + return + } + log.Infof("inspect: using auto-generated self-signed CA (clients will see certificate warnings)") + config.TLS = tlsCfg +} + +// generateSelfSignedCA creates an ephemeral ECDSA P-256 CA certificate. +// Clients will see certificate warnings but can choose to accept. +func generateSelfSignedCA() (*inspect.TLSConfig, error) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, fmt.Errorf("generate CA key: %w", err) + } + + serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + if err != nil { + return nil, fmt.Errorf("generate serial: %w", err) + } + + template := &x509.Certificate{ + SerialNumber: serial, + Subject: pkix.Name{ + Organization: []string{"NetBird Transparent Proxy"}, + CommonName: "NetBird Inspection CA (auto-generated)", + }, + NotBefore: time.Now().Add(-1 * time.Hour), + NotAfter: time.Now().Add(365 * 24 * time.Hour), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + IsCA: true, + MaxPathLen: 0, + } + + certDER, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) + if err != nil { + return nil, fmt.Errorf("create CA certificate: %w", err) + } + + cert, err := x509.ParseCertificate(certDER) + if err != nil { + return nil, fmt.Errorf("parse generated CA certificate: %w", err) + } + + return &inspect.TLSConfig{CA: cert, CAKey: key}, nil +} diff --git a/client/internal/engine_tproxy_test.go b/client/internal/engine_tproxy_test.go new file mode 100644 index 000000000..f4603e9c7 --- /dev/null +++ b/client/internal/engine_tproxy_test.go @@ -0,0 +1,279 @@ +package internal + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/client/inspect" + mgmProto "github.com/netbirdio/netbird/shared/management/proto" +) + +func TestToProxyConfig_Basic(t *testing.T) { + cfg := &mgmProto.TransparentProxyConfig{ + Enabled: true, + Mode: mgmProto.TransparentProxyMode_TP_MODE_BUILTIN, + DefaultAction: mgmProto.TransparentProxyAction_TP_ACTION_ALLOW, + RedirectSources: []string{ + "10.0.0.0/24", + "192.168.1.0/24", + }, + RedirectPorts: []uint32{80, 443}, + Rules: []*mgmProto.TransparentProxyRule{ + { + Id: "block-evil", + Domains: []string{"*.evil.com", "malware.example.com"}, + Action: mgmProto.TransparentProxyAction_TP_ACTION_BLOCK, + Priority: 1, + }, + { + Id: "inspect-internal", + Domains: []string{"*.internal.corp"}, + Networks: []string{"10.1.0.0/16"}, + Ports: []uint32{443, 8443}, + Action: mgmProto.TransparentProxyAction_TP_ACTION_INSPECT, + Priority: 10, + }, + }, + ListenPort: 8443, + } + + config, err := toProxyConfig(cfg) + require.NoError(t, err) + + assert.True(t, config.Enabled) + assert.Equal(t, inspect.ModeBuiltin, config.Mode) + assert.Equal(t, inspect.ActionAllow, config.DefaultAction) + + require.Len(t, config.RedirectSources, 2) + assert.Equal(t, "10.0.0.0/24", config.RedirectSources[0].String()) + assert.Equal(t, "192.168.1.0/24", config.RedirectSources[1].String()) + + require.Len(t, config.RedirectPorts, 2) + assert.Equal(t, uint16(80), config.RedirectPorts[0]) + assert.Equal(t, uint16(443), config.RedirectPorts[1]) + + require.Len(t, config.Rules, 2) + + // Rule 1: block evil domains + assert.Equal(t, "block-evil", string(config.Rules[0].ID)) + assert.Equal(t, inspect.ActionBlock, config.Rules[0].Action) + assert.Equal(t, 1, config.Rules[0].Priority) + require.Len(t, config.Rules[0].Domains, 2) + assert.Equal(t, "*.evil.com", config.Rules[0].Domains[0].PunycodeString()) + assert.Equal(t, "malware.example.com", config.Rules[0].Domains[1].PunycodeString()) + + // Rule 2: inspect internal + assert.Equal(t, "inspect-internal", string(config.Rules[1].ID)) + assert.Equal(t, inspect.ActionInspect, config.Rules[1].Action) + assert.Equal(t, 10, config.Rules[1].Priority) + require.Len(t, config.Rules[1].Networks, 1) + assert.Equal(t, "10.1.0.0/16", config.Rules[1].Networks[0].String()) + require.Len(t, config.Rules[1].Ports, 2) + + // Listen address + assert.True(t, config.ListenAddr.IsValid()) + assert.Equal(t, uint16(8443), config.ListenAddr.Port()) +} + +func TestToProxyConfig_ExternalMode(t *testing.T) { + cfg := &mgmProto.TransparentProxyConfig{ + Enabled: true, + Mode: mgmProto.TransparentProxyMode_TP_MODE_EXTERNAL, + ExternalProxyUrl: "http://proxy.corp:8080", + DefaultAction: mgmProto.TransparentProxyAction_TP_ACTION_BLOCK, + } + + config, err := toProxyConfig(cfg) + require.NoError(t, err) + + assert.Equal(t, inspect.ModeExternal, config.Mode) + assert.Equal(t, inspect.ActionBlock, config.DefaultAction) + require.NotNil(t, config.ExternalURL) + assert.Equal(t, "http", config.ExternalURL.Scheme) + assert.Equal(t, "proxy.corp:8080", config.ExternalURL.Host) +} + +func TestToProxyConfig_ICAP(t *testing.T) { + cfg := &mgmProto.TransparentProxyConfig{ + Enabled: true, + Icap: &mgmProto.TransparentProxyICAPConfig{ + ReqmodUrl: "icap://icap-server:1344/reqmod", + RespmodUrl: "icap://icap-server:1344/respmod", + MaxConnections: 16, + }, + } + + config, err := toProxyConfig(cfg) + require.NoError(t, err) + + require.NotNil(t, config.ICAP) + assert.Equal(t, "icap", config.ICAP.ReqModURL.Scheme) + assert.Equal(t, "icap-server:1344", config.ICAP.ReqModURL.Host) + assert.Equal(t, "/reqmod", config.ICAP.ReqModURL.Path) + assert.Equal(t, "/respmod", config.ICAP.RespModURL.Path) + assert.Equal(t, 16, config.ICAP.MaxConnections) +} + +func TestToProxyConfig_Empty(t *testing.T) { + cfg := &mgmProto.TransparentProxyConfig{ + Enabled: true, + } + + config, err := toProxyConfig(cfg) + require.NoError(t, err) + + assert.True(t, config.Enabled) + assert.Equal(t, inspect.ModeBuiltin, config.Mode) + assert.Equal(t, inspect.ActionAllow, config.DefaultAction) + assert.Empty(t, config.RedirectSources) + assert.Empty(t, config.RedirectPorts) + assert.Empty(t, config.Rules) + assert.Nil(t, config.ICAP) + assert.Nil(t, config.TLS) + assert.False(t, config.ListenAddr.IsValid()) +} + +func TestToProxyConfig_InvalidSource(t *testing.T) { + cfg := &mgmProto.TransparentProxyConfig{ + Enabled: true, + RedirectSources: []string{"not-a-cidr"}, + } + + _, err := toProxyConfig(cfg) + require.Error(t, err) + assert.Contains(t, err.Error(), "parse redirect source") +} + +func TestToProxyConfig_InvalidNetwork(t *testing.T) { + cfg := &mgmProto.TransparentProxyConfig{ + Enabled: true, + Rules: []*mgmProto.TransparentProxyRule{ + { + Id: "bad", + Networks: []string{"not-a-cidr"}, + }, + }, + } + + _, err := toProxyConfig(cfg) + require.Error(t, err) + assert.Contains(t, err.Error(), "parse network") +} + +func TestToProxyAction(t *testing.T) { + assert.Equal(t, inspect.ActionAllow, toProxyAction(mgmProto.TransparentProxyAction_TP_ACTION_ALLOW)) + assert.Equal(t, inspect.ActionBlock, toProxyAction(mgmProto.TransparentProxyAction_TP_ACTION_BLOCK)) + assert.Equal(t, inspect.ActionInspect, toProxyAction(mgmProto.TransparentProxyAction_TP_ACTION_INSPECT)) + // Unknown defaults to allow + assert.Equal(t, inspect.ActionAllow, toProxyAction(99)) +} + +func TestParseUDPPacket_IPv4(t *testing.T) { + // Build a minimal IPv4/UDP packet: 20-byte IPv4 header + 8-byte UDP header + payload + packet := make([]byte, 20+8+4) + + // IPv4 header: version=4, IHL=5 (20 bytes) + packet[0] = 0x45 + // Protocol = UDP (17) + packet[9] = 17 + // Source IP: 10.0.0.1 + packet[12], packet[13], packet[14], packet[15] = 10, 0, 0, 1 + // Dest IP: 192.168.1.1 + packet[16], packet[17], packet[18], packet[19] = 192, 168, 1, 1 + // UDP source port: 54321 (0xD431) + packet[20] = 0xD4 + packet[21] = 0x31 + // UDP dest port: 443 (0x01BB) + packet[22] = 0x01 + packet[23] = 0xBB + // Payload + packet[28] = 0xDE + packet[29] = 0xAD + packet[30] = 0xBE + packet[31] = 0xEF + + srcIP, dstIP, dstPort, payload, ok := parseUDPPacket(packet) + require.True(t, ok) + assert.Equal(t, "10.0.0.1", srcIP.String()) + assert.Equal(t, "192.168.1.1", dstIP.String()) + assert.Equal(t, uint16(443), dstPort) + assert.Equal(t, []byte{0xDE, 0xAD, 0xBE, 0xEF}, payload) +} + +func TestParseUDPPacket_IPv6(t *testing.T) { + // Build a minimal IPv6/UDP packet: 40-byte IPv6 header + 8-byte UDP header + payload + packet := make([]byte, 40+8+4) + + // Version = 6 (0x60 in high nibble) + packet[0] = 0x60 + // Payload length: 8 (UDP header) + 4 (payload) + packet[4] = 0 + packet[5] = 12 + // Next header: UDP (17) + packet[6] = 17 + // Source: 2001:db8::1 + packet[8] = 0x20 + packet[9] = 0x01 + packet[10] = 0x0d + packet[11] = 0xb8 + packet[23] = 0x01 + // Dest: 2001:db8::2 + packet[24] = 0x20 + packet[25] = 0x01 + packet[26] = 0x0d + packet[27] = 0xb8 + packet[39] = 0x02 + // UDP source port: 54321 (0xD431) + packet[40] = 0xD4 + packet[41] = 0x31 + // UDP dest port: 443 (0x01BB) + packet[42] = 0x01 + packet[43] = 0xBB + // Payload + packet[48] = 0xCA + packet[49] = 0xFE + packet[50] = 0xBA + packet[51] = 0xBE + + srcIP, dstIP, dstPort, payload, ok := parseUDPPacket(packet) + require.True(t, ok) + assert.Equal(t, "2001:db8::1", srcIP.String()) + assert.Equal(t, "2001:db8::2", dstIP.String()) + assert.Equal(t, uint16(443), dstPort) + assert.Equal(t, []byte{0xCA, 0xFE, 0xBA, 0xBE}, payload) +} + +func TestParseUDPPacket_TooShort(t *testing.T) { + _, _, _, _, ok := parseUDPPacket(nil) + assert.False(t, ok) + + _, _, _, _, ok = parseUDPPacket([]byte{0x45, 0x00}) + assert.False(t, ok) +} + +func TestParseUDPPacket_IPv6ExtensionHeader(t *testing.T) { + // IPv6 with next header != UDP should be rejected + packet := make([]byte, 48) + packet[0] = 0x60 + packet[6] = 6 // TCP, not UDP + _, _, _, _, ok := parseUDPPacket(packet) + assert.False(t, ok, "should reject IPv6 packets with non-UDP next header") +} + +func TestParseUDPPacket_IPv4MappedIPv6(t *testing.T) { + // IPv4 packet with normal addresses should Unmap correctly + packet := make([]byte, 28) + packet[0] = 0x45 + packet[9] = 17 + packet[12], packet[13], packet[14], packet[15] = 127, 0, 0, 1 + packet[16], packet[17], packet[18], packet[19] = 10, 0, 0, 1 + packet[22] = 0x01 + packet[23] = 0xBB + + srcIP, dstIP, _, _, ok := parseUDPPacket(packet) + require.True(t, ok) + assert.True(t, srcIP.Is4(), "should be plain IPv4, not mapped") + assert.True(t, dstIP.Is4(), "should be plain IPv4, not mapped") +} diff --git a/client/internal/profilemanager/config.go b/client/internal/profilemanager/config.go index 20c615d57..6ffe756de 100644 --- a/client/internal/profilemanager/config.go +++ b/client/internal/profilemanager/config.go @@ -97,6 +97,9 @@ type ConfigInput struct { LazyConnectionEnabled *bool MTU *uint16 + + InspectionCACertPath string + InspectionCAKeyPath string } // Config Configuration type @@ -171,6 +174,13 @@ type Config struct { LazyConnectionEnabled bool MTU uint16 + + // InspectionCACertPath is the path to a PEM CA certificate for transparent proxy MITM. + // Local CA takes priority over management-pushed CA. + InspectionCACertPath string + + // InspectionCAKeyPath is the path to the PEM CA private key for transparent proxy MITM. + InspectionCAKeyPath string } var ConfigDirOverride string @@ -603,6 +613,17 @@ func (config *Config) apply(input ConfigInput) (updated bool, err error) { updated = true } + if input.InspectionCACertPath != "" && input.InspectionCACertPath != config.InspectionCACertPath { + log.Infof("updating inspection CA cert path to %s", input.InspectionCACertPath) + config.InspectionCACertPath = input.InspectionCACertPath + updated = true + } + if input.InspectionCAKeyPath != "" && input.InspectionCAKeyPath != config.InspectionCAKeyPath { + log.Infof("updating inspection CA key path to %s", input.InspectionCAKeyPath) + config.InspectionCAKeyPath = input.InspectionCAKeyPath + updated = true + } + return updated, nil } diff --git a/management/internals/shared/grpc/conversion.go b/management/internals/shared/grpc/conversion.go index ef417d3cf..15784d71d 100644 --- a/management/internals/shared/grpc/conversion.go +++ b/management/internals/shared/grpc/conversion.go @@ -156,6 +156,10 @@ func ToSyncResponse(ctx context.Context, config *nbconfig.Config, httpConfig *nb response.NetworkMap.ForwardingRules = forwardingRules } + if networkMap.TransparentProxyConfig != nil { + response.NetworkMap.TransparentProxyConfig = networkMap.TransparentProxyConfig.ToProto() + } + if networkMap.AuthorizedUsers != nil { hashedUsers, machineUsers := buildAuthorizedUsersProto(ctx, networkMap.AuthorizedUsers) userIDClaim := auth.DefaultUserIDClaim diff --git a/management/server/account/manager.go b/management/server/account/manager.go index b4516d512..e884b952e 100644 --- a/management/server/account/manager.go +++ b/management/server/account/manager.go @@ -113,6 +113,10 @@ type Manager interface { SavePostureChecks(ctx context.Context, accountID, userID string, postureChecks *posture.Checks, create bool) (*posture.Checks, error) DeletePostureChecks(ctx context.Context, accountID, postureChecksID, userID string) error ListPostureChecks(ctx context.Context, accountID, userID string) ([]*posture.Checks, error) + GetInspectionPolicy(ctx context.Context, accountID, policyID, userID string) (*types.InspectionPolicy, error) + SaveInspectionPolicy(ctx context.Context, accountID, userID string, policy *types.InspectionPolicy, create bool) (*types.InspectionPolicy, error) + DeleteInspectionPolicy(ctx context.Context, accountID, policyID, userID string) error + ListInspectionPolicies(ctx context.Context, accountID, userID string) ([]*types.InspectionPolicy, error) GetIdpManager() idp.Manager UpdateIntegratedValidator(ctx context.Context, accountID, userID, validator string, groups []string) error GroupValidation(ctx context.Context, accountId string, groups []string) (bool, error) diff --git a/management/server/http/handler.go b/management/server/http/handler.go index ad36b9d46..dd9a6f0f0 100644 --- a/management/server/http/handler.go +++ b/management/server/http/handler.go @@ -51,6 +51,7 @@ import ( "github.com/netbirdio/netbird/management/server/http/handlers/instance" "github.com/netbirdio/netbird/management/server/http/handlers/networks" "github.com/netbirdio/netbird/management/server/http/handlers/peers" + inspectionHandler "github.com/netbirdio/netbird/management/server/http/handlers/inspection" "github.com/netbirdio/netbird/management/server/http/handlers/policies" "github.com/netbirdio/netbird/management/server/http/handlers/routes" "github.com/netbirdio/netbird/management/server/http/handlers/setup_keys" @@ -162,6 +163,7 @@ func NewAPIHandler(ctx context.Context, accountManager account.Manager, networks setup_keys.AddEndpoints(accountManager, router) policies.AddEndpoints(accountManager, LocationManager, router) policies.AddPostureCheckEndpoints(accountManager, LocationManager, router) + inspectionHandler.AddEndpoints(accountManager, router) policies.AddLocationsEndpoints(accountManager, LocationManager, permissionsManager, router) groups.AddEndpoints(accountManager, router) routes.AddEndpoints(accountManager, router) diff --git a/management/server/http/handlers/inspection/handler.go b/management/server/http/handlers/inspection/handler.go new file mode 100644 index 000000000..6658f299d --- /dev/null +++ b/management/server/http/handlers/inspection/handler.go @@ -0,0 +1,288 @@ +package inspection + +import ( + "encoding/json" + "net/http" + + "github.com/gorilla/mux" + + "github.com/netbirdio/netbird/management/server/account" + nbcontext "github.com/netbirdio/netbird/management/server/context" + "github.com/netbirdio/netbird/management/server/types" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" + "github.com/netbirdio/netbird/shared/management/status" +) + +// Handler manages inspection policy CRUD operations. +type Handler struct { + accountManager account.Manager +} + +// AddEndpoints registers the inspection policy API endpoints. +func AddEndpoints(accountManager account.Manager, router *mux.Router) { + h := &Handler{accountManager: accountManager} + router.HandleFunc("/inspection-policies", h.list).Methods("GET", "OPTIONS") + router.HandleFunc("/inspection-policies", h.create).Methods("POST", "OPTIONS") + router.HandleFunc("/inspection-policies/{policyId}", h.get).Methods("GET", "OPTIONS") + router.HandleFunc("/inspection-policies/{policyId}", h.update).Methods("PUT", "OPTIONS") + router.HandleFunc("/inspection-policies/{policyId}", h.remove).Methods("DELETE", "OPTIONS") +} + +func (h *Handler) list(w http.ResponseWriter, r *http.Request) { + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) + if err != nil { + util.WriteError(r.Context(), err, w) + return + } + + policies, err := h.accountManager.ListInspectionPolicies(r.Context(), userAuth.AccountId, userAuth.UserId) + if err != nil { + util.WriteError(r.Context(), err, w) + return + } + + result := make([]*api.InspectionPolicy, 0, len(policies)) + for _, p := range policies { + result = append(result, toAPIResponse(p)) + } + + util.WriteJSONObject(r.Context(), w, result) +} + +func (h *Handler) create(w http.ResponseWriter, r *http.Request) { + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) + if err != nil { + util.WriteError(r.Context(), err, w) + return + } + + var req api.InspectionPolicyMinimum + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "decode request: %v", err), w) + return + } + + policy := fromAPIRequest(&req) + + saved, err := h.accountManager.SaveInspectionPolicy(r.Context(), userAuth.AccountId, userAuth.UserId, policy, true) + if err != nil { + util.WriteError(r.Context(), err, w) + return + } + + util.WriteJSONObject(r.Context(), w, toAPIResponse(saved)) +} + +func (h *Handler) get(w http.ResponseWriter, r *http.Request) { + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) + if err != nil { + util.WriteError(r.Context(), err, w) + return + } + + policyID := mux.Vars(r)["policyId"] + + policy, err := h.accountManager.GetInspectionPolicy(r.Context(), userAuth.AccountId, policyID, userAuth.UserId) + if err != nil { + util.WriteError(r.Context(), err, w) + return + } + + util.WriteJSONObject(r.Context(), w, toAPIResponse(policy)) +} + +func (h *Handler) update(w http.ResponseWriter, r *http.Request) { + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) + if err != nil { + util.WriteError(r.Context(), err, w) + return + } + + policyID := mux.Vars(r)["policyId"] + + var req api.InspectionPolicyMinimum + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "decode request: %v", err), w) + return + } + + policy := fromAPIRequest(&req) + policy.ID = policyID + + saved, err := h.accountManager.SaveInspectionPolicy(r.Context(), userAuth.AccountId, userAuth.UserId, policy, false) + if err != nil { + util.WriteError(r.Context(), err, w) + return + } + + util.WriteJSONObject(r.Context(), w, toAPIResponse(saved)) +} + +func (h *Handler) remove(w http.ResponseWriter, r *http.Request) { + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) + if err != nil { + util.WriteError(r.Context(), err, w) + return + } + + policyID := mux.Vars(r)["policyId"] + + if err := h.accountManager.DeleteInspectionPolicy(r.Context(), userAuth.AccountId, policyID, userAuth.UserId); err != nil { + util.WriteError(r.Context(), err, w) + return + } + + util.WriteJSONObject(r.Context(), w, struct{}{}) +} + +func toAPIResponse(p *types.InspectionPolicy) *api.InspectionPolicy { + id := p.ID + resp := &api.InspectionPolicy{ + Id: &id, + Name: p.Name, + Enabled: p.Enabled, + } + + if p.Description != "" { + resp.Description = &p.Description + } + if p.Mode != "" { + mode := api.InspectionPolicyMode(p.Mode) + resp.Mode = &mode + } + if p.ExternalURL != "" { + resp.ExternalUrl = &p.ExternalURL + } + if p.DefaultAction != "" { + da := api.InspectionPolicyDefaultAction(p.DefaultAction) + resp.DefaultAction = &da + } + if len(p.RedirectPorts) > 0 { + resp.RedirectPorts = &p.RedirectPorts + } + if p.CACertPEM != "" { + resp.CaCertPem = &p.CACertPEM + } + if p.CAKeyPEM != "" { + resp.CaKeyPem = &p.CAKeyPEM + } + if p.EnvoyBinaryPath != "" { + resp.EnvoyBinaryPath = &p.EnvoyBinaryPath + } + if p.EnvoyAdminPort != 0 { + port := int(p.EnvoyAdminPort) + resp.EnvoyAdminPort = &port + } + if p.ICAP != nil { + resp.Icap = &api.InspectionICAPConfig{} + if p.ICAP.ReqModURL != "" { + resp.Icap.ReqmodUrl = &p.ICAP.ReqModURL + } + if p.ICAP.RespModURL != "" { + resp.Icap.RespmodUrl = &p.ICAP.RespModURL + } + if p.ICAP.MaxConnections != 0 { + resp.Icap.MaxConnections = &p.ICAP.MaxConnections + } + } + + rules := make([]api.InspectionPolicyRule, 0, len(p.Rules)) + for _, r := range p.Rules { + rule := api.InspectionPolicyRule{ + Action: api.InspectionPolicyRuleAction(r.Action), + Priority: r.Priority, + } + if len(r.Domains) > 0 { + rule.Domains = &r.Domains + } + if len(r.Networks) > 0 { + rule.Networks = &r.Networks + } + if len(r.Protocols) > 0 { + protos := make([]api.InspectionPolicyRuleProtocols, len(r.Protocols)) + for i, proto := range r.Protocols { + protos[i] = api.InspectionPolicyRuleProtocols(proto) + } + rule.Protocols = &protos + } + if len(r.Paths) > 0 { + rule.Paths = &r.Paths + } + rules = append(rules, rule) + } + resp.Rules = rules + + return resp +} + +func fromAPIRequest(req *api.InspectionPolicyMinimum) *types.InspectionPolicy { + p := &types.InspectionPolicy{ + Name: req.Name, + Enabled: req.Enabled, + } + + if req.Description != nil { + p.Description = *req.Description + } + if req.Mode != nil { + p.Mode = string(*req.Mode) + } + if req.ExternalUrl != nil { + p.ExternalURL = *req.ExternalUrl + } + if req.DefaultAction != nil { + p.DefaultAction = string(*req.DefaultAction) + } + if req.RedirectPorts != nil { + p.RedirectPorts = *req.RedirectPorts + } + if req.CaCertPem != nil { + p.CACertPEM = *req.CaCertPem + } + if req.CaKeyPem != nil { + p.CAKeyPEM = *req.CaKeyPem + } + if req.EnvoyBinaryPath != nil { + p.EnvoyBinaryPath = *req.EnvoyBinaryPath + } + if req.EnvoyAdminPort != nil { + p.EnvoyAdminPort = *req.EnvoyAdminPort + } + if req.Icap != nil { + p.ICAP = &types.InspectionICAPConfig{} + if req.Icap.ReqmodUrl != nil { + p.ICAP.ReqModURL = *req.Icap.ReqmodUrl + } + if req.Icap.RespmodUrl != nil { + p.ICAP.RespModURL = *req.Icap.RespmodUrl + } + if req.Icap.MaxConnections != nil { + p.ICAP.MaxConnections = *req.Icap.MaxConnections + } + } + + for _, r := range req.Rules { + rule := types.InspectionPolicyRule{ + Action: string(r.Action), + Priority: r.Priority, + } + if r.Domains != nil { + rule.Domains = *r.Domains + } + if r.Networks != nil { + rule.Networks = *r.Networks + } + if r.Protocols != nil { + for _, proto := range *r.Protocols { + rule.Protocols = append(rule.Protocols, string(proto)) + } + } + if r.Paths != nil { + rule.Paths = *r.Paths + } + p.Rules = append(p.Rules, rule) + } + + return p +} diff --git a/management/server/http/handlers/policies/policies_handler.go b/management/server/http/handlers/policies/policies_handler.go index e4d1d73df..f208390e7 100644 --- a/management/server/http/handlers/policies/policies_handler.go +++ b/management/server/http/handlers/policies/policies_handler.go @@ -281,6 +281,10 @@ func (h *handler) savePolicy(w http.ResponseWriter, r *http.Request, accountID s policy.SourcePostureChecks = *req.SourcePostureChecks } + if req.InspectionPolicies != nil { + policy.InspectionPolicies = *req.InspectionPolicies + } + policy, err := h.accountManager.SavePolicy(r.Context(), accountID, userID, policy, create) if err != nil { util.WriteError(r.Context(), err, w) @@ -377,6 +381,7 @@ func toPolicyResponse(groups []*types.Group, policy *types.Policy) *api.Policy { Description: &policy.Description, Enabled: policy.Enabled, SourcePostureChecks: policy.SourcePostureChecks, + InspectionPolicies: &policy.InspectionPolicies, } for _, r := range policy.Rules { rID := r.ID diff --git a/management/server/inspection_policies.go b/management/server/inspection_policies.go new file mode 100644 index 000000000..2ad1abb9b --- /dev/null +++ b/management/server/inspection_policies.go @@ -0,0 +1,52 @@ +package server + +import ( + "context" + + "github.com/rs/xid" + + "github.com/netbirdio/netbird/management/server/store" + "github.com/netbirdio/netbird/management/server/types" +) + +// GetInspectionPolicy returns an inspection policy by ID. +func (am *DefaultAccountManager) GetInspectionPolicy(ctx context.Context, accountID, policyID, userID string) (*types.InspectionPolicy, error) { + return am.Store.GetInspectionPolicyByID(ctx, store.LockingStrengthShare, accountID, policyID) +} + +// SaveInspectionPolicy creates or updates an inspection policy. +func (am *DefaultAccountManager) SaveInspectionPolicy(ctx context.Context, accountID, userID string, policy *types.InspectionPolicy, create bool) (*types.InspectionPolicy, error) { + err := am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { + if create { + policy.ID = xid.New().String() + } + policy.AccountID = accountID + return transaction.SaveInspectionPolicy(ctx, store.LockingStrengthUpdate, policy) + }) + if err != nil { + return nil, err + } + + am.UpdateAccountPeers(ctx, accountID) + + return policy, nil +} + +// DeleteInspectionPolicy removes an inspection policy. +func (am *DefaultAccountManager) DeleteInspectionPolicy(ctx context.Context, accountID, policyID, userID string) error { + err := am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error { + return transaction.DeleteInspectionPolicy(ctx, store.LockingStrengthUpdate, accountID, policyID) + }) + if err != nil { + return err + } + + am.UpdateAccountPeers(ctx, accountID) + + return nil +} + +// ListInspectionPolicies returns all inspection policies for the account. +func (am *DefaultAccountManager) ListInspectionPolicies(ctx context.Context, accountID, userID string) ([]*types.InspectionPolicy, error) { + return am.Store.GetAccountInspectionPolicies(ctx, store.LockingStrengthShare, accountID) +} diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index ff369355e..f77458a00 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -909,6 +909,26 @@ func (am *MockAccountManager) ListPostureChecks(ctx context.Context, accountID, return nil, status.Errorf(codes.Unimplemented, "method ListPostureChecks is not implemented") } +// GetInspectionPolicy mocks GetInspectionPolicy of the AccountManager interface +func (am *MockAccountManager) GetInspectionPolicy(ctx context.Context, accountID, policyID, userID string) (*types.InspectionPolicy, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetInspectionPolicy is not implemented") +} + +// SaveInspectionPolicy mocks SaveInspectionPolicy of the AccountManager interface +func (am *MockAccountManager) SaveInspectionPolicy(ctx context.Context, accountID, userID string, policy *types.InspectionPolicy, create bool) (*types.InspectionPolicy, error) { + return nil, status.Errorf(codes.Unimplemented, "method SaveInspectionPolicy is not implemented") +} + +// DeleteInspectionPolicy mocks DeleteInspectionPolicy of the AccountManager interface +func (am *MockAccountManager) DeleteInspectionPolicy(ctx context.Context, accountID, policyID, userID string) error { + return status.Errorf(codes.Unimplemented, "method DeleteInspectionPolicy is not implemented") +} + +// ListInspectionPolicies mocks ListInspectionPolicies of the AccountManager interface +func (am *MockAccountManager) ListInspectionPolicies(ctx context.Context, accountID, userID string) ([]*types.InspectionPolicy, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListInspectionPolicies is not implemented") +} + // GetIdpManager mocks GetIdpManager of the AccountManager interface func (am *MockAccountManager) GetIdpManager() idp.Manager { if am.GetIdpManagerFunc != nil { diff --git a/management/server/networks/routers/types/router.go b/management/server/networks/routers/types/router.go index e90c61a97..f3954ac7e 100644 --- a/management/server/networks/routers/types/router.go +++ b/management/server/networks/routers/types/router.go @@ -18,6 +18,28 @@ type NetworkRouter struct { Masquerade bool Metric int Enabled bool + Inspection *InspectionConfig `gorm:"serializer:json"` +} + +// InspectionConfig holds traffic inspection settings for a routing peer. +// L7 inspection rules are stored separately as ProxyRule entities. +type InspectionConfig struct { + Enabled bool `json:"enabled"` + Mode string `json:"mode"` // "builtin" or "external" + ExternalURL string `json:"external_url"` + DefaultAction string `json:"default_action"` // "allow", "block", "inspect" + RedirectPorts []int `json:"redirect_ports"` + ICAP *InspectionICAP `json:"icap,omitempty"` + CACertPEM string `json:"ca_cert_pem,omitempty"` + CAKeyPEM string `json:"ca_key_pem,omitempty"` + ListenPort int `json:"listen_port"` +} + +// InspectionICAP holds ICAP service configuration. +type InspectionICAP struct { + ReqModURL string `json:"reqmod_url"` + RespModURL string `json:"respmod_url"` + MaxConnections int `json:"max_connections"` } func NewNetworkRouter(accountID string, networkID string, peer string, peerGroups []string, masquerade bool, metric int, enabled bool) (*NetworkRouter, error) { @@ -38,7 +60,7 @@ func NewNetworkRouter(accountID string, networkID string, peer string, peerGroup } func (n *NetworkRouter) ToAPIResponse() *api.NetworkRouter { - return &api.NetworkRouter{ + resp := &api.NetworkRouter{ Id: n.ID, Peer: &n.Peer, PeerGroups: &n.PeerGroups, @@ -46,6 +68,12 @@ func (n *NetworkRouter) ToAPIResponse() *api.NetworkRouter { Metric: n.Metric, Enabled: n.Enabled, } + + if n.Inspection != nil { + resp.Inspection = inspectionToAPI(n.Inspection) + } + + return resp } func (n *NetworkRouter) FromAPIRequest(req *api.NetworkRouterRequest) { @@ -60,10 +88,11 @@ func (n *NetworkRouter) FromAPIRequest(req *api.NetworkRouterRequest) { n.Masquerade = req.Masquerade n.Metric = req.Metric n.Enabled = req.Enabled + n.Inspection = inspectionFromAPI(req.Inspection) } func (n *NetworkRouter) Copy() *NetworkRouter { - return &NetworkRouter{ + c := &NetworkRouter{ ID: n.ID, NetworkID: n.NetworkID, AccountID: n.AccountID, @@ -73,6 +102,108 @@ func (n *NetworkRouter) Copy() *NetworkRouter { Metric: n.Metric, Enabled: n.Enabled, } + if n.Inspection != nil { + insp := *n.Inspection + c.Inspection = &insp + } + return c +} + +func inspectionToAPI(c *InspectionConfig) *api.RouterInspectionConfig { + if c == nil { + return nil + } + + mode := api.RouterInspectionConfigMode(c.Mode) + defaultAction := api.RouterInspectionConfigDefaultAction(c.DefaultAction) + + resp := &api.RouterInspectionConfig{ + Enabled: c.Enabled, + Mode: &mode, + DefaultAction: &defaultAction, + } + + if c.ExternalURL != "" { + resp.ExternalUrl = &c.ExternalURL + } + + if len(c.RedirectPorts) > 0 { + resp.RedirectPorts = &c.RedirectPorts + } + + if c.CACertPEM != "" { + resp.CaCertPem = &c.CACertPEM + } + if c.CAKeyPEM != "" { + resp.CaKeyPem = &c.CAKeyPEM + } + + if c.ICAP != nil { + icap := api.InspectionICAPConfig{} + if c.ICAP.ReqModURL != "" { + icap.ReqmodUrl = &c.ICAP.ReqModURL + } + if c.ICAP.RespModURL != "" { + icap.RespmodUrl = &c.ICAP.RespModURL + } + if c.ICAP.MaxConnections > 0 { + icap.MaxConnections = &c.ICAP.MaxConnections + } + resp.Icap = &icap + } + + return resp +} + +func inspectionFromAPI(c *api.RouterInspectionConfig) *InspectionConfig { + if c == nil { + return nil + } + + insp := &InspectionConfig{ + Enabled: c.Enabled, + } + + if c.Mode != nil { + insp.Mode = string(*c.Mode) + } + if c.DefaultAction != nil { + insp.DefaultAction = string(*c.DefaultAction) + } + if c.ExternalUrl != nil { + insp.ExternalURL = *c.ExternalUrl + } + if c.RedirectPorts != nil { + insp.RedirectPorts = *c.RedirectPorts + } + if c.CaCertPem != nil { + insp.CACertPEM = *c.CaCertPem + } + if c.CaKeyPem != nil { + insp.CAKeyPEM = *c.CaKeyPem + } + + if c.Icap != nil { + insp.ICAP = &InspectionICAP{} + if c.Icap.ReqmodUrl != nil { + insp.ICAP.ReqModURL = *c.Icap.ReqmodUrl + } + if c.Icap.RespmodUrl != nil { + insp.ICAP.RespModURL = *c.Icap.RespmodUrl + } + if c.Icap.MaxConnections != nil { + insp.ICAP.MaxConnections = *c.Icap.MaxConnections + } + } + + return insp +} + +func derefInt(p *int) int { + if p == nil { + return 0 + } + return *p } func (n *NetworkRouter) EventMeta(network *types.Network) map[string]any { diff --git a/management/server/policy.go b/management/server/policy.go index 3e84c3d10..d16e2441d 100644 --- a/management/server/policy.go +++ b/management/server/policy.go @@ -3,6 +3,7 @@ package server import ( "context" _ "embed" + "slices" "github.com/rs/xid" @@ -150,6 +151,12 @@ func arePolicyChangesAffectPeers(ctx context.Context, transaction store.Store, a return false, nil } + // Inspection policy changes always affect peers since they control + // the transparent proxy config pushed in the network map. + if !slices.Equal(existingPolicy.InspectionPolicies, policy.InspectionPolicies) { + return true, nil + } + for _, rule := range existingPolicy.Rules { if rule.SourceResource.Type != "" || rule.DestinationResource.Type != "" { return true, nil diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index 0b463a724..61d8d7a92 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -134,7 +134,7 @@ func NewSqlStore(ctx context.Context, db *gorm.DB, storeEngine types.Engine, met &installation{}, &types.ExtraSettings{}, &posture.Checks{}, &nbpeer.NetworkAddress{}, &networkTypes.Network{}, &routerTypes.NetworkRouter{}, &resourceTypes.NetworkResource{}, &types.AccountOnboarding{}, &types.Job{}, &zones.Zone{}, &records.Record{}, &types.UserInviteRecord{}, &rpservice.Service{}, &rpservice.Target{}, &domain.Domain{}, - &accesslogs.AccessLogEntry{}, &proxy.Proxy{}, + &accesslogs.AccessLogEntry{}, &proxy.Proxy{}, &types.InspectionPolicy{}, ) if err != nil { return nil, fmt.Errorf("auto migratePreAuto: %w", err) @@ -1115,6 +1115,7 @@ func (s *SqlStore) getAccountGorm(ctx context.Context, accountID string) (*types Preload("RoutesG"). Preload("NameServerGroupsG"). Preload("PostureChecks"). + Preload("InspectionPolicies"). Preload("Networks"). Preload("NetworkRouters"). Preload("NetworkResources"). @@ -3877,6 +3878,63 @@ func (s *SqlStore) DeletePostureChecks(ctx context.Context, accountID, postureCh return nil } +// GetAccountInspectionPolicies returns all inspection policies for the account. +// CA cert and key are decrypted after loading. +func (s *SqlStore) GetAccountInspectionPolicies(ctx context.Context, _ LockingStrength, accountID string) ([]*types.InspectionPolicy, error) { + var policies []*types.InspectionPolicy + result := s.db.Where("account_id = ?", accountID).Find(&policies) + if result.Error != nil { + return nil, status.Errorf(status.Internal, "failed to get inspection policies: %s", result.Error) + } + for _, p := range policies { + if err := p.DecryptSensitiveData(s.fieldEncrypt); err != nil { + return nil, fmt.Errorf("decrypt inspection policy %s: %w", p.ID, err) + } + } + return policies, nil +} + +// GetInspectionPolicyByID returns an inspection policy by ID. +// CA cert and key are decrypted after loading. +func (s *SqlStore) GetInspectionPolicyByID(ctx context.Context, _ LockingStrength, accountID, policyID string) (*types.InspectionPolicy, error) { + var policy types.InspectionPolicy + result := s.db.Where(accountAndIDQueryCondition, accountID, policyID).First(&policy) + if result.Error != nil { + return nil, status.Errorf(status.Internal, "failed to get inspection policy: %s", result.Error) + } + if err := policy.DecryptSensitiveData(s.fieldEncrypt); err != nil { + return nil, fmt.Errorf("decrypt inspection policy %s: %w", policyID, err) + } + return &policy, nil +} + +// SaveInspectionPolicy saves an inspection policy to the database. +// CA cert and key are encrypted before storage. +func (s *SqlStore) SaveInspectionPolicy(ctx context.Context, _ LockingStrength, policy *types.InspectionPolicy) error { + if err := policy.EncryptSensitiveData(s.fieldEncrypt); err != nil { + return fmt.Errorf("encrypt inspection policy: %w", err) + } + result := s.db.Save(policy) + if result.Error != nil { + log.WithContext(ctx).Errorf("failed to save inspection policy: %s", result.Error) + return status.Errorf(status.Internal, "failed to save inspection policy") + } + return nil +} + +// DeleteInspectionPolicy deletes an inspection policy from the database. +func (s *SqlStore) DeleteInspectionPolicy(ctx context.Context, _ LockingStrength, accountID, policyID string) error { + result := s.db.Delete(&types.InspectionPolicy{}, accountAndIDQueryCondition, accountID, policyID) + if result.Error != nil { + log.WithContext(ctx).Errorf("failed to delete inspection policy: %s", result.Error) + return status.Errorf(status.Internal, "failed to delete inspection policy") + } + if result.RowsAffected == 0 { + return status.Errorf(status.NotFound, "inspection policy %s not found", policyID) + } + return nil +} + // GetAccountRoutes retrieves network routes for an account. func (s *SqlStore) GetAccountRoutes(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*route.Route, error) { tx := s.db diff --git a/management/server/store/sql_store_inspection_policy_test.go b/management/server/store/sql_store_inspection_policy_test.go new file mode 100644 index 000000000..fedf13746 --- /dev/null +++ b/management/server/store/sql_store_inspection_policy_test.go @@ -0,0 +1,151 @@ +package store + +import ( + "context" + "runtime" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/management/server/types" +) + +func TestSqlStore_InspectionPolicyCRUD(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("The SQLite store is not properly supported by Windows yet") + } + + runTestForAllEngines(t, "", func(t *testing.T, store Store) { + ctx := context.Background() + accountID := "test-account-inspection" + + // Create account first + account := newAccountWithId(ctx, accountID, "test-user", "example.com") + err := store.SaveAccount(ctx, account) + require.NoError(t, err) + + // Create + policy := &types.InspectionPolicy{ + ID: "ip-1", + AccountID: accountID, + Name: "Block gambling", + Description: "Block all gambling sites", + Enabled: true, + Rules: []types.InspectionPolicyRule{ + { + Domains: []string{"*.gambling.com", "*.betting.com"}, + Action: "block", + Priority: 1, + Protocols: []string{"https"}, + }, + { + Domains: []string{"*.malware.org"}, + Networks: []string{"10.0.0.0/8"}, + Action: "block", + Priority: 2, + }, + }, + } + err = store.SaveInspectionPolicy(ctx, LockingStrengthUpdate, policy) + require.NoError(t, err) + + // Read + got, err := store.GetInspectionPolicyByID(ctx, LockingStrengthShare, accountID, "ip-1") + require.NoError(t, err) + assert.Equal(t, "Block gambling", got.Name) + assert.Equal(t, "Block all gambling sites", got.Description) + assert.True(t, got.Enabled) + require.Len(t, got.Rules, 2) + assert.Equal(t, []string{"*.gambling.com", "*.betting.com"}, got.Rules[0].Domains) + assert.Equal(t, "block", got.Rules[0].Action) + assert.Equal(t, []string{"https"}, got.Rules[0].Protocols) + assert.Equal(t, []string{"10.0.0.0/8"}, got.Rules[1].Networks) + + // List + policies, err := store.GetAccountInspectionPolicies(ctx, LockingStrengthShare, accountID) + require.NoError(t, err) + require.Len(t, policies, 1) + assert.Equal(t, "ip-1", policies[0].ID) + + // Update + policy.Name = "Block gambling updated" + policy.Rules = append(policy.Rules, types.InspectionPolicyRule{ + Domains: []string{"*.phishing.net"}, + Action: "inspect", + Priority: 3, + }) + err = store.SaveInspectionPolicy(ctx, LockingStrengthUpdate, policy) + require.NoError(t, err) + + got, err = store.GetInspectionPolicyByID(ctx, LockingStrengthShare, accountID, "ip-1") + require.NoError(t, err) + assert.Equal(t, "Block gambling updated", got.Name) + require.Len(t, got.Rules, 3) + assert.Equal(t, "inspect", got.Rules[2].Action) + + // Delete + err = store.DeleteInspectionPolicy(ctx, LockingStrengthUpdate, accountID, "ip-1") + require.NoError(t, err) + + _, err = store.GetInspectionPolicyByID(ctx, LockingStrengthShare, accountID, "ip-1") + assert.Error(t, err) + + policies, err = store.GetAccountInspectionPolicies(ctx, LockingStrengthShare, accountID) + require.NoError(t, err) + assert.Empty(t, policies) + }) +} + +func TestSqlStore_InspectionPolicyNotFound(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("The SQLite store is not properly supported by Windows yet") + } + + runTestForAllEngines(t, "", func(t *testing.T, store Store) { + ctx := context.Background() + accountID := "test-account-no-ip" + + account := newAccountWithId(ctx, accountID, "test-user", "example.com") + err := store.SaveAccount(ctx, account) + require.NoError(t, err) + + _, err = store.GetInspectionPolicyByID(ctx, LockingStrengthShare, accountID, "nonexistent") + assert.Error(t, err) + }) +} + +func TestSqlStore_InspectionPolicyIsolation(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("The SQLite store is not properly supported by Windows yet") + } + + runTestForAllEngines(t, "", func(t *testing.T, store Store) { + ctx := context.Background() + + // Create two accounts + acc1 := newAccountWithId(ctx, "acc-1", "user-1", "one.com") + acc2 := newAccountWithId(ctx, "acc-2", "user-2", "two.com") + require.NoError(t, store.SaveAccount(ctx, acc1)) + require.NoError(t, store.SaveAccount(ctx, acc2)) + + // Save policy for acc-1 + policy := &types.InspectionPolicy{ + ID: "ip-acc1", + AccountID: "acc-1", + Name: "Account 1 policy", + Enabled: true, + Rules: []types.InspectionPolicyRule{{Action: "block", Priority: 1}}, + } + require.NoError(t, store.SaveInspectionPolicy(ctx, LockingStrengthUpdate, policy)) + + // acc-2 should not see acc-1's policy + policies, err := store.GetAccountInspectionPolicies(ctx, LockingStrengthShare, "acc-2") + require.NoError(t, err) + assert.Empty(t, policies) + + // acc-2 should not be able to get acc-1's policy by ID + _, err = store.GetInspectionPolicyByID(ctx, LockingStrengthShare, "acc-2", "ip-acc1") + assert.Error(t, err) + }) +} diff --git a/management/server/store/store.go b/management/server/store/store.go index efd9a28fd..60146c336 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -143,6 +143,11 @@ type Store interface { SavePostureChecks(ctx context.Context, postureCheck *posture.Checks) error DeletePostureChecks(ctx context.Context, accountID, postureChecksID string) error + GetAccountInspectionPolicies(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.InspectionPolicy, error) + GetInspectionPolicyByID(ctx context.Context, lockStrength LockingStrength, accountID, policyID string) (*types.InspectionPolicy, error) + SaveInspectionPolicy(ctx context.Context, lockStrength LockingStrength, policy *types.InspectionPolicy) error + DeleteInspectionPolicy(ctx context.Context, lockStrength LockingStrength, accountID, policyID string) error + GetPeerLabelsInAccount(ctx context.Context, lockStrength LockingStrength, accountId string, hostname string) ([]string, error) AddPeerToAllGroup(ctx context.Context, accountID string, peerID string) error AddPeerToGroup(ctx context.Context, accountID, peerId string, groupID string) error diff --git a/management/server/store/store_mock.go b/management/server/store/store_mock.go index 5e609c4ec..5b1ffc6bb 100644 --- a/management/server/store/store_mock.go +++ b/management/server/store/store_mock.go @@ -1,5 +1,10 @@ // Code generated by MockGen. DO NOT EDIT. // Source: ./store.go +// +// Generated by this command: +// +// mockgen -source=./store.go -destination=./store_mock.go -package=store +// // Package store is a generated GoMock package. package store @@ -10,7 +15,6 @@ import ( reflect "reflect" time "time" - gomock "github.com/golang/mock/gomock" dns "github.com/netbirdio/netbird/dns" accesslogs "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/accesslogs" domain "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/domain" @@ -26,12 +30,14 @@ import ( types2 "github.com/netbirdio/netbird/management/server/types" route "github.com/netbirdio/netbird/route" crypt "github.com/netbirdio/netbird/util/crypt" + gomock "go.uber.org/mock/gomock" ) // MockStore is a mock of Store interface. type MockStore struct { ctrl *gomock.Controller recorder *MockStoreMockRecorder + isgomock struct{} } // MockStoreMockRecorder is the mock recorder for MockStore. @@ -61,7 +67,7 @@ func (m *MockStore) AccountExists(ctx context.Context, lockStrength LockingStren } // AccountExists indicates an expected call of AccountExists. -func (mr *MockStoreMockRecorder) AccountExists(ctx, lockStrength, id interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) AccountExists(ctx, lockStrength, id any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AccountExists", reflect.TypeOf((*MockStore)(nil).AccountExists), ctx, lockStrength, id) } @@ -75,23 +81,23 @@ func (m *MockStore) AcquireGlobalLock(ctx context.Context) func() { } // AcquireGlobalLock indicates an expected call of AcquireGlobalLock. -func (mr *MockStoreMockRecorder) AcquireGlobalLock(ctx interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) AcquireGlobalLock(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AcquireGlobalLock", reflect.TypeOf((*MockStore)(nil).AcquireGlobalLock), ctx) } // AddPeerToAccount mocks base method. -func (m *MockStore) AddPeerToAccount(ctx context.Context, peer *peer.Peer) error { +func (m *MockStore) AddPeerToAccount(ctx context.Context, arg1 *peer.Peer) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddPeerToAccount", ctx, peer) + ret := m.ctrl.Call(m, "AddPeerToAccount", ctx, arg1) ret0, _ := ret[0].(error) return ret0 } // AddPeerToAccount indicates an expected call of AddPeerToAccount. -func (mr *MockStoreMockRecorder) AddPeerToAccount(ctx, peer interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) AddPeerToAccount(ctx, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddPeerToAccount", reflect.TypeOf((*MockStore)(nil).AddPeerToAccount), ctx, peer) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddPeerToAccount", reflect.TypeOf((*MockStore)(nil).AddPeerToAccount), ctx, arg1) } // AddPeerToAllGroup mocks base method. @@ -103,7 +109,7 @@ func (m *MockStore) AddPeerToAllGroup(ctx context.Context, accountID, peerID str } // AddPeerToAllGroup indicates an expected call of AddPeerToAllGroup. -func (mr *MockStoreMockRecorder) AddPeerToAllGroup(ctx, accountID, peerID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) AddPeerToAllGroup(ctx, accountID, peerID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddPeerToAllGroup", reflect.TypeOf((*MockStore)(nil).AddPeerToAllGroup), ctx, accountID, peerID) } @@ -117,7 +123,7 @@ func (m *MockStore) AddPeerToGroup(ctx context.Context, accountID, peerId, group } // AddPeerToGroup indicates an expected call of AddPeerToGroup. -func (mr *MockStoreMockRecorder) AddPeerToGroup(ctx, accountID, peerId, groupID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) AddPeerToGroup(ctx, accountID, peerId, groupID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddPeerToGroup", reflect.TypeOf((*MockStore)(nil).AddPeerToGroup), ctx, accountID, peerId, groupID) } @@ -131,7 +137,7 @@ func (m *MockStore) AddResourceToGroup(ctx context.Context, accountId, groupID s } // AddResourceToGroup indicates an expected call of AddResourceToGroup. -func (mr *MockStoreMockRecorder) AddResourceToGroup(ctx, accountId, groupID, resource interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) AddResourceToGroup(ctx, accountId, groupID, resource any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddResourceToGroup", reflect.TypeOf((*MockStore)(nil).AddResourceToGroup), ctx, accountId, groupID, resource) } @@ -146,7 +152,7 @@ func (m *MockStore) ApproveAccountPeers(ctx context.Context, accountID string) ( } // ApproveAccountPeers indicates an expected call of ApproveAccountPeers. -func (mr *MockStoreMockRecorder) ApproveAccountPeers(ctx, accountID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) ApproveAccountPeers(ctx, accountID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApproveAccountPeers", reflect.TypeOf((*MockStore)(nil).ApproveAccountPeers), ctx, accountID) } @@ -160,7 +166,7 @@ func (m *MockStore) CleanupStaleProxies(ctx context.Context, inactivityDuration } // CleanupStaleProxies indicates an expected call of CleanupStaleProxies. -func (mr *MockStoreMockRecorder) CleanupStaleProxies(ctx, inactivityDuration interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) CleanupStaleProxies(ctx, inactivityDuration any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CleanupStaleProxies", reflect.TypeOf((*MockStore)(nil).CleanupStaleProxies), ctx, inactivityDuration) } @@ -174,7 +180,7 @@ func (m *MockStore) Close(ctx context.Context) error { } // Close indicates an expected call of Close. -func (mr *MockStoreMockRecorder) Close(ctx interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) Close(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockStore)(nil).Close), ctx) } @@ -188,24 +194,24 @@ func (m *MockStore) CompletePeerJob(ctx context.Context, job *types2.Job) error } // CompletePeerJob indicates an expected call of CompletePeerJob. -func (mr *MockStoreMockRecorder) CompletePeerJob(ctx, job interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) CompletePeerJob(ctx, job any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CompletePeerJob", reflect.TypeOf((*MockStore)(nil).CompletePeerJob), ctx, job) } // CountAccountsByPrivateDomain mocks base method. -func (m *MockStore) CountAccountsByPrivateDomain(ctx context.Context, domain string) (int64, error) { +func (m *MockStore) CountAccountsByPrivateDomain(ctx context.Context, arg1 string) (int64, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CountAccountsByPrivateDomain", ctx, domain) + ret := m.ctrl.Call(m, "CountAccountsByPrivateDomain", ctx, arg1) ret0, _ := ret[0].(int64) ret1, _ := ret[1].(error) return ret0, ret1 } // CountAccountsByPrivateDomain indicates an expected call of CountAccountsByPrivateDomain. -func (mr *MockStoreMockRecorder) CountAccountsByPrivateDomain(ctx, domain interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) CountAccountsByPrivateDomain(ctx, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountAccountsByPrivateDomain", reflect.TypeOf((*MockStore)(nil).CountAccountsByPrivateDomain), ctx, domain) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountAccountsByPrivateDomain", reflect.TypeOf((*MockStore)(nil).CountAccountsByPrivateDomain), ctx, arg1) } // CountEphemeralServicesByPeer mocks base method. @@ -218,7 +224,7 @@ func (m *MockStore) CountEphemeralServicesByPeer(ctx context.Context, lockStreng } // CountEphemeralServicesByPeer indicates an expected call of CountEphemeralServicesByPeer. -func (mr *MockStoreMockRecorder) CountEphemeralServicesByPeer(ctx, lockStrength, accountID, peerID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) CountEphemeralServicesByPeer(ctx, lockStrength, accountID, peerID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountEphemeralServicesByPeer", reflect.TypeOf((*MockStore)(nil).CountEphemeralServicesByPeer), ctx, lockStrength, accountID, peerID) } @@ -232,7 +238,7 @@ func (m *MockStore) CreateAccessLog(ctx context.Context, log *accesslogs.AccessL } // CreateAccessLog indicates an expected call of CreateAccessLog. -func (mr *MockStoreMockRecorder) CreateAccessLog(ctx, log interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) CreateAccessLog(ctx, log any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAccessLog", reflect.TypeOf((*MockStore)(nil).CreateAccessLog), ctx, log) } @@ -247,7 +253,7 @@ func (m *MockStore) CreateCustomDomain(ctx context.Context, accountID, domainNam } // CreateCustomDomain indicates an expected call of CreateCustomDomain. -func (mr *MockStoreMockRecorder) CreateCustomDomain(ctx, accountID, domainName, targetCluster, validated interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) CreateCustomDomain(ctx, accountID, domainName, targetCluster, validated any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCustomDomain", reflect.TypeOf((*MockStore)(nil).CreateCustomDomain), ctx, accountID, domainName, targetCluster, validated) } @@ -261,7 +267,7 @@ func (m *MockStore) CreateDNSRecord(ctx context.Context, record *records.Record) } // CreateDNSRecord indicates an expected call of CreateDNSRecord. -func (mr *MockStoreMockRecorder) CreateDNSRecord(ctx, record interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) CreateDNSRecord(ctx, record any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateDNSRecord", reflect.TypeOf((*MockStore)(nil).CreateDNSRecord), ctx, record) } @@ -275,7 +281,7 @@ func (m *MockStore) CreateGroup(ctx context.Context, group *types2.Group) error } // CreateGroup indicates an expected call of CreateGroup. -func (mr *MockStoreMockRecorder) CreateGroup(ctx, group interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) CreateGroup(ctx, group any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateGroup", reflect.TypeOf((*MockStore)(nil).CreateGroup), ctx, group) } @@ -289,7 +295,7 @@ func (m *MockStore) CreateGroups(ctx context.Context, accountID string, groups [ } // CreateGroups indicates an expected call of CreateGroups. -func (mr *MockStoreMockRecorder) CreateGroups(ctx, accountID, groups interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) CreateGroups(ctx, accountID, groups any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateGroups", reflect.TypeOf((*MockStore)(nil).CreateGroups), ctx, accountID, groups) } @@ -303,7 +309,7 @@ func (m *MockStore) CreatePeerJob(ctx context.Context, job *types2.Job) error { } // CreatePeerJob indicates an expected call of CreatePeerJob. -func (mr *MockStoreMockRecorder) CreatePeerJob(ctx, job interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) CreatePeerJob(ctx, job any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePeerJob", reflect.TypeOf((*MockStore)(nil).CreatePeerJob), ctx, job) } @@ -317,23 +323,23 @@ func (m *MockStore) CreatePolicy(ctx context.Context, policy *types2.Policy) err } // CreatePolicy indicates an expected call of CreatePolicy. -func (mr *MockStoreMockRecorder) CreatePolicy(ctx, policy interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) CreatePolicy(ctx, policy any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePolicy", reflect.TypeOf((*MockStore)(nil).CreatePolicy), ctx, policy) } // CreateService mocks base method. -func (m *MockStore) CreateService(ctx context.Context, service *service.Service) error { +func (m *MockStore) CreateService(ctx context.Context, arg1 *service.Service) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateService", ctx, service) + ret := m.ctrl.Call(m, "CreateService", ctx, arg1) ret0, _ := ret[0].(error) return ret0 } // CreateService indicates an expected call of CreateService. -func (mr *MockStoreMockRecorder) CreateService(ctx, service interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) CreateService(ctx, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateService", reflect.TypeOf((*MockStore)(nil).CreateService), ctx, service) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateService", reflect.TypeOf((*MockStore)(nil).CreateService), ctx, arg1) } // CreateZone mocks base method. @@ -345,7 +351,7 @@ func (m *MockStore) CreateZone(ctx context.Context, zone *zones.Zone) error { } // CreateZone indicates an expected call of CreateZone. -func (mr *MockStoreMockRecorder) CreateZone(ctx, zone interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) CreateZone(ctx, zone any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateZone", reflect.TypeOf((*MockStore)(nil).CreateZone), ctx, zone) } @@ -359,7 +365,7 @@ func (m *MockStore) DeleteAccount(ctx context.Context, account *types2.Account) } // DeleteAccount indicates an expected call of DeleteAccount. -func (mr *MockStoreMockRecorder) DeleteAccount(ctx, account interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteAccount(ctx, account any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAccount", reflect.TypeOf((*MockStore)(nil).DeleteAccount), ctx, account) } @@ -373,7 +379,7 @@ func (m *MockStore) DeleteCustomDomain(ctx context.Context, accountID, domainID } // DeleteCustomDomain indicates an expected call of DeleteCustomDomain. -func (mr *MockStoreMockRecorder) DeleteCustomDomain(ctx, accountID, domainID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteCustomDomain(ctx, accountID, domainID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCustomDomain", reflect.TypeOf((*MockStore)(nil).DeleteCustomDomain), ctx, accountID, domainID) } @@ -387,7 +393,7 @@ func (m *MockStore) DeleteDNSRecord(ctx context.Context, accountID, zoneID, reco } // DeleteDNSRecord indicates an expected call of DeleteDNSRecord. -func (mr *MockStoreMockRecorder) DeleteDNSRecord(ctx, accountID, zoneID, recordID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteDNSRecord(ctx, accountID, zoneID, recordID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDNSRecord", reflect.TypeOf((*MockStore)(nil).DeleteDNSRecord), ctx, accountID, zoneID, recordID) } @@ -401,7 +407,7 @@ func (m *MockStore) DeleteGroup(ctx context.Context, accountID, groupID string) } // DeleteGroup indicates an expected call of DeleteGroup. -func (mr *MockStoreMockRecorder) DeleteGroup(ctx, accountID, groupID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteGroup(ctx, accountID, groupID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteGroup", reflect.TypeOf((*MockStore)(nil).DeleteGroup), ctx, accountID, groupID) } @@ -415,7 +421,7 @@ func (m *MockStore) DeleteGroups(ctx context.Context, accountID string, groupIDs } // DeleteGroups indicates an expected call of DeleteGroups. -func (mr *MockStoreMockRecorder) DeleteGroups(ctx, accountID, groupIDs interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteGroups(ctx, accountID, groupIDs any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteGroups", reflect.TypeOf((*MockStore)(nil).DeleteGroups), ctx, accountID, groupIDs) } @@ -429,11 +435,25 @@ func (m *MockStore) DeleteHashedPAT2TokenIDIndex(hashedToken string) error { } // DeleteHashedPAT2TokenIDIndex indicates an expected call of DeleteHashedPAT2TokenIDIndex. -func (mr *MockStoreMockRecorder) DeleteHashedPAT2TokenIDIndex(hashedToken interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteHashedPAT2TokenIDIndex(hashedToken any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteHashedPAT2TokenIDIndex", reflect.TypeOf((*MockStore)(nil).DeleteHashedPAT2TokenIDIndex), hashedToken) } +// DeleteInspectionPolicy mocks base method. +func (m *MockStore) DeleteInspectionPolicy(ctx context.Context, lockStrength LockingStrength, accountID, policyID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteInspectionPolicy", ctx, lockStrength, accountID, policyID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteInspectionPolicy indicates an expected call of DeleteInspectionPolicy. +func (mr *MockStoreMockRecorder) DeleteInspectionPolicy(ctx, lockStrength, accountID, policyID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteInspectionPolicy", reflect.TypeOf((*MockStore)(nil).DeleteInspectionPolicy), ctx, lockStrength, accountID, policyID) +} + // DeleteNameServerGroup mocks base method. func (m *MockStore) DeleteNameServerGroup(ctx context.Context, accountID, nameServerGroupID string) error { m.ctrl.T.Helper() @@ -443,7 +463,7 @@ func (m *MockStore) DeleteNameServerGroup(ctx context.Context, accountID, nameSe } // DeleteNameServerGroup indicates an expected call of DeleteNameServerGroup. -func (mr *MockStoreMockRecorder) DeleteNameServerGroup(ctx, accountID, nameServerGroupID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteNameServerGroup(ctx, accountID, nameServerGroupID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteNameServerGroup", reflect.TypeOf((*MockStore)(nil).DeleteNameServerGroup), ctx, accountID, nameServerGroupID) } @@ -457,7 +477,7 @@ func (m *MockStore) DeleteNetwork(ctx context.Context, accountID, networkID stri } // DeleteNetwork indicates an expected call of DeleteNetwork. -func (mr *MockStoreMockRecorder) DeleteNetwork(ctx, accountID, networkID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteNetwork(ctx, accountID, networkID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteNetwork", reflect.TypeOf((*MockStore)(nil).DeleteNetwork), ctx, accountID, networkID) } @@ -471,7 +491,7 @@ func (m *MockStore) DeleteNetworkResource(ctx context.Context, accountID, resour } // DeleteNetworkResource indicates an expected call of DeleteNetworkResource. -func (mr *MockStoreMockRecorder) DeleteNetworkResource(ctx, accountID, resourceID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteNetworkResource(ctx, accountID, resourceID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteNetworkResource", reflect.TypeOf((*MockStore)(nil).DeleteNetworkResource), ctx, accountID, resourceID) } @@ -485,7 +505,7 @@ func (m *MockStore) DeleteNetworkRouter(ctx context.Context, accountID, routerID } // DeleteNetworkRouter indicates an expected call of DeleteNetworkRouter. -func (mr *MockStoreMockRecorder) DeleteNetworkRouter(ctx, accountID, routerID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteNetworkRouter(ctx, accountID, routerID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteNetworkRouter", reflect.TypeOf((*MockStore)(nil).DeleteNetworkRouter), ctx, accountID, routerID) } @@ -500,7 +520,7 @@ func (m *MockStore) DeleteOldAccessLogs(ctx context.Context, olderThan time.Time } // DeleteOldAccessLogs indicates an expected call of DeleteOldAccessLogs. -func (mr *MockStoreMockRecorder) DeleteOldAccessLogs(ctx, olderThan interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteOldAccessLogs(ctx, olderThan any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOldAccessLogs", reflect.TypeOf((*MockStore)(nil).DeleteOldAccessLogs), ctx, olderThan) } @@ -514,7 +534,7 @@ func (m *MockStore) DeletePAT(ctx context.Context, userID, patID string) error { } // DeletePAT indicates an expected call of DeletePAT. -func (mr *MockStoreMockRecorder) DeletePAT(ctx, userID, patID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeletePAT(ctx, userID, patID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePAT", reflect.TypeOf((*MockStore)(nil).DeletePAT), ctx, userID, patID) } @@ -528,7 +548,7 @@ func (m *MockStore) DeletePeer(ctx context.Context, accountID, peerID string) er } // DeletePeer indicates an expected call of DeletePeer. -func (mr *MockStoreMockRecorder) DeletePeer(ctx, accountID, peerID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeletePeer(ctx, accountID, peerID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePeer", reflect.TypeOf((*MockStore)(nil).DeletePeer), ctx, accountID, peerID) } @@ -542,7 +562,7 @@ func (m *MockStore) DeletePolicy(ctx context.Context, accountID, policyID string } // DeletePolicy indicates an expected call of DeletePolicy. -func (mr *MockStoreMockRecorder) DeletePolicy(ctx, accountID, policyID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeletePolicy(ctx, accountID, policyID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePolicy", reflect.TypeOf((*MockStore)(nil).DeletePolicy), ctx, accountID, policyID) } @@ -556,7 +576,7 @@ func (m *MockStore) DeletePostureChecks(ctx context.Context, accountID, postureC } // DeletePostureChecks indicates an expected call of DeletePostureChecks. -func (mr *MockStoreMockRecorder) DeletePostureChecks(ctx, accountID, postureChecksID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeletePostureChecks(ctx, accountID, postureChecksID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePostureChecks", reflect.TypeOf((*MockStore)(nil).DeletePostureChecks), ctx, accountID, postureChecksID) } @@ -570,7 +590,7 @@ func (m *MockStore) DeleteRoute(ctx context.Context, accountID, routeID string) } // DeleteRoute indicates an expected call of DeleteRoute. -func (mr *MockStoreMockRecorder) DeleteRoute(ctx, accountID, routeID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteRoute(ctx, accountID, routeID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRoute", reflect.TypeOf((*MockStore)(nil).DeleteRoute), ctx, accountID, routeID) } @@ -584,7 +604,7 @@ func (m *MockStore) DeleteService(ctx context.Context, accountID, serviceID stri } // DeleteService indicates an expected call of DeleteService. -func (mr *MockStoreMockRecorder) DeleteService(ctx, accountID, serviceID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteService(ctx, accountID, serviceID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteService", reflect.TypeOf((*MockStore)(nil).DeleteService), ctx, accountID, serviceID) } @@ -598,7 +618,7 @@ func (m *MockStore) DeleteServiceTargets(ctx context.Context, accountID, service } // DeleteServiceTargets indicates an expected call of DeleteServiceTargets. -func (mr *MockStoreMockRecorder) DeleteServiceTargets(ctx, accountID, serviceID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteServiceTargets(ctx, accountID, serviceID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteServiceTargets", reflect.TypeOf((*MockStore)(nil).DeleteServiceTargets), ctx, accountID, serviceID) } @@ -612,7 +632,7 @@ func (m *MockStore) DeleteSetupKey(ctx context.Context, accountID, keyID string) } // DeleteSetupKey indicates an expected call of DeleteSetupKey. -func (mr *MockStoreMockRecorder) DeleteSetupKey(ctx, accountID, keyID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteSetupKey(ctx, accountID, keyID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSetupKey", reflect.TypeOf((*MockStore)(nil).DeleteSetupKey), ctx, accountID, keyID) } @@ -626,7 +646,7 @@ func (m *MockStore) DeleteTarget(ctx context.Context, accountID, serviceID strin } // DeleteTarget indicates an expected call of DeleteTarget. -func (mr *MockStoreMockRecorder) DeleteTarget(ctx, accountID, serviceID, targetID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteTarget(ctx, accountID, serviceID, targetID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTarget", reflect.TypeOf((*MockStore)(nil).DeleteTarget), ctx, accountID, serviceID, targetID) } @@ -640,7 +660,7 @@ func (m *MockStore) DeleteTokenID2UserIDIndex(tokenID string) error { } // DeleteTokenID2UserIDIndex indicates an expected call of DeleteTokenID2UserIDIndex. -func (mr *MockStoreMockRecorder) DeleteTokenID2UserIDIndex(tokenID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteTokenID2UserIDIndex(tokenID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTokenID2UserIDIndex", reflect.TypeOf((*MockStore)(nil).DeleteTokenID2UserIDIndex), tokenID) } @@ -654,7 +674,7 @@ func (m *MockStore) DeleteUser(ctx context.Context, accountID, userID string) er } // DeleteUser indicates an expected call of DeleteUser. -func (mr *MockStoreMockRecorder) DeleteUser(ctx, accountID, userID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteUser(ctx, accountID, userID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteUser", reflect.TypeOf((*MockStore)(nil).DeleteUser), ctx, accountID, userID) } @@ -668,7 +688,7 @@ func (m *MockStore) DeleteUserInvite(ctx context.Context, inviteID string) error } // DeleteUserInvite indicates an expected call of DeleteUserInvite. -func (mr *MockStoreMockRecorder) DeleteUserInvite(ctx, inviteID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteUserInvite(ctx, inviteID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteUserInvite", reflect.TypeOf((*MockStore)(nil).DeleteUserInvite), ctx, inviteID) } @@ -682,7 +702,7 @@ func (m *MockStore) DeleteZone(ctx context.Context, accountID, zoneID string) er } // DeleteZone indicates an expected call of DeleteZone. -func (mr *MockStoreMockRecorder) DeleteZone(ctx, accountID, zoneID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteZone(ctx, accountID, zoneID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteZone", reflect.TypeOf((*MockStore)(nil).DeleteZone), ctx, accountID, zoneID) } @@ -696,24 +716,24 @@ func (m *MockStore) DeleteZoneDNSRecords(ctx context.Context, accountID, zoneID } // DeleteZoneDNSRecords indicates an expected call of DeleteZoneDNSRecords. -func (mr *MockStoreMockRecorder) DeleteZoneDNSRecords(ctx, accountID, zoneID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteZoneDNSRecords(ctx, accountID, zoneID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteZoneDNSRecords", reflect.TypeOf((*MockStore)(nil).DeleteZoneDNSRecords), ctx, accountID, zoneID) } // EphemeralServiceExists mocks base method. -func (m *MockStore) EphemeralServiceExists(ctx context.Context, lockStrength LockingStrength, accountID, peerID, domain string) (bool, error) { +func (m *MockStore) EphemeralServiceExists(ctx context.Context, lockStrength LockingStrength, accountID, peerID, arg4 string) (bool, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "EphemeralServiceExists", ctx, lockStrength, accountID, peerID, domain) + ret := m.ctrl.Call(m, "EphemeralServiceExists", ctx, lockStrength, accountID, peerID, arg4) ret0, _ := ret[0].(bool) ret1, _ := ret[1].(error) return ret0, ret1 } // EphemeralServiceExists indicates an expected call of EphemeralServiceExists. -func (mr *MockStoreMockRecorder) EphemeralServiceExists(ctx, lockStrength, accountID, peerID, domain interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) EphemeralServiceExists(ctx, lockStrength, accountID, peerID, arg4 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EphemeralServiceExists", reflect.TypeOf((*MockStore)(nil).EphemeralServiceExists), ctx, lockStrength, accountID, peerID, domain) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EphemeralServiceExists", reflect.TypeOf((*MockStore)(nil).EphemeralServiceExists), ctx, lockStrength, accountID, peerID, arg4) } // ExecuteInTransaction mocks base method. @@ -725,7 +745,7 @@ func (m *MockStore) ExecuteInTransaction(ctx context.Context, f func(Store) erro } // ExecuteInTransaction indicates an expected call of ExecuteInTransaction. -func (mr *MockStoreMockRecorder) ExecuteInTransaction(ctx, f interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) ExecuteInTransaction(ctx, f any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteInTransaction", reflect.TypeOf((*MockStore)(nil).ExecuteInTransaction), ctx, f) } @@ -740,7 +760,7 @@ func (m *MockStore) GetAccount(ctx context.Context, accountID string) (*types2.A } // GetAccount indicates an expected call of GetAccount. -func (mr *MockStoreMockRecorder) GetAccount(ctx, accountID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAccount(ctx, accountID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccount", reflect.TypeOf((*MockStore)(nil).GetAccount), ctx, accountID) } @@ -756,7 +776,7 @@ func (m *MockStore) GetAccountAccessLogs(ctx context.Context, lockStrength Locki } // GetAccountAccessLogs indicates an expected call of GetAccountAccessLogs. -func (mr *MockStoreMockRecorder) GetAccountAccessLogs(ctx, lockStrength, accountID, filter interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAccountAccessLogs(ctx, lockStrength, accountID, filter any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountAccessLogs", reflect.TypeOf((*MockStore)(nil).GetAccountAccessLogs), ctx, lockStrength, accountID, filter) } @@ -771,7 +791,7 @@ func (m *MockStore) GetAccountByPeerID(ctx context.Context, peerID string) (*typ } // GetAccountByPeerID indicates an expected call of GetAccountByPeerID. -func (mr *MockStoreMockRecorder) GetAccountByPeerID(ctx, peerID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAccountByPeerID(ctx, peerID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountByPeerID", reflect.TypeOf((*MockStore)(nil).GetAccountByPeerID), ctx, peerID) } @@ -786,24 +806,24 @@ func (m *MockStore) GetAccountByPeerPubKey(ctx context.Context, peerKey string) } // GetAccountByPeerPubKey indicates an expected call of GetAccountByPeerPubKey. -func (mr *MockStoreMockRecorder) GetAccountByPeerPubKey(ctx, peerKey interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAccountByPeerPubKey(ctx, peerKey any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountByPeerPubKey", reflect.TypeOf((*MockStore)(nil).GetAccountByPeerPubKey), ctx, peerKey) } // GetAccountByPrivateDomain mocks base method. -func (m *MockStore) GetAccountByPrivateDomain(ctx context.Context, domain string) (*types2.Account, error) { +func (m *MockStore) GetAccountByPrivateDomain(ctx context.Context, arg1 string) (*types2.Account, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAccountByPrivateDomain", ctx, domain) + ret := m.ctrl.Call(m, "GetAccountByPrivateDomain", ctx, arg1) ret0, _ := ret[0].(*types2.Account) ret1, _ := ret[1].(error) return ret0, ret1 } // GetAccountByPrivateDomain indicates an expected call of GetAccountByPrivateDomain. -func (mr *MockStoreMockRecorder) GetAccountByPrivateDomain(ctx, domain interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAccountByPrivateDomain(ctx, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountByPrivateDomain", reflect.TypeOf((*MockStore)(nil).GetAccountByPrivateDomain), ctx, domain) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountByPrivateDomain", reflect.TypeOf((*MockStore)(nil).GetAccountByPrivateDomain), ctx, arg1) } // GetAccountBySetupKey mocks base method. @@ -816,7 +836,7 @@ func (m *MockStore) GetAccountBySetupKey(ctx context.Context, setupKey string) ( } // GetAccountBySetupKey indicates an expected call of GetAccountBySetupKey. -func (mr *MockStoreMockRecorder) GetAccountBySetupKey(ctx, setupKey interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAccountBySetupKey(ctx, setupKey any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountBySetupKey", reflect.TypeOf((*MockStore)(nil).GetAccountBySetupKey), ctx, setupKey) } @@ -831,7 +851,7 @@ func (m *MockStore) GetAccountByUser(ctx context.Context, userID string) (*types } // GetAccountByUser indicates an expected call of GetAccountByUser. -func (mr *MockStoreMockRecorder) GetAccountByUser(ctx, userID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAccountByUser(ctx, userID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountByUser", reflect.TypeOf((*MockStore)(nil).GetAccountByUser), ctx, userID) } @@ -846,7 +866,7 @@ func (m *MockStore) GetAccountCreatedBy(ctx context.Context, lockStrength Lockin } // GetAccountCreatedBy indicates an expected call of GetAccountCreatedBy. -func (mr *MockStoreMockRecorder) GetAccountCreatedBy(ctx, lockStrength, accountID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAccountCreatedBy(ctx, lockStrength, accountID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountCreatedBy", reflect.TypeOf((*MockStore)(nil).GetAccountCreatedBy), ctx, lockStrength, accountID) } @@ -861,7 +881,7 @@ func (m *MockStore) GetAccountDNSSettings(ctx context.Context, lockStrength Lock } // GetAccountDNSSettings indicates an expected call of GetAccountDNSSettings. -func (mr *MockStoreMockRecorder) GetAccountDNSSettings(ctx, lockStrength, accountID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAccountDNSSettings(ctx, lockStrength, accountID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountDNSSettings", reflect.TypeOf((*MockStore)(nil).GetAccountDNSSettings), ctx, lockStrength, accountID) } @@ -877,7 +897,7 @@ func (m *MockStore) GetAccountDomainAndCategory(ctx context.Context, lockStrengt } // GetAccountDomainAndCategory indicates an expected call of GetAccountDomainAndCategory. -func (mr *MockStoreMockRecorder) GetAccountDomainAndCategory(ctx, lockStrength, accountID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAccountDomainAndCategory(ctx, lockStrength, accountID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountDomainAndCategory", reflect.TypeOf((*MockStore)(nil).GetAccountDomainAndCategory), ctx, lockStrength, accountID) } @@ -892,7 +912,7 @@ func (m *MockStore) GetAccountGroupPeers(ctx context.Context, lockStrength Locki } // GetAccountGroupPeers indicates an expected call of GetAccountGroupPeers. -func (mr *MockStoreMockRecorder) GetAccountGroupPeers(ctx, lockStrength, accountID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAccountGroupPeers(ctx, lockStrength, accountID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountGroupPeers", reflect.TypeOf((*MockStore)(nil).GetAccountGroupPeers), ctx, lockStrength, accountID) } @@ -907,7 +927,7 @@ func (m *MockStore) GetAccountGroups(ctx context.Context, lockStrength LockingSt } // GetAccountGroups indicates an expected call of GetAccountGroups. -func (mr *MockStoreMockRecorder) GetAccountGroups(ctx, lockStrength, accountID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAccountGroups(ctx, lockStrength, accountID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountGroups", reflect.TypeOf((*MockStore)(nil).GetAccountGroups), ctx, lockStrength, accountID) } @@ -922,7 +942,7 @@ func (m *MockStore) GetAccountIDByPeerID(ctx context.Context, lockStrength Locki } // GetAccountIDByPeerID indicates an expected call of GetAccountIDByPeerID. -func (mr *MockStoreMockRecorder) GetAccountIDByPeerID(ctx, lockStrength, peerID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAccountIDByPeerID(ctx, lockStrength, peerID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountIDByPeerID", reflect.TypeOf((*MockStore)(nil).GetAccountIDByPeerID), ctx, lockStrength, peerID) } @@ -937,24 +957,24 @@ func (m *MockStore) GetAccountIDByPeerPubKey(ctx context.Context, peerKey string } // GetAccountIDByPeerPubKey indicates an expected call of GetAccountIDByPeerPubKey. -func (mr *MockStoreMockRecorder) GetAccountIDByPeerPubKey(ctx, peerKey interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAccountIDByPeerPubKey(ctx, peerKey any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountIDByPeerPubKey", reflect.TypeOf((*MockStore)(nil).GetAccountIDByPeerPubKey), ctx, peerKey) } // GetAccountIDByPrivateDomain mocks base method. -func (m *MockStore) GetAccountIDByPrivateDomain(ctx context.Context, lockStrength LockingStrength, domain string) (string, error) { +func (m *MockStore) GetAccountIDByPrivateDomain(ctx context.Context, lockStrength LockingStrength, arg2 string) (string, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAccountIDByPrivateDomain", ctx, lockStrength, domain) + ret := m.ctrl.Call(m, "GetAccountIDByPrivateDomain", ctx, lockStrength, arg2) ret0, _ := ret[0].(string) ret1, _ := ret[1].(error) return ret0, ret1 } // GetAccountIDByPrivateDomain indicates an expected call of GetAccountIDByPrivateDomain. -func (mr *MockStoreMockRecorder) GetAccountIDByPrivateDomain(ctx, lockStrength, domain interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAccountIDByPrivateDomain(ctx, lockStrength, arg2 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountIDByPrivateDomain", reflect.TypeOf((*MockStore)(nil).GetAccountIDByPrivateDomain), ctx, lockStrength, domain) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountIDByPrivateDomain", reflect.TypeOf((*MockStore)(nil).GetAccountIDByPrivateDomain), ctx, lockStrength, arg2) } // GetAccountIDBySetupKey mocks base method. @@ -967,7 +987,7 @@ func (m *MockStore) GetAccountIDBySetupKey(ctx context.Context, peerKey string) } // GetAccountIDBySetupKey indicates an expected call of GetAccountIDBySetupKey. -func (mr *MockStoreMockRecorder) GetAccountIDBySetupKey(ctx, peerKey interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAccountIDBySetupKey(ctx, peerKey any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountIDBySetupKey", reflect.TypeOf((*MockStore)(nil).GetAccountIDBySetupKey), ctx, peerKey) } @@ -982,11 +1002,26 @@ func (m *MockStore) GetAccountIDByUserID(ctx context.Context, lockStrength Locki } // GetAccountIDByUserID indicates an expected call of GetAccountIDByUserID. -func (mr *MockStoreMockRecorder) GetAccountIDByUserID(ctx, lockStrength, userID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAccountIDByUserID(ctx, lockStrength, userID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountIDByUserID", reflect.TypeOf((*MockStore)(nil).GetAccountIDByUserID), ctx, lockStrength, userID) } +// GetAccountInspectionPolicies mocks base method. +func (m *MockStore) GetAccountInspectionPolicies(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types2.InspectionPolicy, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAccountInspectionPolicies", ctx, lockStrength, accountID) + ret0, _ := ret[0].([]*types2.InspectionPolicy) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAccountInspectionPolicies indicates an expected call of GetAccountInspectionPolicies. +func (mr *MockStoreMockRecorder) GetAccountInspectionPolicies(ctx, lockStrength, accountID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountInspectionPolicies", reflect.TypeOf((*MockStore)(nil).GetAccountInspectionPolicies), ctx, lockStrength, accountID) +} + // GetAccountMeta mocks base method. func (m *MockStore) GetAccountMeta(ctx context.Context, lockStrength LockingStrength, accountID string) (*types2.AccountMeta, error) { m.ctrl.T.Helper() @@ -997,7 +1032,7 @@ func (m *MockStore) GetAccountMeta(ctx context.Context, lockStrength LockingStre } // GetAccountMeta indicates an expected call of GetAccountMeta. -func (mr *MockStoreMockRecorder) GetAccountMeta(ctx, lockStrength, accountID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAccountMeta(ctx, lockStrength, accountID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountMeta", reflect.TypeOf((*MockStore)(nil).GetAccountMeta), ctx, lockStrength, accountID) } @@ -1012,7 +1047,7 @@ func (m *MockStore) GetAccountNameServerGroups(ctx context.Context, lockStrength } // GetAccountNameServerGroups indicates an expected call of GetAccountNameServerGroups. -func (mr *MockStoreMockRecorder) GetAccountNameServerGroups(ctx, lockStrength, accountID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAccountNameServerGroups(ctx, lockStrength, accountID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountNameServerGroups", reflect.TypeOf((*MockStore)(nil).GetAccountNameServerGroups), ctx, lockStrength, accountID) } @@ -1027,7 +1062,7 @@ func (m *MockStore) GetAccountNetwork(ctx context.Context, lockStrength LockingS } // GetAccountNetwork indicates an expected call of GetAccountNetwork. -func (mr *MockStoreMockRecorder) GetAccountNetwork(ctx, lockStrength, accountId interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAccountNetwork(ctx, lockStrength, accountId any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountNetwork", reflect.TypeOf((*MockStore)(nil).GetAccountNetwork), ctx, lockStrength, accountId) } @@ -1042,7 +1077,7 @@ func (m *MockStore) GetAccountNetworks(ctx context.Context, lockStrength Locking } // GetAccountNetworks indicates an expected call of GetAccountNetworks. -func (mr *MockStoreMockRecorder) GetAccountNetworks(ctx, lockStrength, accountID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAccountNetworks(ctx, lockStrength, accountID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountNetworks", reflect.TypeOf((*MockStore)(nil).GetAccountNetworks), ctx, lockStrength, accountID) } @@ -1057,7 +1092,7 @@ func (m *MockStore) GetAccountOnboarding(ctx context.Context, accountID string) } // GetAccountOnboarding indicates an expected call of GetAccountOnboarding. -func (mr *MockStoreMockRecorder) GetAccountOnboarding(ctx, accountID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAccountOnboarding(ctx, accountID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountOnboarding", reflect.TypeOf((*MockStore)(nil).GetAccountOnboarding), ctx, accountID) } @@ -1072,7 +1107,7 @@ func (m *MockStore) GetAccountOwner(ctx context.Context, lockStrength LockingStr } // GetAccountOwner indicates an expected call of GetAccountOwner. -func (mr *MockStoreMockRecorder) GetAccountOwner(ctx, lockStrength, accountID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAccountOwner(ctx, lockStrength, accountID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountOwner", reflect.TypeOf((*MockStore)(nil).GetAccountOwner), ctx, lockStrength, accountID) } @@ -1087,7 +1122,7 @@ func (m *MockStore) GetAccountPeers(ctx context.Context, lockStrength LockingStr } // GetAccountPeers indicates an expected call of GetAccountPeers. -func (mr *MockStoreMockRecorder) GetAccountPeers(ctx, lockStrength, accountID, nameFilter, ipFilter interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAccountPeers(ctx, lockStrength, accountID, nameFilter, ipFilter any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountPeers", reflect.TypeOf((*MockStore)(nil).GetAccountPeers), ctx, lockStrength, accountID, nameFilter, ipFilter) } @@ -1102,7 +1137,7 @@ func (m *MockStore) GetAccountPeersWithExpiration(ctx context.Context, lockStren } // GetAccountPeersWithExpiration indicates an expected call of GetAccountPeersWithExpiration. -func (mr *MockStoreMockRecorder) GetAccountPeersWithExpiration(ctx, lockStrength, accountID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAccountPeersWithExpiration(ctx, lockStrength, accountID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountPeersWithExpiration", reflect.TypeOf((*MockStore)(nil).GetAccountPeersWithExpiration), ctx, lockStrength, accountID) } @@ -1117,7 +1152,7 @@ func (m *MockStore) GetAccountPeersWithInactivity(ctx context.Context, lockStren } // GetAccountPeersWithInactivity indicates an expected call of GetAccountPeersWithInactivity. -func (mr *MockStoreMockRecorder) GetAccountPeersWithInactivity(ctx, lockStrength, accountID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAccountPeersWithInactivity(ctx, lockStrength, accountID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountPeersWithInactivity", reflect.TypeOf((*MockStore)(nil).GetAccountPeersWithInactivity), ctx, lockStrength, accountID) } @@ -1132,7 +1167,7 @@ func (m *MockStore) GetAccountPolicies(ctx context.Context, lockStrength Locking } // GetAccountPolicies indicates an expected call of GetAccountPolicies. -func (mr *MockStoreMockRecorder) GetAccountPolicies(ctx, lockStrength, accountID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAccountPolicies(ctx, lockStrength, accountID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountPolicies", reflect.TypeOf((*MockStore)(nil).GetAccountPolicies), ctx, lockStrength, accountID) } @@ -1147,7 +1182,7 @@ func (m *MockStore) GetAccountPostureChecks(ctx context.Context, lockStrength Lo } // GetAccountPostureChecks indicates an expected call of GetAccountPostureChecks. -func (mr *MockStoreMockRecorder) GetAccountPostureChecks(ctx, lockStrength, accountID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAccountPostureChecks(ctx, lockStrength, accountID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountPostureChecks", reflect.TypeOf((*MockStore)(nil).GetAccountPostureChecks), ctx, lockStrength, accountID) } @@ -1162,7 +1197,7 @@ func (m *MockStore) GetAccountRoutes(ctx context.Context, lockStrength LockingSt } // GetAccountRoutes indicates an expected call of GetAccountRoutes. -func (mr *MockStoreMockRecorder) GetAccountRoutes(ctx, lockStrength, accountID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAccountRoutes(ctx, lockStrength, accountID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountRoutes", reflect.TypeOf((*MockStore)(nil).GetAccountRoutes), ctx, lockStrength, accountID) } @@ -1177,7 +1212,7 @@ func (m *MockStore) GetAccountServices(ctx context.Context, lockStrength Locking } // GetAccountServices indicates an expected call of GetAccountServices. -func (mr *MockStoreMockRecorder) GetAccountServices(ctx, lockStrength, accountID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAccountServices(ctx, lockStrength, accountID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountServices", reflect.TypeOf((*MockStore)(nil).GetAccountServices), ctx, lockStrength, accountID) } @@ -1192,7 +1227,7 @@ func (m *MockStore) GetAccountSettings(ctx context.Context, lockStrength Locking } // GetAccountSettings indicates an expected call of GetAccountSettings. -func (mr *MockStoreMockRecorder) GetAccountSettings(ctx, lockStrength, accountID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAccountSettings(ctx, lockStrength, accountID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountSettings", reflect.TypeOf((*MockStore)(nil).GetAccountSettings), ctx, lockStrength, accountID) } @@ -1207,7 +1242,7 @@ func (m *MockStore) GetAccountSetupKeys(ctx context.Context, lockStrength Lockin } // GetAccountSetupKeys indicates an expected call of GetAccountSetupKeys. -func (mr *MockStoreMockRecorder) GetAccountSetupKeys(ctx, lockStrength, accountID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAccountSetupKeys(ctx, lockStrength, accountID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountSetupKeys", reflect.TypeOf((*MockStore)(nil).GetAccountSetupKeys), ctx, lockStrength, accountID) } @@ -1222,7 +1257,7 @@ func (m *MockStore) GetAccountUserInvites(ctx context.Context, lockStrength Lock } // GetAccountUserInvites indicates an expected call of GetAccountUserInvites. -func (mr *MockStoreMockRecorder) GetAccountUserInvites(ctx, lockStrength, accountID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAccountUserInvites(ctx, lockStrength, accountID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountUserInvites", reflect.TypeOf((*MockStore)(nil).GetAccountUserInvites), ctx, lockStrength, accountID) } @@ -1237,7 +1272,7 @@ func (m *MockStore) GetAccountUsers(ctx context.Context, lockStrength LockingStr } // GetAccountUsers indicates an expected call of GetAccountUsers. -func (mr *MockStoreMockRecorder) GetAccountUsers(ctx, lockStrength, accountID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAccountUsers(ctx, lockStrength, accountID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountUsers", reflect.TypeOf((*MockStore)(nil).GetAccountUsers), ctx, lockStrength, accountID) } @@ -1252,7 +1287,7 @@ func (m *MockStore) GetAccountZones(ctx context.Context, lockStrength LockingStr } // GetAccountZones indicates an expected call of GetAccountZones. -func (mr *MockStoreMockRecorder) GetAccountZones(ctx, lockStrength, accountID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAccountZones(ctx, lockStrength, accountID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountZones", reflect.TypeOf((*MockStore)(nil).GetAccountZones), ctx, lockStrength, accountID) } @@ -1267,7 +1302,7 @@ func (m *MockStore) GetAccountsCounter(ctx context.Context) (int64, error) { } // GetAccountsCounter indicates an expected call of GetAccountsCounter. -func (mr *MockStoreMockRecorder) GetAccountsCounter(ctx interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAccountsCounter(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountsCounter", reflect.TypeOf((*MockStore)(nil).GetAccountsCounter), ctx) } @@ -1282,7 +1317,7 @@ func (m *MockStore) GetActiveProxyClusterAddresses(ctx context.Context) ([]strin } // GetActiveProxyClusterAddresses indicates an expected call of GetActiveProxyClusterAddresses. -func (mr *MockStoreMockRecorder) GetActiveProxyClusterAddresses(ctx interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetActiveProxyClusterAddresses(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActiveProxyClusterAddresses", reflect.TypeOf((*MockStore)(nil).GetActiveProxyClusterAddresses), ctx) } @@ -1297,7 +1332,7 @@ func (m *MockStore) GetActiveProxyClusters(ctx context.Context) ([]proxy.Cluster } // GetActiveProxyClusters indicates an expected call of GetActiveProxyClusters. -func (mr *MockStoreMockRecorder) GetActiveProxyClusters(ctx interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetActiveProxyClusters(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActiveProxyClusters", reflect.TypeOf((*MockStore)(nil).GetActiveProxyClusters), ctx) } @@ -1311,7 +1346,7 @@ func (m *MockStore) GetAllAccounts(ctx context.Context) []*types2.Account { } // GetAllAccounts indicates an expected call of GetAllAccounts. -func (mr *MockStoreMockRecorder) GetAllAccounts(ctx interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAllAccounts(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllAccounts", reflect.TypeOf((*MockStore)(nil).GetAllAccounts), ctx) } @@ -1326,7 +1361,7 @@ func (m *MockStore) GetAllEphemeralPeers(ctx context.Context, lockStrength Locki } // GetAllEphemeralPeers indicates an expected call of GetAllEphemeralPeers. -func (mr *MockStoreMockRecorder) GetAllEphemeralPeers(ctx, lockStrength interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAllEphemeralPeers(ctx, lockStrength any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllEphemeralPeers", reflect.TypeOf((*MockStore)(nil).GetAllEphemeralPeers), ctx, lockStrength) } @@ -1341,7 +1376,7 @@ func (m *MockStore) GetAllProxyAccessTokens(ctx context.Context, lockStrength Lo } // GetAllProxyAccessTokens indicates an expected call of GetAllProxyAccessTokens. -func (mr *MockStoreMockRecorder) GetAllProxyAccessTokens(ctx, lockStrength interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAllProxyAccessTokens(ctx, lockStrength any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllProxyAccessTokens", reflect.TypeOf((*MockStore)(nil).GetAllProxyAccessTokens), ctx, lockStrength) } @@ -1356,7 +1391,7 @@ func (m *MockStore) GetAnyAccountID(ctx context.Context) (string, error) { } // GetAnyAccountID indicates an expected call of GetAnyAccountID. -func (mr *MockStoreMockRecorder) GetAnyAccountID(ctx interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAnyAccountID(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAnyAccountID", reflect.TypeOf((*MockStore)(nil).GetAnyAccountID), ctx) } @@ -1370,7 +1405,7 @@ func (m *MockStore) GetClusterRequireSubdomain(ctx context.Context, clusterAddr } // GetClusterRequireSubdomain indicates an expected call of GetClusterRequireSubdomain. -func (mr *MockStoreMockRecorder) GetClusterRequireSubdomain(ctx, clusterAddr interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetClusterRequireSubdomain(ctx, clusterAddr any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClusterRequireSubdomain", reflect.TypeOf((*MockStore)(nil).GetClusterRequireSubdomain), ctx, clusterAddr) } @@ -1384,7 +1419,7 @@ func (m *MockStore) GetClusterSupportsCustomPorts(ctx context.Context, clusterAd } // GetClusterSupportsCustomPorts indicates an expected call of GetClusterSupportsCustomPorts. -func (mr *MockStoreMockRecorder) GetClusterSupportsCustomPorts(ctx, clusterAddr interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetClusterSupportsCustomPorts(ctx, clusterAddr any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClusterSupportsCustomPorts", reflect.TypeOf((*MockStore)(nil).GetClusterSupportsCustomPorts), ctx, clusterAddr) } @@ -1399,7 +1434,7 @@ func (m *MockStore) GetCustomDomain(ctx context.Context, accountID, domainID str } // GetCustomDomain indicates an expected call of GetCustomDomain. -func (mr *MockStoreMockRecorder) GetCustomDomain(ctx, accountID, domainID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetCustomDomain(ctx, accountID, domainID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCustomDomain", reflect.TypeOf((*MockStore)(nil).GetCustomDomain), ctx, accountID, domainID) } @@ -1415,7 +1450,7 @@ func (m *MockStore) GetCustomDomainsCounts(ctx context.Context) (int64, int64, e } // GetCustomDomainsCounts indicates an expected call of GetCustomDomainsCounts. -func (mr *MockStoreMockRecorder) GetCustomDomainsCounts(ctx interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetCustomDomainsCounts(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCustomDomainsCounts", reflect.TypeOf((*MockStore)(nil).GetCustomDomainsCounts), ctx) } @@ -1430,7 +1465,7 @@ func (m *MockStore) GetDNSRecordByID(ctx context.Context, lockStrength LockingSt } // GetDNSRecordByID indicates an expected call of GetDNSRecordByID. -func (mr *MockStoreMockRecorder) GetDNSRecordByID(ctx, lockStrength, accountID, zoneID, recordID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetDNSRecordByID(ctx, lockStrength, accountID, zoneID, recordID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDNSRecordByID", reflect.TypeOf((*MockStore)(nil).GetDNSRecordByID), ctx, lockStrength, accountID, zoneID, recordID) } @@ -1445,7 +1480,7 @@ func (m *MockStore) GetExpiredEphemeralServices(ctx context.Context, ttl time.Du } // GetExpiredEphemeralServices indicates an expected call of GetExpiredEphemeralServices. -func (mr *MockStoreMockRecorder) GetExpiredEphemeralServices(ctx, ttl, limit interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetExpiredEphemeralServices(ctx, ttl, limit any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetExpiredEphemeralServices", reflect.TypeOf((*MockStore)(nil).GetExpiredEphemeralServices), ctx, ttl, limit) } @@ -1460,24 +1495,24 @@ func (m *MockStore) GetGroupByID(ctx context.Context, lockStrength LockingStreng } // GetGroupByID indicates an expected call of GetGroupByID. -func (mr *MockStoreMockRecorder) GetGroupByID(ctx, lockStrength, accountID, groupID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetGroupByID(ctx, lockStrength, accountID, groupID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupByID", reflect.TypeOf((*MockStore)(nil).GetGroupByID), ctx, lockStrength, accountID, groupID) } // GetGroupByName mocks base method. -func (m *MockStore) GetGroupByName(ctx context.Context, lockStrength LockingStrength, accountID, groupName string) (*types2.Group, error) { +func (m *MockStore) GetGroupByName(ctx context.Context, lockStrength LockingStrength, groupName, accountID string) (*types2.Group, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetGroupByName", ctx, lockStrength, accountID, groupName) + ret := m.ctrl.Call(m, "GetGroupByName", ctx, lockStrength, groupName, accountID) ret0, _ := ret[0].(*types2.Group) ret1, _ := ret[1].(error) return ret0, ret1 } // GetGroupByName indicates an expected call of GetGroupByName. -func (mr *MockStoreMockRecorder) GetGroupByName(ctx, lockStrength, accountID, groupName interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetGroupByName(ctx, lockStrength, groupName, accountID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupByName", reflect.TypeOf((*MockStore)(nil).GetGroupByName), ctx, lockStrength, accountID, groupName) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupByName", reflect.TypeOf((*MockStore)(nil).GetGroupByName), ctx, lockStrength, groupName, accountID) } // GetGroupsByIDs mocks base method. @@ -1490,11 +1525,26 @@ func (m *MockStore) GetGroupsByIDs(ctx context.Context, lockStrength LockingStre } // GetGroupsByIDs indicates an expected call of GetGroupsByIDs. -func (mr *MockStoreMockRecorder) GetGroupsByIDs(ctx, lockStrength, accountID, groupIDs interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetGroupsByIDs(ctx, lockStrength, accountID, groupIDs any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupsByIDs", reflect.TypeOf((*MockStore)(nil).GetGroupsByIDs), ctx, lockStrength, accountID, groupIDs) } +// GetInspectionPolicyByID mocks base method. +func (m *MockStore) GetInspectionPolicyByID(ctx context.Context, lockStrength LockingStrength, accountID, policyID string) (*types2.InspectionPolicy, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetInspectionPolicyByID", ctx, lockStrength, accountID, policyID) + ret0, _ := ret[0].(*types2.InspectionPolicy) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetInspectionPolicyByID indicates an expected call of GetInspectionPolicyByID. +func (mr *MockStoreMockRecorder) GetInspectionPolicyByID(ctx, lockStrength, accountID, policyID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInspectionPolicyByID", reflect.TypeOf((*MockStore)(nil).GetInspectionPolicyByID), ctx, lockStrength, accountID, policyID) +} + // GetInstallationID mocks base method. func (m *MockStore) GetInstallationID() string { m.ctrl.T.Helper() @@ -1519,7 +1569,7 @@ func (m *MockStore) GetNameServerGroupByID(ctx context.Context, lockStrength Loc } // GetNameServerGroupByID indicates an expected call of GetNameServerGroupByID. -func (mr *MockStoreMockRecorder) GetNameServerGroupByID(ctx, lockStrength, nameServerGroupID, accountID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetNameServerGroupByID(ctx, lockStrength, nameServerGroupID, accountID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNameServerGroupByID", reflect.TypeOf((*MockStore)(nil).GetNameServerGroupByID), ctx, lockStrength, nameServerGroupID, accountID) } @@ -1534,7 +1584,7 @@ func (m *MockStore) GetNetworkByID(ctx context.Context, lockStrength LockingStre } // GetNetworkByID indicates an expected call of GetNetworkByID. -func (mr *MockStoreMockRecorder) GetNetworkByID(ctx, lockStrength, accountID, networkID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetNetworkByID(ctx, lockStrength, accountID, networkID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNetworkByID", reflect.TypeOf((*MockStore)(nil).GetNetworkByID), ctx, lockStrength, accountID, networkID) } @@ -1549,7 +1599,7 @@ func (m *MockStore) GetNetworkResourceByID(ctx context.Context, lockStrength Loc } // GetNetworkResourceByID indicates an expected call of GetNetworkResourceByID. -func (mr *MockStoreMockRecorder) GetNetworkResourceByID(ctx, lockStrength, accountID, resourceID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetNetworkResourceByID(ctx, lockStrength, accountID, resourceID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNetworkResourceByID", reflect.TypeOf((*MockStore)(nil).GetNetworkResourceByID), ctx, lockStrength, accountID, resourceID) } @@ -1564,7 +1614,7 @@ func (m *MockStore) GetNetworkResourceByName(ctx context.Context, lockStrength L } // GetNetworkResourceByName indicates an expected call of GetNetworkResourceByName. -func (mr *MockStoreMockRecorder) GetNetworkResourceByName(ctx, lockStrength, accountID, resourceName interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetNetworkResourceByName(ctx, lockStrength, accountID, resourceName any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNetworkResourceByName", reflect.TypeOf((*MockStore)(nil).GetNetworkResourceByName), ctx, lockStrength, accountID, resourceName) } @@ -1579,7 +1629,7 @@ func (m *MockStore) GetNetworkResourcesByAccountID(ctx context.Context, lockStre } // GetNetworkResourcesByAccountID indicates an expected call of GetNetworkResourcesByAccountID. -func (mr *MockStoreMockRecorder) GetNetworkResourcesByAccountID(ctx, lockStrength, accountID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetNetworkResourcesByAccountID(ctx, lockStrength, accountID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNetworkResourcesByAccountID", reflect.TypeOf((*MockStore)(nil).GetNetworkResourcesByAccountID), ctx, lockStrength, accountID) } @@ -1594,7 +1644,7 @@ func (m *MockStore) GetNetworkResourcesByNetID(ctx context.Context, lockStrength } // GetNetworkResourcesByNetID indicates an expected call of GetNetworkResourcesByNetID. -func (mr *MockStoreMockRecorder) GetNetworkResourcesByNetID(ctx, lockStrength, accountID, netID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetNetworkResourcesByNetID(ctx, lockStrength, accountID, netID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNetworkResourcesByNetID", reflect.TypeOf((*MockStore)(nil).GetNetworkResourcesByNetID), ctx, lockStrength, accountID, netID) } @@ -1609,7 +1659,7 @@ func (m *MockStore) GetNetworkRouterByID(ctx context.Context, lockStrength Locki } // GetNetworkRouterByID indicates an expected call of GetNetworkRouterByID. -func (mr *MockStoreMockRecorder) GetNetworkRouterByID(ctx, lockStrength, accountID, routerID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetNetworkRouterByID(ctx, lockStrength, accountID, routerID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNetworkRouterByID", reflect.TypeOf((*MockStore)(nil).GetNetworkRouterByID), ctx, lockStrength, accountID, routerID) } @@ -1624,7 +1674,7 @@ func (m *MockStore) GetNetworkRoutersByAccountID(ctx context.Context, lockStreng } // GetNetworkRoutersByAccountID indicates an expected call of GetNetworkRoutersByAccountID. -func (mr *MockStoreMockRecorder) GetNetworkRoutersByAccountID(ctx, lockStrength, accountID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetNetworkRoutersByAccountID(ctx, lockStrength, accountID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNetworkRoutersByAccountID", reflect.TypeOf((*MockStore)(nil).GetNetworkRoutersByAccountID), ctx, lockStrength, accountID) } @@ -1639,7 +1689,7 @@ func (m *MockStore) GetNetworkRoutersByNetID(ctx context.Context, lockStrength L } // GetNetworkRoutersByNetID indicates an expected call of GetNetworkRoutersByNetID. -func (mr *MockStoreMockRecorder) GetNetworkRoutersByNetID(ctx, lockStrength, accountID, netID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetNetworkRoutersByNetID(ctx, lockStrength, accountID, netID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNetworkRoutersByNetID", reflect.TypeOf((*MockStore)(nil).GetNetworkRoutersByNetID), ctx, lockStrength, accountID, netID) } @@ -1654,7 +1704,7 @@ func (m *MockStore) GetPATByHashedToken(ctx context.Context, lockStrength Lockin } // GetPATByHashedToken indicates an expected call of GetPATByHashedToken. -func (mr *MockStoreMockRecorder) GetPATByHashedToken(ctx, lockStrength, hashedToken interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetPATByHashedToken(ctx, lockStrength, hashedToken any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPATByHashedToken", reflect.TypeOf((*MockStore)(nil).GetPATByHashedToken), ctx, lockStrength, hashedToken) } @@ -1669,7 +1719,7 @@ func (m *MockStore) GetPATByID(ctx context.Context, lockStrength LockingStrength } // GetPATByID indicates an expected call of GetPATByID. -func (mr *MockStoreMockRecorder) GetPATByID(ctx, lockStrength, userID, patID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetPATByID(ctx, lockStrength, userID, patID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPATByID", reflect.TypeOf((*MockStore)(nil).GetPATByID), ctx, lockStrength, userID, patID) } @@ -1684,7 +1734,7 @@ func (m *MockStore) GetPeerByID(ctx context.Context, lockStrength LockingStrengt } // GetPeerByID indicates an expected call of GetPeerByID. -func (mr *MockStoreMockRecorder) GetPeerByID(ctx, lockStrength, accountID, peerID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetPeerByID(ctx, lockStrength, accountID, peerID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPeerByID", reflect.TypeOf((*MockStore)(nil).GetPeerByID), ctx, lockStrength, accountID, peerID) } @@ -1699,7 +1749,7 @@ func (m *MockStore) GetPeerByIP(ctx context.Context, lockStrength LockingStrengt } // GetPeerByIP indicates an expected call of GetPeerByIP. -func (mr *MockStoreMockRecorder) GetPeerByIP(ctx, lockStrength, accountID, ip interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetPeerByIP(ctx, lockStrength, accountID, ip any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPeerByIP", reflect.TypeOf((*MockStore)(nil).GetPeerByIP), ctx, lockStrength, accountID, ip) } @@ -1714,7 +1764,7 @@ func (m *MockStore) GetPeerByPeerPubKey(ctx context.Context, lockStrength Lockin } // GetPeerByPeerPubKey indicates an expected call of GetPeerByPeerPubKey. -func (mr *MockStoreMockRecorder) GetPeerByPeerPubKey(ctx, lockStrength, peerKey interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetPeerByPeerPubKey(ctx, lockStrength, peerKey any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPeerByPeerPubKey", reflect.TypeOf((*MockStore)(nil).GetPeerByPeerPubKey), ctx, lockStrength, peerKey) } @@ -1729,7 +1779,7 @@ func (m *MockStore) GetPeerGroupIDs(ctx context.Context, lockStrength LockingStr } // GetPeerGroupIDs indicates an expected call of GetPeerGroupIDs. -func (mr *MockStoreMockRecorder) GetPeerGroupIDs(ctx, lockStrength, accountId, peerId interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetPeerGroupIDs(ctx, lockStrength, accountId, peerId any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPeerGroupIDs", reflect.TypeOf((*MockStore)(nil).GetPeerGroupIDs), ctx, lockStrength, accountId, peerId) } @@ -1744,7 +1794,7 @@ func (m *MockStore) GetPeerGroups(ctx context.Context, lockStrength LockingStren } // GetPeerGroups indicates an expected call of GetPeerGroups. -func (mr *MockStoreMockRecorder) GetPeerGroups(ctx, lockStrength, accountId, peerId interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetPeerGroups(ctx, lockStrength, accountId, peerId any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPeerGroups", reflect.TypeOf((*MockStore)(nil).GetPeerGroups), ctx, lockStrength, accountId, peerId) } @@ -1759,7 +1809,7 @@ func (m *MockStore) GetPeerIDByKey(ctx context.Context, lockStrength LockingStre } // GetPeerIDByKey indicates an expected call of GetPeerIDByKey. -func (mr *MockStoreMockRecorder) GetPeerIDByKey(ctx, lockStrength, key interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetPeerIDByKey(ctx, lockStrength, key any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPeerIDByKey", reflect.TypeOf((*MockStore)(nil).GetPeerIDByKey), ctx, lockStrength, key) } @@ -1774,7 +1824,7 @@ func (m *MockStore) GetPeerIdByLabel(ctx context.Context, lockStrength LockingSt } // GetPeerIdByLabel indicates an expected call of GetPeerIdByLabel. -func (mr *MockStoreMockRecorder) GetPeerIdByLabel(ctx, lockStrength, accountID, hostname interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetPeerIdByLabel(ctx, lockStrength, accountID, hostname any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPeerIdByLabel", reflect.TypeOf((*MockStore)(nil).GetPeerIdByLabel), ctx, lockStrength, accountID, hostname) } @@ -1789,7 +1839,7 @@ func (m *MockStore) GetPeerJobByID(ctx context.Context, accountID, jobID string) } // GetPeerJobByID indicates an expected call of GetPeerJobByID. -func (mr *MockStoreMockRecorder) GetPeerJobByID(ctx, accountID, jobID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetPeerJobByID(ctx, accountID, jobID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPeerJobByID", reflect.TypeOf((*MockStore)(nil).GetPeerJobByID), ctx, accountID, jobID) } @@ -1804,7 +1854,7 @@ func (m *MockStore) GetPeerJobs(ctx context.Context, accountID, peerID string) ( } // GetPeerJobs indicates an expected call of GetPeerJobs. -func (mr *MockStoreMockRecorder) GetPeerJobs(ctx, accountID, peerID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetPeerJobs(ctx, accountID, peerID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPeerJobs", reflect.TypeOf((*MockStore)(nil).GetPeerJobs), ctx, accountID, peerID) } @@ -1819,7 +1869,7 @@ func (m *MockStore) GetPeerLabelsInAccount(ctx context.Context, lockStrength Loc } // GetPeerLabelsInAccount indicates an expected call of GetPeerLabelsInAccount. -func (mr *MockStoreMockRecorder) GetPeerLabelsInAccount(ctx, lockStrength, accountId, hostname interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetPeerLabelsInAccount(ctx, lockStrength, accountId, hostname any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPeerLabelsInAccount", reflect.TypeOf((*MockStore)(nil).GetPeerLabelsInAccount), ctx, lockStrength, accountId, hostname) } @@ -1834,7 +1884,7 @@ func (m *MockStore) GetPeersByGroupIDs(ctx context.Context, accountID string, gr } // GetPeersByGroupIDs indicates an expected call of GetPeersByGroupIDs. -func (mr *MockStoreMockRecorder) GetPeersByGroupIDs(ctx, accountID, groupIDs interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetPeersByGroupIDs(ctx, accountID, groupIDs any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPeersByGroupIDs", reflect.TypeOf((*MockStore)(nil).GetPeersByGroupIDs), ctx, accountID, groupIDs) } @@ -1849,7 +1899,7 @@ func (m *MockStore) GetPeersByIDs(ctx context.Context, lockStrength LockingStren } // GetPeersByIDs indicates an expected call of GetPeersByIDs. -func (mr *MockStoreMockRecorder) GetPeersByIDs(ctx, lockStrength, accountID, peerIDs interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetPeersByIDs(ctx, lockStrength, accountID, peerIDs any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPeersByIDs", reflect.TypeOf((*MockStore)(nil).GetPeersByIDs), ctx, lockStrength, accountID, peerIDs) } @@ -1864,7 +1914,7 @@ func (m *MockStore) GetPolicyByID(ctx context.Context, lockStrength LockingStren } // GetPolicyByID indicates an expected call of GetPolicyByID. -func (mr *MockStoreMockRecorder) GetPolicyByID(ctx, lockStrength, accountID, policyID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetPolicyByID(ctx, lockStrength, accountID, policyID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPolicyByID", reflect.TypeOf((*MockStore)(nil).GetPolicyByID), ctx, lockStrength, accountID, policyID) } @@ -1879,7 +1929,7 @@ func (m *MockStore) GetPolicyRulesByResourceID(ctx context.Context, lockStrength } // GetPolicyRulesByResourceID indicates an expected call of GetPolicyRulesByResourceID. -func (mr *MockStoreMockRecorder) GetPolicyRulesByResourceID(ctx, lockStrength, accountID, peerID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetPolicyRulesByResourceID(ctx, lockStrength, accountID, peerID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPolicyRulesByResourceID", reflect.TypeOf((*MockStore)(nil).GetPolicyRulesByResourceID), ctx, lockStrength, accountID, peerID) } @@ -1894,7 +1944,7 @@ func (m *MockStore) GetPostureCheckByChecksDefinition(accountID string, checks * } // GetPostureCheckByChecksDefinition indicates an expected call of GetPostureCheckByChecksDefinition. -func (mr *MockStoreMockRecorder) GetPostureCheckByChecksDefinition(accountID, checks interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetPostureCheckByChecksDefinition(accountID, checks any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPostureCheckByChecksDefinition", reflect.TypeOf((*MockStore)(nil).GetPostureCheckByChecksDefinition), accountID, checks) } @@ -1909,7 +1959,7 @@ func (m *MockStore) GetPostureChecksByID(ctx context.Context, lockStrength Locki } // GetPostureChecksByID indicates an expected call of GetPostureChecksByID. -func (mr *MockStoreMockRecorder) GetPostureChecksByID(ctx, lockStrength, accountID, postureCheckID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetPostureChecksByID(ctx, lockStrength, accountID, postureCheckID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPostureChecksByID", reflect.TypeOf((*MockStore)(nil).GetPostureChecksByID), ctx, lockStrength, accountID, postureCheckID) } @@ -1924,7 +1974,7 @@ func (m *MockStore) GetPostureChecksByIDs(ctx context.Context, lockStrength Lock } // GetPostureChecksByIDs indicates an expected call of GetPostureChecksByIDs. -func (mr *MockStoreMockRecorder) GetPostureChecksByIDs(ctx, lockStrength, accountID, postureChecksIDs interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetPostureChecksByIDs(ctx, lockStrength, accountID, postureChecksIDs any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPostureChecksByIDs", reflect.TypeOf((*MockStore)(nil).GetPostureChecksByIDs), ctx, lockStrength, accountID, postureChecksIDs) } @@ -1939,7 +1989,7 @@ func (m *MockStore) GetProxyAccessTokenByHashedToken(ctx context.Context, lockSt } // GetProxyAccessTokenByHashedToken indicates an expected call of GetProxyAccessTokenByHashedToken. -func (mr *MockStoreMockRecorder) GetProxyAccessTokenByHashedToken(ctx, lockStrength, hashedToken interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetProxyAccessTokenByHashedToken(ctx, lockStrength, hashedToken any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProxyAccessTokenByHashedToken", reflect.TypeOf((*MockStore)(nil).GetProxyAccessTokenByHashedToken), ctx, lockStrength, hashedToken) } @@ -1954,7 +2004,7 @@ func (m *MockStore) GetResourceGroups(ctx context.Context, lockStrength LockingS } // GetResourceGroups indicates an expected call of GetResourceGroups. -func (mr *MockStoreMockRecorder) GetResourceGroups(ctx, lockStrength, accountID, resourceID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetResourceGroups(ctx, lockStrength, accountID, resourceID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetResourceGroups", reflect.TypeOf((*MockStore)(nil).GetResourceGroups), ctx, lockStrength, accountID, resourceID) } @@ -1969,7 +2019,7 @@ func (m *MockStore) GetRouteByID(ctx context.Context, lockStrength LockingStreng } // GetRouteByID indicates an expected call of GetRouteByID. -func (mr *MockStoreMockRecorder) GetRouteByID(ctx, lockStrength, accountID, routeID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetRouteByID(ctx, lockStrength, accountID, routeID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRouteByID", reflect.TypeOf((*MockStore)(nil).GetRouteByID), ctx, lockStrength, accountID, routeID) } @@ -1984,24 +2034,24 @@ func (m *MockStore) GetRoutingPeerNetworks(ctx context.Context, accountID, peerI } // GetRoutingPeerNetworks indicates an expected call of GetRoutingPeerNetworks. -func (mr *MockStoreMockRecorder) GetRoutingPeerNetworks(ctx, accountID, peerID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetRoutingPeerNetworks(ctx, accountID, peerID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRoutingPeerNetworks", reflect.TypeOf((*MockStore)(nil).GetRoutingPeerNetworks), ctx, accountID, peerID) } // GetServiceByDomain mocks base method. -func (m *MockStore) GetServiceByDomain(ctx context.Context, domain string) (*service.Service, error) { +func (m *MockStore) GetServiceByDomain(ctx context.Context, arg1 string) (*service.Service, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetServiceByDomain", ctx, domain) + ret := m.ctrl.Call(m, "GetServiceByDomain", ctx, arg1) ret0, _ := ret[0].(*service.Service) ret1, _ := ret[1].(error) return ret0, ret1 } // GetServiceByDomain indicates an expected call of GetServiceByDomain. -func (mr *MockStoreMockRecorder) GetServiceByDomain(ctx, domain interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetServiceByDomain(ctx, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServiceByDomain", reflect.TypeOf((*MockStore)(nil).GetServiceByDomain), ctx, domain) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServiceByDomain", reflect.TypeOf((*MockStore)(nil).GetServiceByDomain), ctx, arg1) } // GetServiceByID mocks base method. @@ -2014,7 +2064,7 @@ func (m *MockStore) GetServiceByID(ctx context.Context, lockStrength LockingStre } // GetServiceByID indicates an expected call of GetServiceByID. -func (mr *MockStoreMockRecorder) GetServiceByID(ctx, lockStrength, accountID, serviceID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetServiceByID(ctx, lockStrength, accountID, serviceID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServiceByID", reflect.TypeOf((*MockStore)(nil).GetServiceByID), ctx, lockStrength, accountID, serviceID) } @@ -2029,7 +2079,7 @@ func (m *MockStore) GetServiceTargetByTargetID(ctx context.Context, lockStrength } // GetServiceTargetByTargetID indicates an expected call of GetServiceTargetByTargetID. -func (mr *MockStoreMockRecorder) GetServiceTargetByTargetID(ctx, lockStrength, accountID, targetID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetServiceTargetByTargetID(ctx, lockStrength, accountID, targetID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServiceTargetByTargetID", reflect.TypeOf((*MockStore)(nil).GetServiceTargetByTargetID), ctx, lockStrength, accountID, targetID) } @@ -2044,7 +2094,7 @@ func (m *MockStore) GetServices(ctx context.Context, lockStrength LockingStrengt } // GetServices indicates an expected call of GetServices. -func (mr *MockStoreMockRecorder) GetServices(ctx, lockStrength interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetServices(ctx, lockStrength any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServices", reflect.TypeOf((*MockStore)(nil).GetServices), ctx, lockStrength) } @@ -2059,7 +2109,7 @@ func (m *MockStore) GetServicesByCluster(ctx context.Context, lockStrength Locki } // GetServicesByCluster indicates an expected call of GetServicesByCluster. -func (mr *MockStoreMockRecorder) GetServicesByCluster(ctx, lockStrength, proxyCluster interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetServicesByCluster(ctx, lockStrength, proxyCluster any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServicesByCluster", reflect.TypeOf((*MockStore)(nil).GetServicesByCluster), ctx, lockStrength, proxyCluster) } @@ -2074,7 +2124,7 @@ func (m *MockStore) GetServicesByClusterAndPort(ctx context.Context, lockStrengt } // GetServicesByClusterAndPort indicates an expected call of GetServicesByClusterAndPort. -func (mr *MockStoreMockRecorder) GetServicesByClusterAndPort(ctx, lockStrength, proxyCluster, mode, listenPort interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetServicesByClusterAndPort(ctx, lockStrength, proxyCluster, mode, listenPort any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServicesByClusterAndPort", reflect.TypeOf((*MockStore)(nil).GetServicesByClusterAndPort), ctx, lockStrength, proxyCluster, mode, listenPort) } @@ -2089,7 +2139,7 @@ func (m *MockStore) GetSetupKeyByID(ctx context.Context, lockStrength LockingStr } // GetSetupKeyByID indicates an expected call of GetSetupKeyByID. -func (mr *MockStoreMockRecorder) GetSetupKeyByID(ctx, lockStrength, accountID, setupKeyID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetSetupKeyByID(ctx, lockStrength, accountID, setupKeyID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSetupKeyByID", reflect.TypeOf((*MockStore)(nil).GetSetupKeyByID), ctx, lockStrength, accountID, setupKeyID) } @@ -2104,7 +2154,7 @@ func (m *MockStore) GetSetupKeyBySecret(ctx context.Context, lockStrength Lockin } // GetSetupKeyBySecret indicates an expected call of GetSetupKeyBySecret. -func (mr *MockStoreMockRecorder) GetSetupKeyBySecret(ctx, lockStrength, key interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetSetupKeyBySecret(ctx, lockStrength, key any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSetupKeyBySecret", reflect.TypeOf((*MockStore)(nil).GetSetupKeyBySecret), ctx, lockStrength, key) } @@ -2133,7 +2183,7 @@ func (m *MockStore) GetTakenIPs(ctx context.Context, lockStrength LockingStrengt } // GetTakenIPs indicates an expected call of GetTakenIPs. -func (mr *MockStoreMockRecorder) GetTakenIPs(ctx, lockStrength, accountId interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetTakenIPs(ctx, lockStrength, accountId any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTakenIPs", reflect.TypeOf((*MockStore)(nil).GetTakenIPs), ctx, lockStrength, accountId) } @@ -2148,7 +2198,7 @@ func (m *MockStore) GetTargetsByServiceID(ctx context.Context, lockStrength Lock } // GetTargetsByServiceID indicates an expected call of GetTargetsByServiceID. -func (mr *MockStoreMockRecorder) GetTargetsByServiceID(ctx, lockStrength, accountID, serviceID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetTargetsByServiceID(ctx, lockStrength, accountID, serviceID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTargetsByServiceID", reflect.TypeOf((*MockStore)(nil).GetTargetsByServiceID), ctx, lockStrength, accountID, serviceID) } @@ -2163,7 +2213,7 @@ func (m *MockStore) GetTokenIDByHashedToken(ctx context.Context, secret string) } // GetTokenIDByHashedToken indicates an expected call of GetTokenIDByHashedToken. -func (mr *MockStoreMockRecorder) GetTokenIDByHashedToken(ctx, secret interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetTokenIDByHashedToken(ctx, secret any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTokenIDByHashedToken", reflect.TypeOf((*MockStore)(nil).GetTokenIDByHashedToken), ctx, secret) } @@ -2178,7 +2228,7 @@ func (m *MockStore) GetUserByPATID(ctx context.Context, lockStrength LockingStre } // GetUserByPATID indicates an expected call of GetUserByPATID. -func (mr *MockStoreMockRecorder) GetUserByPATID(ctx, lockStrength, patID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetUserByPATID(ctx, lockStrength, patID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByPATID", reflect.TypeOf((*MockStore)(nil).GetUserByPATID), ctx, lockStrength, patID) } @@ -2193,7 +2243,7 @@ func (m *MockStore) GetUserByUserID(ctx context.Context, lockStrength LockingStr } // GetUserByUserID indicates an expected call of GetUserByUserID. -func (mr *MockStoreMockRecorder) GetUserByUserID(ctx, lockStrength, userID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetUserByUserID(ctx, lockStrength, userID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByUserID", reflect.TypeOf((*MockStore)(nil).GetUserByUserID), ctx, lockStrength, userID) } @@ -2208,7 +2258,7 @@ func (m *MockStore) GetUserIDByPeerKey(ctx context.Context, lockStrength Locking } // GetUserIDByPeerKey indicates an expected call of GetUserIDByPeerKey. -func (mr *MockStoreMockRecorder) GetUserIDByPeerKey(ctx, lockStrength, peerKey interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetUserIDByPeerKey(ctx, lockStrength, peerKey any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserIDByPeerKey", reflect.TypeOf((*MockStore)(nil).GetUserIDByPeerKey), ctx, lockStrength, peerKey) } @@ -2223,7 +2273,7 @@ func (m *MockStore) GetUserInviteByEmail(ctx context.Context, lockStrength Locki } // GetUserInviteByEmail indicates an expected call of GetUserInviteByEmail. -func (mr *MockStoreMockRecorder) GetUserInviteByEmail(ctx, lockStrength, accountID, email interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetUserInviteByEmail(ctx, lockStrength, accountID, email any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserInviteByEmail", reflect.TypeOf((*MockStore)(nil).GetUserInviteByEmail), ctx, lockStrength, accountID, email) } @@ -2238,7 +2288,7 @@ func (m *MockStore) GetUserInviteByHashedToken(ctx context.Context, lockStrength } // GetUserInviteByHashedToken indicates an expected call of GetUserInviteByHashedToken. -func (mr *MockStoreMockRecorder) GetUserInviteByHashedToken(ctx, lockStrength, hashedToken interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetUserInviteByHashedToken(ctx, lockStrength, hashedToken any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserInviteByHashedToken", reflect.TypeOf((*MockStore)(nil).GetUserInviteByHashedToken), ctx, lockStrength, hashedToken) } @@ -2253,7 +2303,7 @@ func (m *MockStore) GetUserInviteByID(ctx context.Context, lockStrength LockingS } // GetUserInviteByID indicates an expected call of GetUserInviteByID. -func (mr *MockStoreMockRecorder) GetUserInviteByID(ctx, lockStrength, accountID, inviteID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetUserInviteByID(ctx, lockStrength, accountID, inviteID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserInviteByID", reflect.TypeOf((*MockStore)(nil).GetUserInviteByID), ctx, lockStrength, accountID, inviteID) } @@ -2268,7 +2318,7 @@ func (m *MockStore) GetUserPATs(ctx context.Context, lockStrength LockingStrengt } // GetUserPATs indicates an expected call of GetUserPATs. -func (mr *MockStoreMockRecorder) GetUserPATs(ctx, lockStrength, userID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetUserPATs(ctx, lockStrength, userID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserPATs", reflect.TypeOf((*MockStore)(nil).GetUserPATs), ctx, lockStrength, userID) } @@ -2283,24 +2333,24 @@ func (m *MockStore) GetUserPeers(ctx context.Context, lockStrength LockingStreng } // GetUserPeers indicates an expected call of GetUserPeers. -func (mr *MockStoreMockRecorder) GetUserPeers(ctx, lockStrength, accountID, userID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetUserPeers(ctx, lockStrength, accountID, userID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserPeers", reflect.TypeOf((*MockStore)(nil).GetUserPeers), ctx, lockStrength, accountID, userID) } // GetZoneByDomain mocks base method. -func (m *MockStore) GetZoneByDomain(ctx context.Context, accountID, domain string) (*zones.Zone, error) { +func (m *MockStore) GetZoneByDomain(ctx context.Context, accountID, arg2 string) (*zones.Zone, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetZoneByDomain", ctx, accountID, domain) + ret := m.ctrl.Call(m, "GetZoneByDomain", ctx, accountID, arg2) ret0, _ := ret[0].(*zones.Zone) ret1, _ := ret[1].(error) return ret0, ret1 } // GetZoneByDomain indicates an expected call of GetZoneByDomain. -func (mr *MockStoreMockRecorder) GetZoneByDomain(ctx, accountID, domain interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetZoneByDomain(ctx, accountID, arg2 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetZoneByDomain", reflect.TypeOf((*MockStore)(nil).GetZoneByDomain), ctx, accountID, domain) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetZoneByDomain", reflect.TypeOf((*MockStore)(nil).GetZoneByDomain), ctx, accountID, arg2) } // GetZoneByID mocks base method. @@ -2313,7 +2363,7 @@ func (m *MockStore) GetZoneByID(ctx context.Context, lockStrength LockingStrengt } // GetZoneByID indicates an expected call of GetZoneByID. -func (mr *MockStoreMockRecorder) GetZoneByID(ctx, lockStrength, accountID, zoneID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetZoneByID(ctx, lockStrength, accountID, zoneID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetZoneByID", reflect.TypeOf((*MockStore)(nil).GetZoneByID), ctx, lockStrength, accountID, zoneID) } @@ -2328,7 +2378,7 @@ func (m *MockStore) GetZoneDNSRecords(ctx context.Context, lockStrength LockingS } // GetZoneDNSRecords indicates an expected call of GetZoneDNSRecords. -func (mr *MockStoreMockRecorder) GetZoneDNSRecords(ctx, lockStrength, accountID, zoneID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetZoneDNSRecords(ctx, lockStrength, accountID, zoneID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetZoneDNSRecords", reflect.TypeOf((*MockStore)(nil).GetZoneDNSRecords), ctx, lockStrength, accountID, zoneID) } @@ -2343,7 +2393,7 @@ func (m *MockStore) GetZoneDNSRecordsByName(ctx context.Context, lockStrength Lo } // GetZoneDNSRecordsByName indicates an expected call of GetZoneDNSRecordsByName. -func (mr *MockStoreMockRecorder) GetZoneDNSRecordsByName(ctx, lockStrength, accountID, zoneID, name interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetZoneDNSRecordsByName(ctx, lockStrength, accountID, zoneID, name any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetZoneDNSRecordsByName", reflect.TypeOf((*MockStore)(nil).GetZoneDNSRecordsByName), ctx, lockStrength, accountID, zoneID, name) } @@ -2357,7 +2407,7 @@ func (m *MockStore) IncrementNetworkSerial(ctx context.Context, accountId string } // IncrementNetworkSerial indicates an expected call of IncrementNetworkSerial. -func (mr *MockStoreMockRecorder) IncrementNetworkSerial(ctx, accountId interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) IncrementNetworkSerial(ctx, accountId any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IncrementNetworkSerial", reflect.TypeOf((*MockStore)(nil).IncrementNetworkSerial), ctx, accountId) } @@ -2371,7 +2421,7 @@ func (m *MockStore) IncrementSetupKeyUsage(ctx context.Context, setupKeyID strin } // IncrementSetupKeyUsage indicates an expected call of IncrementSetupKeyUsage. -func (mr *MockStoreMockRecorder) IncrementSetupKeyUsage(ctx, setupKeyID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) IncrementSetupKeyUsage(ctx, setupKeyID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IncrementSetupKeyUsage", reflect.TypeOf((*MockStore)(nil).IncrementSetupKeyUsage), ctx, setupKeyID) } @@ -2387,7 +2437,7 @@ func (m *MockStore) IsPrimaryAccount(ctx context.Context, accountID string) (boo } // IsPrimaryAccount indicates an expected call of IsPrimaryAccount. -func (mr *MockStoreMockRecorder) IsPrimaryAccount(ctx, accountID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) IsPrimaryAccount(ctx, accountID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsPrimaryAccount", reflect.TypeOf((*MockStore)(nil).IsPrimaryAccount), ctx, accountID) } @@ -2402,7 +2452,7 @@ func (m *MockStore) ListCustomDomains(ctx context.Context, accountID string) ([] } // ListCustomDomains indicates an expected call of ListCustomDomains. -func (mr *MockStoreMockRecorder) ListCustomDomains(ctx, accountID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) ListCustomDomains(ctx, accountID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListCustomDomains", reflect.TypeOf((*MockStore)(nil).ListCustomDomains), ctx, accountID) } @@ -2417,7 +2467,7 @@ func (m *MockStore) ListFreeDomains(ctx context.Context, accountID string) ([]st } // ListFreeDomains indicates an expected call of ListFreeDomains. -func (mr *MockStoreMockRecorder) ListFreeDomains(ctx, accountID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) ListFreeDomains(ctx, accountID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListFreeDomains", reflect.TypeOf((*MockStore)(nil).ListFreeDomains), ctx, accountID) } @@ -2431,7 +2481,7 @@ func (m *MockStore) MarkAccountPrimary(ctx context.Context, accountID string) er } // MarkAccountPrimary indicates an expected call of MarkAccountPrimary. -func (mr *MockStoreMockRecorder) MarkAccountPrimary(ctx, accountID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) MarkAccountPrimary(ctx, accountID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MarkAccountPrimary", reflect.TypeOf((*MockStore)(nil).MarkAccountPrimary), ctx, accountID) } @@ -2445,7 +2495,7 @@ func (m *MockStore) MarkAllPendingJobsAsFailed(ctx context.Context, accountID, p } // MarkAllPendingJobsAsFailed indicates an expected call of MarkAllPendingJobsAsFailed. -func (mr *MockStoreMockRecorder) MarkAllPendingJobsAsFailed(ctx, accountID, peerID, reason interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) MarkAllPendingJobsAsFailed(ctx, accountID, peerID, reason any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MarkAllPendingJobsAsFailed", reflect.TypeOf((*MockStore)(nil).MarkAllPendingJobsAsFailed), ctx, accountID, peerID, reason) } @@ -2459,7 +2509,7 @@ func (m *MockStore) MarkPATUsed(ctx context.Context, patID string) error { } // MarkPATUsed indicates an expected call of MarkPATUsed. -func (mr *MockStoreMockRecorder) MarkPATUsed(ctx, patID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) MarkPATUsed(ctx, patID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MarkPATUsed", reflect.TypeOf((*MockStore)(nil).MarkPATUsed), ctx, patID) } @@ -2473,7 +2523,7 @@ func (m *MockStore) MarkPendingJobsAsFailed(ctx context.Context, accountID, peer } // MarkPendingJobsAsFailed indicates an expected call of MarkPendingJobsAsFailed. -func (mr *MockStoreMockRecorder) MarkPendingJobsAsFailed(ctx, accountID, peerID, jobID, reason interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) MarkPendingJobsAsFailed(ctx, accountID, peerID, jobID, reason any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MarkPendingJobsAsFailed", reflect.TypeOf((*MockStore)(nil).MarkPendingJobsAsFailed), ctx, accountID, peerID, jobID, reason) } @@ -2487,7 +2537,7 @@ func (m *MockStore) MarkProxyAccessTokenUsed(ctx context.Context, tokenID string } // MarkProxyAccessTokenUsed indicates an expected call of MarkProxyAccessTokenUsed. -func (mr *MockStoreMockRecorder) MarkProxyAccessTokenUsed(ctx, tokenID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) MarkProxyAccessTokenUsed(ctx, tokenID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MarkProxyAccessTokenUsed", reflect.TypeOf((*MockStore)(nil).MarkProxyAccessTokenUsed), ctx, tokenID) } @@ -2501,7 +2551,7 @@ func (m *MockStore) RemovePeerFromAllGroups(ctx context.Context, peerID string) } // RemovePeerFromAllGroups indicates an expected call of RemovePeerFromAllGroups. -func (mr *MockStoreMockRecorder) RemovePeerFromAllGroups(ctx, peerID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) RemovePeerFromAllGroups(ctx, peerID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemovePeerFromAllGroups", reflect.TypeOf((*MockStore)(nil).RemovePeerFromAllGroups), ctx, peerID) } @@ -2515,7 +2565,7 @@ func (m *MockStore) RemovePeerFromGroup(ctx context.Context, peerID, groupID str } // RemovePeerFromGroup indicates an expected call of RemovePeerFromGroup. -func (mr *MockStoreMockRecorder) RemovePeerFromGroup(ctx, peerID, groupID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) RemovePeerFromGroup(ctx, peerID, groupID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemovePeerFromGroup", reflect.TypeOf((*MockStore)(nil).RemovePeerFromGroup), ctx, peerID, groupID) } @@ -2529,7 +2579,7 @@ func (m *MockStore) RemoveResourceFromGroup(ctx context.Context, accountId, grou } // RemoveResourceFromGroup indicates an expected call of RemoveResourceFromGroup. -func (mr *MockStoreMockRecorder) RemoveResourceFromGroup(ctx, accountId, groupID, resourceID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) RemoveResourceFromGroup(ctx, accountId, groupID, resourceID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveResourceFromGroup", reflect.TypeOf((*MockStore)(nil).RemoveResourceFromGroup), ctx, accountId, groupID, resourceID) } @@ -2543,7 +2593,7 @@ func (m *MockStore) RenewEphemeralService(ctx context.Context, accountID, peerID } // RenewEphemeralService indicates an expected call of RenewEphemeralService. -func (mr *MockStoreMockRecorder) RenewEphemeralService(ctx, accountID, peerID, serviceID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) RenewEphemeralService(ctx, accountID, peerID, serviceID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RenewEphemeralService", reflect.TypeOf((*MockStore)(nil).RenewEphemeralService), ctx, accountID, peerID, serviceID) } @@ -2557,7 +2607,7 @@ func (m *MockStore) RevokeProxyAccessToken(ctx context.Context, tokenID string) } // RevokeProxyAccessToken indicates an expected call of RevokeProxyAccessToken. -func (mr *MockStoreMockRecorder) RevokeProxyAccessToken(ctx, tokenID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) RevokeProxyAccessToken(ctx, tokenID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevokeProxyAccessToken", reflect.TypeOf((*MockStore)(nil).RevokeProxyAccessToken), ctx, tokenID) } @@ -2571,7 +2621,7 @@ func (m *MockStore) SaveAccount(ctx context.Context, account *types2.Account) er } // SaveAccount indicates an expected call of SaveAccount. -func (mr *MockStoreMockRecorder) SaveAccount(ctx, account interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) SaveAccount(ctx, account any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveAccount", reflect.TypeOf((*MockStore)(nil).SaveAccount), ctx, account) } @@ -2585,7 +2635,7 @@ func (m *MockStore) SaveAccountOnboarding(ctx context.Context, onboarding *types } // SaveAccountOnboarding indicates an expected call of SaveAccountOnboarding. -func (mr *MockStoreMockRecorder) SaveAccountOnboarding(ctx, onboarding interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) SaveAccountOnboarding(ctx, onboarding any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveAccountOnboarding", reflect.TypeOf((*MockStore)(nil).SaveAccountOnboarding), ctx, onboarding) } @@ -2599,7 +2649,7 @@ func (m *MockStore) SaveAccountSettings(ctx context.Context, accountID string, s } // SaveAccountSettings indicates an expected call of SaveAccountSettings. -func (mr *MockStoreMockRecorder) SaveAccountSettings(ctx, accountID, settings interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) SaveAccountSettings(ctx, accountID, settings any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveAccountSettings", reflect.TypeOf((*MockStore)(nil).SaveAccountSettings), ctx, accountID, settings) } @@ -2613,11 +2663,25 @@ func (m *MockStore) SaveDNSSettings(ctx context.Context, accountID string, setti } // SaveDNSSettings indicates an expected call of SaveDNSSettings. -func (mr *MockStoreMockRecorder) SaveDNSSettings(ctx, accountID, settings interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) SaveDNSSettings(ctx, accountID, settings any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveDNSSettings", reflect.TypeOf((*MockStore)(nil).SaveDNSSettings), ctx, accountID, settings) } +// SaveInspectionPolicy mocks base method. +func (m *MockStore) SaveInspectionPolicy(ctx context.Context, lockStrength LockingStrength, policy *types2.InspectionPolicy) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SaveInspectionPolicy", ctx, lockStrength, policy) + ret0, _ := ret[0].(error) + return ret0 +} + +// SaveInspectionPolicy indicates an expected call of SaveInspectionPolicy. +func (mr *MockStoreMockRecorder) SaveInspectionPolicy(ctx, lockStrength, policy any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveInspectionPolicy", reflect.TypeOf((*MockStore)(nil).SaveInspectionPolicy), ctx, lockStrength, policy) +} + // SaveInstallationID mocks base method. func (m *MockStore) SaveInstallationID(ctx context.Context, ID string) error { m.ctrl.T.Helper() @@ -2627,7 +2691,7 @@ func (m *MockStore) SaveInstallationID(ctx context.Context, ID string) error { } // SaveInstallationID indicates an expected call of SaveInstallationID. -func (mr *MockStoreMockRecorder) SaveInstallationID(ctx, ID interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) SaveInstallationID(ctx, ID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveInstallationID", reflect.TypeOf((*MockStore)(nil).SaveInstallationID), ctx, ID) } @@ -2641,7 +2705,7 @@ func (m *MockStore) SaveNameServerGroup(ctx context.Context, nameServerGroup *dn } // SaveNameServerGroup indicates an expected call of SaveNameServerGroup. -func (mr *MockStoreMockRecorder) SaveNameServerGroup(ctx, nameServerGroup interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) SaveNameServerGroup(ctx, nameServerGroup any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveNameServerGroup", reflect.TypeOf((*MockStore)(nil).SaveNameServerGroup), ctx, nameServerGroup) } @@ -2655,7 +2719,7 @@ func (m *MockStore) SaveNetwork(ctx context.Context, network *types1.Network) er } // SaveNetwork indicates an expected call of SaveNetwork. -func (mr *MockStoreMockRecorder) SaveNetwork(ctx, network interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) SaveNetwork(ctx, network any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveNetwork", reflect.TypeOf((*MockStore)(nil).SaveNetwork), ctx, network) } @@ -2669,7 +2733,7 @@ func (m *MockStore) SaveNetworkResource(ctx context.Context, resource *types.Net } // SaveNetworkResource indicates an expected call of SaveNetworkResource. -func (mr *MockStoreMockRecorder) SaveNetworkResource(ctx, resource interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) SaveNetworkResource(ctx, resource any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveNetworkResource", reflect.TypeOf((*MockStore)(nil).SaveNetworkResource), ctx, resource) } @@ -2683,7 +2747,7 @@ func (m *MockStore) SaveNetworkRouter(ctx context.Context, router *types0.Networ } // SaveNetworkRouter indicates an expected call of SaveNetworkRouter. -func (mr *MockStoreMockRecorder) SaveNetworkRouter(ctx, router interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) SaveNetworkRouter(ctx, router any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveNetworkRouter", reflect.TypeOf((*MockStore)(nil).SaveNetworkRouter), ctx, router) } @@ -2697,37 +2761,37 @@ func (m *MockStore) SavePAT(ctx context.Context, pat *types2.PersonalAccessToken } // SavePAT indicates an expected call of SavePAT. -func (mr *MockStoreMockRecorder) SavePAT(ctx, pat interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) SavePAT(ctx, pat any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SavePAT", reflect.TypeOf((*MockStore)(nil).SavePAT), ctx, pat) } // SavePeer mocks base method. -func (m *MockStore) SavePeer(ctx context.Context, accountID string, peer *peer.Peer) error { +func (m *MockStore) SavePeer(ctx context.Context, accountID string, arg2 *peer.Peer) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SavePeer", ctx, accountID, peer) + ret := m.ctrl.Call(m, "SavePeer", ctx, accountID, arg2) ret0, _ := ret[0].(error) return ret0 } // SavePeer indicates an expected call of SavePeer. -func (mr *MockStoreMockRecorder) SavePeer(ctx, accountID, peer interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) SavePeer(ctx, accountID, arg2 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SavePeer", reflect.TypeOf((*MockStore)(nil).SavePeer), ctx, accountID, peer) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SavePeer", reflect.TypeOf((*MockStore)(nil).SavePeer), ctx, accountID, arg2) } // SavePeerLocation mocks base method. -func (m *MockStore) SavePeerLocation(ctx context.Context, accountID string, peer *peer.Peer) error { +func (m *MockStore) SavePeerLocation(ctx context.Context, accountID string, arg2 *peer.Peer) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SavePeerLocation", ctx, accountID, peer) + ret := m.ctrl.Call(m, "SavePeerLocation", ctx, accountID, arg2) ret0, _ := ret[0].(error) return ret0 } // SavePeerLocation indicates an expected call of SavePeerLocation. -func (mr *MockStoreMockRecorder) SavePeerLocation(ctx, accountID, peer interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) SavePeerLocation(ctx, accountID, arg2 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SavePeerLocation", reflect.TypeOf((*MockStore)(nil).SavePeerLocation), ctx, accountID, peer) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SavePeerLocation", reflect.TypeOf((*MockStore)(nil).SavePeerLocation), ctx, accountID, arg2) } // SavePeerStatus mocks base method. @@ -2739,7 +2803,7 @@ func (m *MockStore) SavePeerStatus(ctx context.Context, accountID, peerID string } // SavePeerStatus indicates an expected call of SavePeerStatus. -func (mr *MockStoreMockRecorder) SavePeerStatus(ctx, accountID, peerID, status interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) SavePeerStatus(ctx, accountID, peerID, status any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SavePeerStatus", reflect.TypeOf((*MockStore)(nil).SavePeerStatus), ctx, accountID, peerID, status) } @@ -2753,7 +2817,7 @@ func (m *MockStore) SavePolicy(ctx context.Context, policy *types2.Policy) error } // SavePolicy indicates an expected call of SavePolicy. -func (mr *MockStoreMockRecorder) SavePolicy(ctx, policy interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) SavePolicy(ctx, policy any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SavePolicy", reflect.TypeOf((*MockStore)(nil).SavePolicy), ctx, policy) } @@ -2767,23 +2831,23 @@ func (m *MockStore) SavePostureChecks(ctx context.Context, postureCheck *posture } // SavePostureChecks indicates an expected call of SavePostureChecks. -func (mr *MockStoreMockRecorder) SavePostureChecks(ctx, postureCheck interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) SavePostureChecks(ctx, postureCheck any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SavePostureChecks", reflect.TypeOf((*MockStore)(nil).SavePostureChecks), ctx, postureCheck) } // SaveProxy mocks base method. -func (m *MockStore) SaveProxy(ctx context.Context, proxy *proxy.Proxy) error { +func (m *MockStore) SaveProxy(ctx context.Context, arg1 *proxy.Proxy) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SaveProxy", ctx, proxy) + ret := m.ctrl.Call(m, "SaveProxy", ctx, arg1) ret0, _ := ret[0].(error) return ret0 } // SaveProxy indicates an expected call of SaveProxy. -func (mr *MockStoreMockRecorder) SaveProxy(ctx, proxy interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) SaveProxy(ctx, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveProxy", reflect.TypeOf((*MockStore)(nil).SaveProxy), ctx, proxy) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveProxy", reflect.TypeOf((*MockStore)(nil).SaveProxy), ctx, arg1) } // SaveProxyAccessToken mocks base method. @@ -2795,23 +2859,23 @@ func (m *MockStore) SaveProxyAccessToken(ctx context.Context, token *types2.Prox } // SaveProxyAccessToken indicates an expected call of SaveProxyAccessToken. -func (mr *MockStoreMockRecorder) SaveProxyAccessToken(ctx, token interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) SaveProxyAccessToken(ctx, token any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveProxyAccessToken", reflect.TypeOf((*MockStore)(nil).SaveProxyAccessToken), ctx, token) } // SaveRoute mocks base method. -func (m *MockStore) SaveRoute(ctx context.Context, route *route.Route) error { +func (m *MockStore) SaveRoute(ctx context.Context, arg1 *route.Route) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SaveRoute", ctx, route) + ret := m.ctrl.Call(m, "SaveRoute", ctx, arg1) ret0, _ := ret[0].(error) return ret0 } // SaveRoute indicates an expected call of SaveRoute. -func (mr *MockStoreMockRecorder) SaveRoute(ctx, route interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) SaveRoute(ctx, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveRoute", reflect.TypeOf((*MockStore)(nil).SaveRoute), ctx, route) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveRoute", reflect.TypeOf((*MockStore)(nil).SaveRoute), ctx, arg1) } // SaveSetupKey mocks base method. @@ -2823,7 +2887,7 @@ func (m *MockStore) SaveSetupKey(ctx context.Context, setupKey *types2.SetupKey) } // SaveSetupKey indicates an expected call of SaveSetupKey. -func (mr *MockStoreMockRecorder) SaveSetupKey(ctx, setupKey interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) SaveSetupKey(ctx, setupKey any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveSetupKey", reflect.TypeOf((*MockStore)(nil).SaveSetupKey), ctx, setupKey) } @@ -2837,7 +2901,7 @@ func (m *MockStore) SaveUser(ctx context.Context, user *types2.User) error { } // SaveUser indicates an expected call of SaveUser. -func (mr *MockStoreMockRecorder) SaveUser(ctx, user interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) SaveUser(ctx, user any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveUser", reflect.TypeOf((*MockStore)(nil).SaveUser), ctx, user) } @@ -2851,7 +2915,7 @@ func (m *MockStore) SaveUserInvite(ctx context.Context, invite *types2.UserInvit } // SaveUserInvite indicates an expected call of SaveUserInvite. -func (mr *MockStoreMockRecorder) SaveUserInvite(ctx, invite interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) SaveUserInvite(ctx, invite any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveUserInvite", reflect.TypeOf((*MockStore)(nil).SaveUserInvite), ctx, invite) } @@ -2865,7 +2929,7 @@ func (m *MockStore) SaveUserLastLogin(ctx context.Context, accountID, userID str } // SaveUserLastLogin indicates an expected call of SaveUserLastLogin. -func (mr *MockStoreMockRecorder) SaveUserLastLogin(ctx, accountID, userID, lastLogin interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) SaveUserLastLogin(ctx, accountID, userID, lastLogin any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveUserLastLogin", reflect.TypeOf((*MockStore)(nil).SaveUserLastLogin), ctx, accountID, userID, lastLogin) } @@ -2879,7 +2943,7 @@ func (m *MockStore) SaveUsers(ctx context.Context, users []*types2.User) error { } // SaveUsers indicates an expected call of SaveUsers. -func (mr *MockStoreMockRecorder) SaveUsers(ctx, users interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) SaveUsers(ctx, users any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveUsers", reflect.TypeOf((*MockStore)(nil).SaveUsers), ctx, users) } @@ -2891,23 +2955,23 @@ func (m *MockStore) SetFieldEncrypt(enc *crypt.FieldEncrypt) { } // SetFieldEncrypt indicates an expected call of SetFieldEncrypt. -func (mr *MockStoreMockRecorder) SetFieldEncrypt(enc interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) SetFieldEncrypt(enc any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetFieldEncrypt", reflect.TypeOf((*MockStore)(nil).SetFieldEncrypt), enc) } // UpdateAccountDomainAttributes mocks base method. -func (m *MockStore) UpdateAccountDomainAttributes(ctx context.Context, accountID, domain, category string, isPrimaryDomain bool) error { +func (m *MockStore) UpdateAccountDomainAttributes(ctx context.Context, accountID, arg2, category string, isPrimaryDomain bool) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateAccountDomainAttributes", ctx, accountID, domain, category, isPrimaryDomain) + ret := m.ctrl.Call(m, "UpdateAccountDomainAttributes", ctx, accountID, arg2, category, isPrimaryDomain) ret0, _ := ret[0].(error) return ret0 } // UpdateAccountDomainAttributes indicates an expected call of UpdateAccountDomainAttributes. -func (mr *MockStoreMockRecorder) UpdateAccountDomainAttributes(ctx, accountID, domain, category, isPrimaryDomain interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateAccountDomainAttributes(ctx, accountID, arg2, category, isPrimaryDomain any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAccountDomainAttributes", reflect.TypeOf((*MockStore)(nil).UpdateAccountDomainAttributes), ctx, accountID, domain, category, isPrimaryDomain) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAccountDomainAttributes", reflect.TypeOf((*MockStore)(nil).UpdateAccountDomainAttributes), ctx, accountID, arg2, category, isPrimaryDomain) } // UpdateAccountNetwork mocks base method. @@ -2919,7 +2983,7 @@ func (m *MockStore) UpdateAccountNetwork(ctx context.Context, accountID string, } // UpdateAccountNetwork indicates an expected call of UpdateAccountNetwork. -func (mr *MockStoreMockRecorder) UpdateAccountNetwork(ctx, accountID, ipNet interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateAccountNetwork(ctx, accountID, ipNet any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAccountNetwork", reflect.TypeOf((*MockStore)(nil).UpdateAccountNetwork), ctx, accountID, ipNet) } @@ -2934,7 +2998,7 @@ func (m *MockStore) UpdateCustomDomain(ctx context.Context, accountID string, d } // UpdateCustomDomain indicates an expected call of UpdateCustomDomain. -func (mr *MockStoreMockRecorder) UpdateCustomDomain(ctx, accountID, d interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateCustomDomain(ctx, accountID, d any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateCustomDomain", reflect.TypeOf((*MockStore)(nil).UpdateCustomDomain), ctx, accountID, d) } @@ -2948,7 +3012,7 @@ func (m *MockStore) UpdateDNSRecord(ctx context.Context, record *records.Record) } // UpdateDNSRecord indicates an expected call of UpdateDNSRecord. -func (mr *MockStoreMockRecorder) UpdateDNSRecord(ctx, record interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateDNSRecord(ctx, record any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDNSRecord", reflect.TypeOf((*MockStore)(nil).UpdateDNSRecord), ctx, record) } @@ -2962,7 +3026,7 @@ func (m *MockStore) UpdateGroup(ctx context.Context, group *types2.Group) error } // UpdateGroup indicates an expected call of UpdateGroup. -func (mr *MockStoreMockRecorder) UpdateGroup(ctx, group interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateGroup(ctx, group any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateGroup", reflect.TypeOf((*MockStore)(nil).UpdateGroup), ctx, group) } @@ -2976,7 +3040,7 @@ func (m *MockStore) UpdateGroups(ctx context.Context, accountID string, groups [ } // UpdateGroups indicates an expected call of UpdateGroups. -func (mr *MockStoreMockRecorder) UpdateGroups(ctx, accountID, groups interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateGroups(ctx, accountID, groups any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateGroups", reflect.TypeOf((*MockStore)(nil).UpdateGroups), ctx, accountID, groups) } @@ -2990,23 +3054,23 @@ func (m *MockStore) UpdateProxyHeartbeat(ctx context.Context, proxyID, clusterAd } // UpdateProxyHeartbeat indicates an expected call of UpdateProxyHeartbeat. -func (mr *MockStoreMockRecorder) UpdateProxyHeartbeat(ctx, proxyID, clusterAddress, ipAddress interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateProxyHeartbeat(ctx, proxyID, clusterAddress, ipAddress any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateProxyHeartbeat", reflect.TypeOf((*MockStore)(nil).UpdateProxyHeartbeat), ctx, proxyID, clusterAddress, ipAddress) } // UpdateService mocks base method. -func (m *MockStore) UpdateService(ctx context.Context, service *service.Service) error { +func (m *MockStore) UpdateService(ctx context.Context, arg1 *service.Service) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateService", ctx, service) + ret := m.ctrl.Call(m, "UpdateService", ctx, arg1) ret0, _ := ret[0].(error) return ret0 } // UpdateService indicates an expected call of UpdateService. -func (mr *MockStoreMockRecorder) UpdateService(ctx, service interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateService(ctx, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateService", reflect.TypeOf((*MockStore)(nil).UpdateService), ctx, service) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateService", reflect.TypeOf((*MockStore)(nil).UpdateService), ctx, arg1) } // UpdateZone mocks base method. @@ -3018,7 +3082,7 @@ func (m *MockStore) UpdateZone(ctx context.Context, zone *zones.Zone) error { } // UpdateZone indicates an expected call of UpdateZone. -func (mr *MockStoreMockRecorder) UpdateZone(ctx, zone interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateZone(ctx, zone any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateZone", reflect.TypeOf((*MockStore)(nil).UpdateZone), ctx, zone) } diff --git a/management/server/types/account.go b/management/server/types/account.go index c448813db..f7afb7945 100644 --- a/management/server/types/account.go +++ b/management/server/types/account.go @@ -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 diff --git a/management/server/types/account_components.go b/management/server/types/account_components.go index bd4244546..8f6f33b39 100644 --- a/management/server/types/account_components.go +++ b/management/server/types/account_components.go @@ -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{ diff --git a/management/server/types/inspection_policy.go b/management/server/types/inspection_policy.go new file mode 100644 index 000000000..f79011df4 --- /dev/null +++ b/management/server/types/inspection_policy.go @@ -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 +} diff --git a/management/server/types/network.go b/management/server/types/network.go index 0d13de10f..19fdafb7d 100644 --- a/management/server/types/network.go +++ b/management/server/types/network.go @@ -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) { diff --git a/management/server/types/networkmap_components.go b/management/server/types/networkmap_components.go index 23d84a994..6108745c2 100644 --- a/management/server/types/networkmap_components.go +++ b/management/server/types/networkmap_components.go @@ -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 + } +} diff --git a/management/server/types/policy.go b/management/server/types/policy.go index d4e1a8816..1b13a7537 100644 --- a/management/server/types/policy.go +++ b/management/server/types/policy.go @@ -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 } diff --git a/management/server/types/proxy_routes.go b/management/server/types/proxy_routes.go new file mode 100644 index 000000000..d67b8ed0d --- /dev/null +++ b/management/server/types/proxy_routes.go @@ -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 +} diff --git a/management/server/types/proxy_routes_test.go b/management/server/types/proxy_routes_test.go new file mode 100644 index 000000000..66885c4ad --- /dev/null +++ b/management/server/types/proxy_routes_test.go @@ -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) +} diff --git a/management/server/types/transparent_proxy.go b/management/server/types/transparent_proxy.go new file mode 100644 index 000000000..85bab1ac5 --- /dev/null +++ b/management/server/types/transparent_proxy.go @@ -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 + } +} diff --git a/shared/management/http/api/openapi.yml b/shared/management/http/api/openapi.yml index 766fdf0de..8f3fb65db 100644 --- a/shared/management/http/api/openapi.yml +++ b/shared/management/http/api/openapi.yml @@ -1533,6 +1533,12 @@ components: items: type: string example: "chacdk86lnnboviihd70" + inspection_policies: + description: Inspection policy IDs applied to traffic matching this policy. When set, traffic is routed through a transparent proxy on the destination network's routing peers. + type: array + items: + type: string + example: "chacdk86lnnboviihd71" rules: description: Policy rule object for policy UI editor type: array @@ -1551,6 +1557,12 @@ components: items: type: string example: "chacdk86lnnboviihd70" + inspection_policies: + description: Inspection policy IDs applied to traffic matching this policy + type: array + items: + type: string + example: "chacdk86lnnboviihd71" rules: description: Policy rule object for policy UI editor type: array @@ -1573,6 +1585,12 @@ components: items: type: string example: "chacdk86lnnboviihd70" + inspection_policies: + description: Inspection policy IDs applied to traffic matching this policy + type: array + items: + type: string + example: "chacdk86lnnboviihd71" rules: description: Policy rule object for policy UI editor type: array @@ -2050,6 +2068,9 @@ components: description: Network router status type: boolean example: true + inspection: + description: Optional traffic inspection configuration. When enabled, traffic through this routing peer is transparently proxied and inspected. + $ref: '#/components/schemas/RouterInspectionConfig' required: # Only one property has to be set #- peer @@ -2057,6 +2078,174 @@ components: - metric - masquerade - enabled + RouterInspectionConfig: + type: object + properties: + enabled: + description: Whether traffic inspection is active on this routing peer + type: boolean + example: false + mode: + description: Inspection mode + type: string + enum: [ "builtin", "envoy", "external" ] + example: builtin + external_url: + description: External proxy URL (http:// or socks5://) when mode is external + type: string + example: "http://proxy.corp:8080" + default_action: + description: Action when no inspection rule matches + type: string + enum: [ "allow", "block", "inspect" ] + example: allow + redirect_ports: + description: Destination ports to intercept. Empty means all ports. + type: array + items: + type: integer + example: [80, 443] + icap: + description: ICAP service configuration for external content scanning + $ref: '#/components/schemas/InspectionICAPConfig' + ca_cert_pem: + description: PEM-encoded CA certificate for MITM TLS inspection + type: string + ca_key_pem: + description: PEM-encoded CA private key for MITM TLS inspection + type: string + envoy_binary_path: + description: Path to envoy binary when mode is envoy. Empty searches $PATH. + type: string + envoy_admin_port: + description: Envoy admin API port for health checks. 0 picks a free port. + type: integer + required: + - enabled + InspectionPolicyMinimum: + type: object + properties: + name: + description: Human-readable name for this inspection policy + type: string + example: "Corporate web filtering" + description: + description: Description + type: string + enabled: + description: Whether this inspection policy is active + type: boolean + example: true + rules: + description: L7 inspection rules + type: array + items: + $ref: '#/components/schemas/InspectionPolicyRule' + mode: + description: Proxy operation mode + type: string + enum: [ "builtin", "envoy", "external" ] + example: "builtin" + external_url: + description: External proxy URL (HTTP CONNECT or SOCKS5) when mode is external + type: string + example: "socks5://proxy.corp.com:1080" + default_action: + description: Action for recognized traffic when no rule matches + type: string + enum: [ "allow", "block", "inspect" ] + example: "allow" + redirect_ports: + description: Destination ports to intercept at L4. Empty means all ports. + type: array + items: + type: integer + example: [80, 443] + ca_cert_pem: + description: PEM-encoded CA certificate for MITM TLS inspection + type: string + ca_key_pem: + description: PEM-encoded CA private key for MITM TLS inspection + type: string + envoy_binary_path: + description: Path to envoy binary when mode is envoy. Empty searches $PATH. + type: string + envoy_admin_port: + description: Envoy admin API port for health checks. 0 picks a free port. + type: integer + icap: + description: ICAP configuration for external content scanning + $ref: '#/components/schemas/InspectionICAPConfig' + required: + - name + - enabled + - rules + InspectionPolicy: + allOf: + - type: object + properties: + id: + description: Inspection Policy ID + type: string + readOnly: true + required: + - id + - $ref: '#/components/schemas/InspectionPolicyMinimum' + InspectionPolicyRule: + type: object + properties: + domains: + description: Domain patterns to match via SNI or Host header. Supports wildcards (*.example.com). + type: array + items: + type: string + example: ["*.gambling.com", "*.betting.com"] + networks: + description: Destination CIDRs for optional L7 destination filtering + type: array + items: + type: string + example: ["10.0.0.0/8"] + protocols: + description: Protocols this rule applies to. Empty means all. + type: array + items: + type: string + enum: [ "http", "https", "h2", "h3", "websocket", "other" ] + example: ["https", "h2"] + paths: + description: URL path patterns. Exact ("/login"), prefix ("/api/*"), contains ("*/admin/*"). HTTPS requires inspect (MITM). Empty means all paths. + type: array + items: + type: string + example: ["/admin/*", "/api/internal/*"] + action: + description: What to do with matched connections + type: string + enum: [ "allow", "block", "inspect" ] + example: block + priority: + description: Evaluation order. Lower values are evaluated first. + type: integer + example: 1 + required: + - action + - priority + InspectionICAPConfig: + type: object + properties: + reqmod_url: + description: ICAP REQMOD service URL + type: string + example: "icap://icap-server:1344/reqmod" + respmod_url: + description: ICAP RESPMOD service URL + type: string + example: "icap://icap-server:1344/respmod" + max_connections: + description: Maximum ICAP connection pool size + type: integer + example: 8 NetworkRouter: allOf: - type: object @@ -7410,6 +7599,144 @@ paths: "$ref": "#/components/responses/forbidden" '500': "$ref": "#/components/responses/internal_error" + /api/inspection-policies: + get: + summary: List all Inspection Policies + description: Returns a list of all reusable inspection policy rule sets + tags: [ Inspection Policies ] + security: + - BearerAuth: [ ] + - TokenAuth: [ ] + responses: + '200': + description: A JSON Array of Inspection Policies + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/InspectionPolicy' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" + post: + summary: Create an Inspection Policy + description: Creates a reusable inspection policy rule set + tags: [ Inspection Policies ] + security: + - BearerAuth: [ ] + - TokenAuth: [ ] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/InspectionPolicyMinimum' + responses: + '200': + description: An Inspection Policy object + content: + application/json: + schema: + $ref: '#/components/schemas/InspectionPolicy' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" + /api/inspection-policies/{policyId}: + get: + summary: Get an Inspection Policy + description: Returns an inspection policy rule set + tags: [ Inspection Policies ] + security: + - BearerAuth: [ ] + - TokenAuth: [ ] + parameters: + - in: path + name: policyId + required: true + schema: + type: string + responses: + '200': + description: An Inspection Policy object + content: + application/json: + schema: + $ref: '#/components/schemas/InspectionPolicy' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" + put: + summary: Update an Inspection Policy + description: Updates an inspection policy rule set + tags: [ Inspection Policies ] + security: + - BearerAuth: [ ] + - TokenAuth: [ ] + parameters: + - in: path + name: policyId + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/InspectionPolicyMinimum' + responses: + '200': + description: An Inspection Policy object + content: + application/json: + schema: + $ref: '#/components/schemas/InspectionPolicy' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" + delete: + summary: Delete an Inspection Policy + description: Deletes an inspection policy rule set + tags: [ Inspection Policies ] + security: + - BearerAuth: [ ] + - TokenAuth: [ ] + parameters: + - in: path + name: policyId + required: true + schema: + type: string + responses: + '200': + description: Successfully deleted + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" /api/dns/nameservers: get: summary: List all Nameserver Groups diff --git a/shared/management/http/api/types.gen.go b/shared/management/http/api/types.gen.go index 14bb6ee03..07825b68f 100644 --- a/shared/management/http/api/types.gen.go +++ b/shared/management/http/api/types.gen.go @@ -584,6 +584,141 @@ func (e IngressPortAllocationRequestPortRangeProtocol) Valid() bool { } } +// Defines values for InspectionPolicyDefaultAction. +const ( + InspectionPolicyDefaultActionAllow InspectionPolicyDefaultAction = "allow" + InspectionPolicyDefaultActionBlock InspectionPolicyDefaultAction = "block" + InspectionPolicyDefaultActionInspect InspectionPolicyDefaultAction = "inspect" +) + +// Valid indicates whether the value is a known member of the InspectionPolicyDefaultAction enum. +func (e InspectionPolicyDefaultAction) Valid() bool { + switch e { + case InspectionPolicyDefaultActionAllow: + return true + case InspectionPolicyDefaultActionBlock: + return true + case InspectionPolicyDefaultActionInspect: + return true + default: + return false + } +} + +// Defines values for InspectionPolicyMode. +const ( + InspectionPolicyModeBuiltin InspectionPolicyMode = "builtin" + InspectionPolicyModeEnvoy InspectionPolicyMode = "envoy" + InspectionPolicyModeExternal InspectionPolicyMode = "external" +) + +// Valid indicates whether the value is a known member of the InspectionPolicyMode enum. +func (e InspectionPolicyMode) Valid() bool { + switch e { + case InspectionPolicyModeBuiltin: + return true + case InspectionPolicyModeEnvoy: + return true + case InspectionPolicyModeExternal: + return true + default: + return false + } +} + +// Defines values for InspectionPolicyMinimumDefaultAction. +const ( + InspectionPolicyMinimumDefaultActionAllow InspectionPolicyMinimumDefaultAction = "allow" + InspectionPolicyMinimumDefaultActionBlock InspectionPolicyMinimumDefaultAction = "block" + InspectionPolicyMinimumDefaultActionInspect InspectionPolicyMinimumDefaultAction = "inspect" +) + +// Valid indicates whether the value is a known member of the InspectionPolicyMinimumDefaultAction enum. +func (e InspectionPolicyMinimumDefaultAction) Valid() bool { + switch e { + case InspectionPolicyMinimumDefaultActionAllow: + return true + case InspectionPolicyMinimumDefaultActionBlock: + return true + case InspectionPolicyMinimumDefaultActionInspect: + return true + default: + return false + } +} + +// Defines values for InspectionPolicyMinimumMode. +const ( + InspectionPolicyMinimumModeBuiltin InspectionPolicyMinimumMode = "builtin" + InspectionPolicyMinimumModeEnvoy InspectionPolicyMinimumMode = "envoy" + InspectionPolicyMinimumModeExternal InspectionPolicyMinimumMode = "external" +) + +// Valid indicates whether the value is a known member of the InspectionPolicyMinimumMode enum. +func (e InspectionPolicyMinimumMode) Valid() bool { + switch e { + case InspectionPolicyMinimumModeBuiltin: + return true + case InspectionPolicyMinimumModeEnvoy: + return true + case InspectionPolicyMinimumModeExternal: + return true + default: + return false + } +} + +// Defines values for InspectionPolicyRuleAction. +const ( + InspectionPolicyRuleActionAllow InspectionPolicyRuleAction = "allow" + InspectionPolicyRuleActionBlock InspectionPolicyRuleAction = "block" + InspectionPolicyRuleActionInspect InspectionPolicyRuleAction = "inspect" +) + +// Valid indicates whether the value is a known member of the InspectionPolicyRuleAction enum. +func (e InspectionPolicyRuleAction) Valid() bool { + switch e { + case InspectionPolicyRuleActionAllow: + return true + case InspectionPolicyRuleActionBlock: + return true + case InspectionPolicyRuleActionInspect: + return true + default: + return false + } +} + +// Defines values for InspectionPolicyRuleProtocols. +const ( + InspectionPolicyRuleProtocolsH2 InspectionPolicyRuleProtocols = "h2" + InspectionPolicyRuleProtocolsH3 InspectionPolicyRuleProtocols = "h3" + InspectionPolicyRuleProtocolsHttp InspectionPolicyRuleProtocols = "http" + InspectionPolicyRuleProtocolsHttps InspectionPolicyRuleProtocols = "https" + InspectionPolicyRuleProtocolsOther InspectionPolicyRuleProtocols = "other" + InspectionPolicyRuleProtocolsWebsocket InspectionPolicyRuleProtocols = "websocket" +) + +// Valid indicates whether the value is a known member of the InspectionPolicyRuleProtocols enum. +func (e InspectionPolicyRuleProtocols) Valid() bool { + switch e { + case InspectionPolicyRuleProtocolsH2: + return true + case InspectionPolicyRuleProtocolsH3: + return true + case InspectionPolicyRuleProtocolsHttp: + return true + case InspectionPolicyRuleProtocolsHttps: + return true + case InspectionPolicyRuleProtocolsOther: + return true + case InspectionPolicyRuleProtocolsWebsocket: + return true + default: + return false + } +} + // Defines values for IntegrationResponsePlatform. const ( IntegrationResponsePlatformDatadog IntegrationResponsePlatform = "datadog" @@ -896,6 +1031,48 @@ func (e ReverseProxyDomainType) Valid() bool { } } +// Defines values for RouterInspectionConfigDefaultAction. +const ( + RouterInspectionConfigDefaultActionAllow RouterInspectionConfigDefaultAction = "allow" + RouterInspectionConfigDefaultActionBlock RouterInspectionConfigDefaultAction = "block" + RouterInspectionConfigDefaultActionInspect RouterInspectionConfigDefaultAction = "inspect" +) + +// Valid indicates whether the value is a known member of the RouterInspectionConfigDefaultAction enum. +func (e RouterInspectionConfigDefaultAction) Valid() bool { + switch e { + case RouterInspectionConfigDefaultActionAllow: + return true + case RouterInspectionConfigDefaultActionBlock: + return true + case RouterInspectionConfigDefaultActionInspect: + return true + default: + return false + } +} + +// Defines values for RouterInspectionConfigMode. +const ( + RouterInspectionConfigModeBuiltin RouterInspectionConfigMode = "builtin" + RouterInspectionConfigModeEnvoy RouterInspectionConfigMode = "envoy" + RouterInspectionConfigModeExternal RouterInspectionConfigMode = "external" +) + +// Valid indicates whether the value is a known member of the RouterInspectionConfigMode enum. +func (e RouterInspectionConfigMode) Valid() bool { + switch e { + case RouterInspectionConfigModeBuiltin: + return true + case RouterInspectionConfigModeEnvoy: + return true + case RouterInspectionConfigModeExternal: + return true + default: + return false + } +} + // Defines values for SentinelOneMatchAttributesNetworkStatus. const ( SentinelOneMatchAttributesNetworkStatusConnected SentinelOneMatchAttributesNetworkStatus = "connected" @@ -2464,6 +2641,140 @@ type IngressPortAllocationRequestPortRange struct { // IngressPortAllocationRequestPortRangeProtocol The protocol accepted by the port range type IngressPortAllocationRequestPortRangeProtocol string +// InspectionICAPConfig defines model for InspectionICAPConfig. +type InspectionICAPConfig struct { + // MaxConnections Maximum ICAP connection pool size + MaxConnections *int `json:"max_connections,omitempty"` + + // ReqmodUrl ICAP REQMOD service URL + ReqmodUrl *string `json:"reqmod_url,omitempty"` + + // RespmodUrl ICAP RESPMOD service URL + RespmodUrl *string `json:"respmod_url,omitempty"` +} + +// InspectionPolicy defines model for InspectionPolicy. +type InspectionPolicy struct { + // CaCertPem PEM-encoded CA certificate for MITM TLS inspection + CaCertPem *string `json:"ca_cert_pem,omitempty"` + + // CaKeyPem PEM-encoded CA private key for MITM TLS inspection + CaKeyPem *string `json:"ca_key_pem,omitempty"` + + // DefaultAction Action for recognized traffic when no rule matches + DefaultAction *InspectionPolicyDefaultAction `json:"default_action,omitempty"` + + // Description Description + Description *string `json:"description,omitempty"` + + // Enabled Whether this inspection policy is active + Enabled bool `json:"enabled"` + + // EnvoyAdminPort Envoy admin API port for health checks. 0 picks a free port. + EnvoyAdminPort *int `json:"envoy_admin_port,omitempty"` + + // EnvoyBinaryPath Path to envoy binary when mode is envoy. Empty searches $PATH. + EnvoyBinaryPath *string `json:"envoy_binary_path,omitempty"` + + // ExternalUrl External proxy URL (HTTP CONNECT or SOCKS5) when mode is external + ExternalUrl *string `json:"external_url,omitempty"` + Icap *InspectionICAPConfig `json:"icap,omitempty"` + + // Id Inspection Policy ID + Id *string `json:"id,omitempty"` + + // Mode Proxy operation mode + Mode *InspectionPolicyMode `json:"mode,omitempty"` + + // Name Human-readable name for this inspection policy + Name string `json:"name"` + + // RedirectPorts Destination ports to intercept at L4. Empty means all ports. + RedirectPorts *[]int `json:"redirect_ports,omitempty"` + + // Rules L7 inspection rules + Rules []InspectionPolicyRule `json:"rules"` +} + +// InspectionPolicyDefaultAction Action for recognized traffic when no rule matches +type InspectionPolicyDefaultAction string + +// InspectionPolicyMode Proxy operation mode +type InspectionPolicyMode string + +// InspectionPolicyMinimum defines model for InspectionPolicyMinimum. +type InspectionPolicyMinimum struct { + // CaCertPem PEM-encoded CA certificate for MITM TLS inspection + CaCertPem *string `json:"ca_cert_pem,omitempty"` + + // CaKeyPem PEM-encoded CA private key for MITM TLS inspection + CaKeyPem *string `json:"ca_key_pem,omitempty"` + + // DefaultAction Action for recognized traffic when no rule matches + DefaultAction *InspectionPolicyMinimumDefaultAction `json:"default_action,omitempty"` + + // Description Description + Description *string `json:"description,omitempty"` + + // Enabled Whether this inspection policy is active + Enabled bool `json:"enabled"` + + // EnvoyAdminPort Envoy admin API port for health checks. 0 picks a free port. + EnvoyAdminPort *int `json:"envoy_admin_port,omitempty"` + + // EnvoyBinaryPath Path to envoy binary when mode is envoy. Empty searches $PATH. + EnvoyBinaryPath *string `json:"envoy_binary_path,omitempty"` + + // ExternalUrl External proxy URL (HTTP CONNECT or SOCKS5) when mode is external + ExternalUrl *string `json:"external_url,omitempty"` + Icap *InspectionICAPConfig `json:"icap,omitempty"` + + // Mode Proxy operation mode + Mode *InspectionPolicyMinimumMode `json:"mode,omitempty"` + + // Name Human-readable name for this inspection policy + Name string `json:"name"` + + // RedirectPorts Destination ports to intercept at L4. Empty means all ports. + RedirectPorts *[]int `json:"redirect_ports,omitempty"` + + // Rules L7 inspection rules + Rules []InspectionPolicyRule `json:"rules"` +} + +// InspectionPolicyMinimumDefaultAction Action for recognized traffic when no rule matches +type InspectionPolicyMinimumDefaultAction string + +// InspectionPolicyMinimumMode Proxy operation mode +type InspectionPolicyMinimumMode string + +// InspectionPolicyRule defines model for InspectionPolicyRule. +type InspectionPolicyRule struct { + // Action What to do with matched connections + Action InspectionPolicyRuleAction `json:"action"` + + // Domains Domain patterns to match via SNI or Host header. Supports wildcards (*.example.com). + Domains *[]string `json:"domains,omitempty"` + + // Networks Destination CIDRs for optional L7 destination filtering + Networks *[]string `json:"networks,omitempty"` + + // Paths URL path patterns. Exact ("/login"), prefix ("/api/*"), contains ("*/admin/*"). HTTPS requires inspect (MITM). Empty means all paths. + Paths *[]string `json:"paths,omitempty"` + + // Priority Evaluation order. Lower values are evaluated first. + Priority int `json:"priority"` + + // Protocols Protocols this rule applies to. Empty means all. + Protocols *[]InspectionPolicyRuleProtocols `json:"protocols,omitempty"` +} + +// InspectionPolicyRuleAction What to do with matched connections +type InspectionPolicyRuleAction string + +// InspectionPolicyRuleProtocols defines model for InspectionPolicyRule.Protocols. +type InspectionPolicyRuleProtocols string + // InstanceStatus Instance status information type InstanceStatus struct { // SetupRequired Indicates whether the instance requires initial setup @@ -2774,7 +3085,8 @@ type NetworkRouter struct { Enabled bool `json:"enabled"` // Id Network Router Id - Id string `json:"id"` + Id string `json:"id"` + Inspection *RouterInspectionConfig `json:"inspection,omitempty"` // Masquerade Indicate if peer should masquerade traffic to this route's prefix Masquerade bool `json:"masquerade"` @@ -2792,7 +3104,8 @@ type NetworkRouter struct { // NetworkRouterRequest defines model for NetworkRouterRequest. type NetworkRouterRequest struct { // Enabled Network router status - Enabled bool `json:"enabled"` + Enabled bool `json:"enabled"` + Inspection *RouterInspectionConfig `json:"inspection,omitempty"` // Masquerade Indicate if peer should masquerade traffic to this route's prefix Masquerade bool `json:"masquerade"` @@ -3380,6 +3693,9 @@ type Policy struct { // Id Policy ID Id *string `json:"id,omitempty"` + // InspectionPolicies Inspection policy IDs applied to traffic matching this policy + InspectionPolicies *[]string `json:"inspection_policies,omitempty"` + // Name Policy name identifier Name string `json:"name"` @@ -3398,6 +3714,9 @@ type PolicyCreate struct { // Enabled Policy status Enabled bool `json:"enabled"` + // InspectionPolicies Inspection policy IDs applied to traffic matching this policy + InspectionPolicies *[]string `json:"inspection_policies,omitempty"` + // Name Policy name identifier Name string `json:"name"` @@ -3558,6 +3877,9 @@ type PolicyUpdate struct { // Enabled Policy status Enabled bool `json:"enabled"` + // InspectionPolicies Inspection policy IDs applied to traffic matching this policy. When set, traffic is routed through a transparent proxy on the destination network's routing peers. + InspectionPolicies *[]string `json:"inspection_policies,omitempty"` + // Name Policy name identifier Name string `json:"name"` @@ -3874,6 +4196,43 @@ type RouteRequest struct { SkipAutoApply *bool `json:"skip_auto_apply,omitempty"` } +// RouterInspectionConfig defines model for RouterInspectionConfig. +type RouterInspectionConfig struct { + // CaCertPem PEM-encoded CA certificate for MITM TLS inspection + CaCertPem *string `json:"ca_cert_pem,omitempty"` + + // CaKeyPem PEM-encoded CA private key for MITM TLS inspection + CaKeyPem *string `json:"ca_key_pem,omitempty"` + + // DefaultAction Action when no inspection rule matches + DefaultAction *RouterInspectionConfigDefaultAction `json:"default_action,omitempty"` + + // Enabled Whether traffic inspection is active on this routing peer + Enabled bool `json:"enabled"` + + // EnvoyAdminPort Envoy admin API port for health checks. 0 picks a free port. + EnvoyAdminPort *int `json:"envoy_admin_port,omitempty"` + + // EnvoyBinaryPath Path to envoy binary when mode is envoy. Empty searches $PATH. + EnvoyBinaryPath *string `json:"envoy_binary_path,omitempty"` + + // ExternalUrl External proxy URL (http:// or socks5://) when mode is external + ExternalUrl *string `json:"external_url,omitempty"` + Icap *InspectionICAPConfig `json:"icap,omitempty"` + + // Mode Inspection mode + Mode *RouterInspectionConfigMode `json:"mode,omitempty"` + + // RedirectPorts Destination ports to intercept. Empty means all ports. + RedirectPorts *[]int `json:"redirect_ports,omitempty"` +} + +// RouterInspectionConfigDefaultAction Action when no inspection rule matches +type RouterInspectionConfigDefaultAction string + +// RouterInspectionConfigMode Inspection mode +type RouterInspectionConfigMode string + // RulePortRange Policy rule affected ports range type RulePortRange struct { // End The ending port of the range @@ -4959,6 +5318,12 @@ type PostApiIngressPeersJSONRequestBody = IngressPeerCreateRequest // PutApiIngressPeersIngressPeerIdJSONRequestBody defines body for PutApiIngressPeersIngressPeerId for application/json ContentType. type PutApiIngressPeersIngressPeerIdJSONRequestBody = IngressPeerUpdateRequest +// PostApiInspectionPoliciesJSONRequestBody defines body for PostApiInspectionPolicies for application/json ContentType. +type PostApiInspectionPoliciesJSONRequestBody = InspectionPolicyMinimum + +// PutApiInspectionPoliciesPolicyIdJSONRequestBody defines body for PutApiInspectionPoliciesPolicyId for application/json ContentType. +type PutApiInspectionPoliciesPolicyIdJSONRequestBody = InspectionPolicyMinimum + // CreateAzureIntegrationJSONRequestBody defines body for CreateAzureIntegration for application/json ContentType. type CreateAzureIntegrationJSONRequestBody = CreateAzureIntegrationRequest diff --git a/shared/management/proto/management.pb.go b/shared/management/proto/management.pb.go index 604f9c793..37cda0a86 100644 --- a/shared/management/proto/management.pb.go +++ b/shared/management/proto/management.pb.go @@ -276,6 +276,165 @@ func (ExposeProtocol) EnumDescriptor() ([]byte, []int) { return file_management_proto_rawDescGZIP(), []int{4} } +type TransparentProxyMode int32 + +const ( + TransparentProxyMode_TP_MODE_BUILTIN TransparentProxyMode = 0 + TransparentProxyMode_TP_MODE_EXTERNAL TransparentProxyMode = 1 + TransparentProxyMode_TP_MODE_ENVOY TransparentProxyMode = 2 +) + +// Enum value maps for TransparentProxyMode. +var ( + TransparentProxyMode_name = map[int32]string{ + 0: "TP_MODE_BUILTIN", + 1: "TP_MODE_EXTERNAL", + 2: "TP_MODE_ENVOY", + } + TransparentProxyMode_value = map[string]int32{ + "TP_MODE_BUILTIN": 0, + "TP_MODE_EXTERNAL": 1, + "TP_MODE_ENVOY": 2, + } +) + +func (x TransparentProxyMode) Enum() *TransparentProxyMode { + p := new(TransparentProxyMode) + *p = x + return p +} + +func (x TransparentProxyMode) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (TransparentProxyMode) Descriptor() protoreflect.EnumDescriptor { + return file_management_proto_enumTypes[5].Descriptor() +} + +func (TransparentProxyMode) Type() protoreflect.EnumType { + return &file_management_proto_enumTypes[5] +} + +func (x TransparentProxyMode) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use TransparentProxyMode.Descriptor instead. +func (TransparentProxyMode) EnumDescriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{5} +} + +type TransparentProxyAction int32 + +const ( + TransparentProxyAction_TP_ACTION_ALLOW TransparentProxyAction = 0 + TransparentProxyAction_TP_ACTION_BLOCK TransparentProxyAction = 1 + TransparentProxyAction_TP_ACTION_INSPECT TransparentProxyAction = 2 +) + +// Enum value maps for TransparentProxyAction. +var ( + TransparentProxyAction_name = map[int32]string{ + 0: "TP_ACTION_ALLOW", + 1: "TP_ACTION_BLOCK", + 2: "TP_ACTION_INSPECT", + } + TransparentProxyAction_value = map[string]int32{ + "TP_ACTION_ALLOW": 0, + "TP_ACTION_BLOCK": 1, + "TP_ACTION_INSPECT": 2, + } +) + +func (x TransparentProxyAction) Enum() *TransparentProxyAction { + p := new(TransparentProxyAction) + *p = x + return p +} + +func (x TransparentProxyAction) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (TransparentProxyAction) Descriptor() protoreflect.EnumDescriptor { + return file_management_proto_enumTypes[6].Descriptor() +} + +func (TransparentProxyAction) Type() protoreflect.EnumType { + return &file_management_proto_enumTypes[6] +} + +func (x TransparentProxyAction) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use TransparentProxyAction.Descriptor instead. +func (TransparentProxyAction) EnumDescriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{6} +} + +type TransparentProxyProtocol int32 + +const ( + TransparentProxyProtocol_TP_PROTO_ALL TransparentProxyProtocol = 0 + TransparentProxyProtocol_TP_PROTO_HTTP TransparentProxyProtocol = 1 + TransparentProxyProtocol_TP_PROTO_HTTPS TransparentProxyProtocol = 2 + TransparentProxyProtocol_TP_PROTO_H2 TransparentProxyProtocol = 3 + TransparentProxyProtocol_TP_PROTO_H3 TransparentProxyProtocol = 4 + TransparentProxyProtocol_TP_PROTO_WEBSOCKET TransparentProxyProtocol = 5 + TransparentProxyProtocol_TP_PROTO_OTHER TransparentProxyProtocol = 6 +) + +// Enum value maps for TransparentProxyProtocol. +var ( + TransparentProxyProtocol_name = map[int32]string{ + 0: "TP_PROTO_ALL", + 1: "TP_PROTO_HTTP", + 2: "TP_PROTO_HTTPS", + 3: "TP_PROTO_H2", + 4: "TP_PROTO_H3", + 5: "TP_PROTO_WEBSOCKET", + 6: "TP_PROTO_OTHER", + } + TransparentProxyProtocol_value = map[string]int32{ + "TP_PROTO_ALL": 0, + "TP_PROTO_HTTP": 1, + "TP_PROTO_HTTPS": 2, + "TP_PROTO_H2": 3, + "TP_PROTO_H3": 4, + "TP_PROTO_WEBSOCKET": 5, + "TP_PROTO_OTHER": 6, + } +) + +func (x TransparentProxyProtocol) Enum() *TransparentProxyProtocol { + p := new(TransparentProxyProtocol) + *p = x + return p +} + +func (x TransparentProxyProtocol) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (TransparentProxyProtocol) Descriptor() protoreflect.EnumDescriptor { + return file_management_proto_enumTypes[7].Descriptor() +} + +func (TransparentProxyProtocol) Type() protoreflect.EnumType { + return &file_management_proto_enumTypes[7] +} + +func (x TransparentProxyProtocol) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use TransparentProxyProtocol.Descriptor instead. +func (TransparentProxyProtocol) EnumDescriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{7} +} + type HostConfig_Protocol int32 const ( @@ -315,11 +474,11 @@ func (x HostConfig_Protocol) String() string { } func (HostConfig_Protocol) Descriptor() protoreflect.EnumDescriptor { - return file_management_proto_enumTypes[5].Descriptor() + return file_management_proto_enumTypes[8].Descriptor() } func (HostConfig_Protocol) Type() protoreflect.EnumType { - return &file_management_proto_enumTypes[5] + return &file_management_proto_enumTypes[8] } func (x HostConfig_Protocol) Number() protoreflect.EnumNumber { @@ -358,11 +517,11 @@ func (x DeviceAuthorizationFlowProvider) String() string { } func (DeviceAuthorizationFlowProvider) Descriptor() protoreflect.EnumDescriptor { - return file_management_proto_enumTypes[6].Descriptor() + return file_management_proto_enumTypes[9].Descriptor() } func (DeviceAuthorizationFlowProvider) Type() protoreflect.EnumType { - return &file_management_proto_enumTypes[6] + return &file_management_proto_enumTypes[9] } func (x DeviceAuthorizationFlowProvider) Number() protoreflect.EnumNumber { @@ -2343,6 +2502,8 @@ type NetworkMap struct { ForwardingRules []*ForwardingRule `protobuf:"bytes,12,rep,name=forwardingRules,proto3" json:"forwardingRules,omitempty"` // SSHAuth represents SSH authorization configuration SshAuth *SSHAuth `protobuf:"bytes,13,opt,name=sshAuth,proto3" json:"sshAuth,omitempty"` + // TransparentProxyConfig represents transparent proxy configuration for this peer + TransparentProxyConfig *TransparentProxyConfig `protobuf:"bytes,14,opt,name=transparentProxyConfig,proto3" json:"transparentProxyConfig,omitempty"` } func (x *NetworkMap) Reset() { @@ -2468,6 +2629,13 @@ func (x *NetworkMap) GetSshAuth() *SSHAuth { return nil } +func (x *NetworkMap) GetTransparentProxyConfig() *TransparentProxyConfig { + if x != nil { + return x.TransparentProxyConfig + } + return nil +} + type SSHAuth struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -4385,6 +4553,407 @@ func (*StopExposeResponse) Descriptor() ([]byte, []int) { return file_management_proto_rawDescGZIP(), []int{52} } +// TransparentProxyConfig configures the transparent forward proxy on a routing peer. +type TransparentProxyConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` + Mode TransparentProxyMode `protobuf:"varint,2,opt,name=mode,proto3,enum=management.TransparentProxyMode" json:"mode,omitempty"` + // External proxy URL for MODE_EXTERNAL (http:// or socks5://) + ExternalProxyUrl string `protobuf:"bytes,3,opt,name=externalProxyUrl,proto3" json:"externalProxyUrl,omitempty"` + DefaultAction TransparentProxyAction `protobuf:"varint,4,opt,name=defaultAction,proto3,enum=management.TransparentProxyAction" json:"defaultAction,omitempty"` + // L3/L4 interception: which traffic gets redirected to the proxy. + // Admin decides: activate for these users/subnets on these ports. + // Used for both kernel TPROXY rules and userspace forwarder source filtering. + RedirectSources []string `protobuf:"bytes,5,rep,name=redirectSources,proto3" json:"redirectSources,omitempty"` + // Destination ports to intercept. Empty means all ports. + RedirectPorts []uint32 `protobuf:"varint,6,rep,packed,name=redirectPorts,proto3" json:"redirectPorts,omitempty"` + // L7 inspection rules: what the proxy does with intercepted traffic. + Rules []*TransparentProxyRule `protobuf:"bytes,7,rep,name=rules,proto3" json:"rules,omitempty"` + Icap *TransparentProxyICAPConfig `protobuf:"bytes,8,opt,name=icap,proto3" json:"icap,omitempty"` + // MITM CA certificate in PEM format + CaCertPem []byte `protobuf:"bytes,9,opt,name=caCertPem,proto3" json:"caCertPem,omitempty"` + // MITM CA private key in PEM format + CaKeyPem []byte `protobuf:"bytes,10,opt,name=caKeyPem,proto3" json:"caKeyPem,omitempty"` + // TPROXY listen port for kernel mode. 0 means auto-assign. + ListenPort uint32 `protobuf:"varint,11,opt,name=listenPort,proto3" json:"listenPort,omitempty"` + // Envoy sidecar configuration (MODE_ENVOY only) + EnvoyBinaryPath string `protobuf:"bytes,12,opt,name=envoyBinaryPath,proto3" json:"envoyBinaryPath,omitempty"` + EnvoyAdminPort uint32 `protobuf:"varint,13,opt,name=envoyAdminPort,proto3" json:"envoyAdminPort,omitempty"` + EnvoySnippets *TransparentProxyEnvoySnippets `protobuf:"bytes,14,opt,name=envoySnippets,proto3" json:"envoySnippets,omitempty"` +} + +func (x *TransparentProxyConfig) Reset() { + *x = TransparentProxyConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_management_proto_msgTypes[53] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TransparentProxyConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TransparentProxyConfig) ProtoMessage() {} + +func (x *TransparentProxyConfig) ProtoReflect() protoreflect.Message { + mi := &file_management_proto_msgTypes[53] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TransparentProxyConfig.ProtoReflect.Descriptor instead. +func (*TransparentProxyConfig) Descriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{53} +} + +func (x *TransparentProxyConfig) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +func (x *TransparentProxyConfig) GetMode() TransparentProxyMode { + if x != nil { + return x.Mode + } + return TransparentProxyMode_TP_MODE_BUILTIN +} + +func (x *TransparentProxyConfig) GetExternalProxyUrl() string { + if x != nil { + return x.ExternalProxyUrl + } + return "" +} + +func (x *TransparentProxyConfig) GetDefaultAction() TransparentProxyAction { + if x != nil { + return x.DefaultAction + } + return TransparentProxyAction_TP_ACTION_ALLOW +} + +func (x *TransparentProxyConfig) GetRedirectSources() []string { + if x != nil { + return x.RedirectSources + } + return nil +} + +func (x *TransparentProxyConfig) GetRedirectPorts() []uint32 { + if x != nil { + return x.RedirectPorts + } + return nil +} + +func (x *TransparentProxyConfig) GetRules() []*TransparentProxyRule { + if x != nil { + return x.Rules + } + return nil +} + +func (x *TransparentProxyConfig) GetIcap() *TransparentProxyICAPConfig { + if x != nil { + return x.Icap + } + return nil +} + +func (x *TransparentProxyConfig) GetCaCertPem() []byte { + if x != nil { + return x.CaCertPem + } + return nil +} + +func (x *TransparentProxyConfig) GetCaKeyPem() []byte { + if x != nil { + return x.CaKeyPem + } + return nil +} + +func (x *TransparentProxyConfig) GetListenPort() uint32 { + if x != nil { + return x.ListenPort + } + return 0 +} + +func (x *TransparentProxyConfig) GetEnvoyBinaryPath() string { + if x != nil { + return x.EnvoyBinaryPath + } + return "" +} + +func (x *TransparentProxyConfig) GetEnvoyAdminPort() uint32 { + if x != nil { + return x.EnvoyAdminPort + } + return 0 +} + +func (x *TransparentProxyConfig) GetEnvoySnippets() *TransparentProxyEnvoySnippets { + if x != nil { + return x.EnvoySnippets + } + return nil +} + +// TransparentProxyRule is an L7 inspection rule evaluated by the proxy engine. +type TransparentProxyRule struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // Domain patterns to match via SNI or Host header (e.g., *.example.com) + Domains []string `protobuf:"bytes,2,rep,name=domains,proto3" json:"domains,omitempty"` + // Destination CIDRs for optional L7 destination filtering + Networks []string `protobuf:"bytes,3,rep,name=networks,proto3" json:"networks,omitempty"` + // Destination ports for optional per-rule port filtering + Ports []uint32 `protobuf:"varint,4,rep,packed,name=ports,proto3" json:"ports,omitempty"` + Action TransparentProxyAction `protobuf:"varint,5,opt,name=action,proto3,enum=management.TransparentProxyAction" json:"action,omitempty"` + Priority int32 `protobuf:"varint,6,opt,name=priority,proto3" json:"priority,omitempty"` + // Protocols to match. Empty means all protocols. + Protocols []TransparentProxyProtocol `protobuf:"varint,7,rep,packed,name=protocols,proto3,enum=management.TransparentProxyProtocol" json:"protocols,omitempty"` + // URL path patterns to match (HTTP only, requires inspect for HTTPS). + // Supports prefix ("/api/"), exact ("/login"), and wildcard ("/admin/*"). + Paths []string `protobuf:"bytes,8,rep,name=paths,proto3" json:"paths,omitempty"` +} + +func (x *TransparentProxyRule) Reset() { + *x = TransparentProxyRule{} + if protoimpl.UnsafeEnabled { + mi := &file_management_proto_msgTypes[54] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TransparentProxyRule) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TransparentProxyRule) ProtoMessage() {} + +func (x *TransparentProxyRule) ProtoReflect() protoreflect.Message { + mi := &file_management_proto_msgTypes[54] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TransparentProxyRule.ProtoReflect.Descriptor instead. +func (*TransparentProxyRule) Descriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{54} +} + +func (x *TransparentProxyRule) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *TransparentProxyRule) GetDomains() []string { + if x != nil { + return x.Domains + } + return nil +} + +func (x *TransparentProxyRule) GetNetworks() []string { + if x != nil { + return x.Networks + } + return nil +} + +func (x *TransparentProxyRule) GetPorts() []uint32 { + if x != nil { + return x.Ports + } + return nil +} + +func (x *TransparentProxyRule) GetAction() TransparentProxyAction { + if x != nil { + return x.Action + } + return TransparentProxyAction_TP_ACTION_ALLOW +} + +func (x *TransparentProxyRule) GetPriority() int32 { + if x != nil { + return x.Priority + } + return 0 +} + +func (x *TransparentProxyRule) GetProtocols() []TransparentProxyProtocol { + if x != nil { + return x.Protocols + } + return nil +} + +func (x *TransparentProxyRule) GetPaths() []string { + if x != nil { + return x.Paths + } + return nil +} + +type TransparentProxyICAPConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ReqmodUrl string `protobuf:"bytes,1,opt,name=reqmodUrl,proto3" json:"reqmodUrl,omitempty"` + RespmodUrl string `protobuf:"bytes,2,opt,name=respmodUrl,proto3" json:"respmodUrl,omitempty"` + MaxConnections int32 `protobuf:"varint,3,opt,name=maxConnections,proto3" json:"maxConnections,omitempty"` +} + +func (x *TransparentProxyICAPConfig) Reset() { + *x = TransparentProxyICAPConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_management_proto_msgTypes[55] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TransparentProxyICAPConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TransparentProxyICAPConfig) ProtoMessage() {} + +func (x *TransparentProxyICAPConfig) ProtoReflect() protoreflect.Message { + mi := &file_management_proto_msgTypes[55] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TransparentProxyICAPConfig.ProtoReflect.Descriptor instead. +func (*TransparentProxyICAPConfig) Descriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{55} +} + +func (x *TransparentProxyICAPConfig) GetReqmodUrl() string { + if x != nil { + return x.ReqmodUrl + } + return "" +} + +func (x *TransparentProxyICAPConfig) GetRespmodUrl() string { + if x != nil { + return x.RespmodUrl + } + return "" +} + +func (x *TransparentProxyICAPConfig) GetMaxConnections() int32 { + if x != nil { + return x.MaxConnections + } + return 0 +} + +type TransparentProxyEnvoySnippets struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // YAML injected into the HCM filter chain before the router filter. + HttpFilters string `protobuf:"bytes,1,opt,name=httpFilters,proto3" json:"httpFilters,omitempty"` + // YAML for additional upstream clusters referenced by filters. + Clusters string `protobuf:"bytes,2,opt,name=clusters,proto3" json:"clusters,omitempty"` + // YAML injected into the TLS filter chain before tcp_proxy (L4 filters). + NetworkFilters string `protobuf:"bytes,3,opt,name=networkFilters,proto3" json:"networkFilters,omitempty"` +} + +func (x *TransparentProxyEnvoySnippets) Reset() { + *x = TransparentProxyEnvoySnippets{} + if protoimpl.UnsafeEnabled { + mi := &file_management_proto_msgTypes[56] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TransparentProxyEnvoySnippets) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TransparentProxyEnvoySnippets) ProtoMessage() {} + +func (x *TransparentProxyEnvoySnippets) ProtoReflect() protoreflect.Message { + mi := &file_management_proto_msgTypes[56] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TransparentProxyEnvoySnippets.ProtoReflect.Descriptor instead. +func (*TransparentProxyEnvoySnippets) Descriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{56} +} + +func (x *TransparentProxyEnvoySnippets) GetHttpFilters() string { + if x != nil { + return x.HttpFilters + } + return "" +} + +func (x *TransparentProxyEnvoySnippets) GetClusters() string { + if x != nil { + return x.Clusters + } + return "" +} + +func (x *TransparentProxyEnvoySnippets) GetNetworkFilters() string { + if x != nil { + return x.NetworkFilters + } + return "" +} + type PortInfo_Range struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -4397,7 +4966,7 @@ type PortInfo_Range struct { func (x *PortInfo_Range) Reset() { *x = PortInfo_Range{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[54] + mi := &file_management_proto_msgTypes[58] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4410,7 +4979,7 @@ func (x *PortInfo_Range) String() string { func (*PortInfo_Range) ProtoMessage() {} func (x *PortInfo_Range) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[54] + mi := &file_management_proto_msgTypes[58] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4742,7 +5311,7 @@ var file_management_proto_rawDesc = []byte{ 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x22, 0x0a, 0x0c, 0x61, 0x6c, 0x77, 0x61, 0x79, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x61, 0x6c, 0x77, 0x61, 0x79, 0x73, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x22, 0xe8, 0x05, 0x0a, 0x0a, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, + 0x61, 0x74, 0x65, 0x22, 0xc4, 0x06, 0x0a, 0x0a, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, @@ -4788,349 +5357,453 @@ var file_management_proto_rawDesc = []byte{ 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x73, 0x73, 0x68, 0x41, 0x75, 0x74, 0x68, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, - 0x48, 0x41, 0x75, 0x74, 0x68, 0x52, 0x07, 0x73, 0x73, 0x68, 0x41, 0x75, 0x74, 0x68, 0x22, 0x82, - 0x02, 0x0a, 0x07, 0x53, 0x53, 0x48, 0x41, 0x75, 0x74, 0x68, 0x12, 0x20, 0x0a, 0x0b, 0x55, 0x73, - 0x65, 0x72, 0x49, 0x44, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0b, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x12, 0x28, 0x0a, 0x0f, - 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x55, 0x73, 0x65, 0x72, 0x73, 0x18, - 0x02, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x0f, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, - 0x64, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x4a, 0x0a, 0x0d, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, - 0x65, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, 0x2e, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x41, 0x75, - 0x74, 0x68, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0c, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x55, 0x73, 0x65, - 0x72, 0x73, 0x1a, 0x5f, 0x0a, 0x11, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x55, 0x73, 0x65, - 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x34, 0x0a, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x55, 0x73, 0x65, - 0x72, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, - 0x02, 0x38, 0x01, 0x22, 0x2e, 0x0a, 0x12, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x55, 0x73, - 0x65, 0x72, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x69, 0x6e, 0x64, - 0x65, 0x78, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0d, 0x52, 0x07, 0x69, 0x6e, 0x64, 0x65, - 0x78, 0x65, 0x73, 0x22, 0xbb, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, - 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75, - 0x62, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, - 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, - 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, - 0x64, 0x49, 0x70, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, - 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, - 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x22, 0x0a, - 0x0c, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x22, 0x7e, 0x0a, 0x09, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1e, - 0x0a, 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1c, - 0x0a, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x33, 0x0a, 0x09, - 0x6a, 0x77, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4a, 0x57, 0x54, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x6a, 0x77, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x22, 0x20, 0x0a, 0x1e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, - 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x22, 0xbf, 0x01, 0x0a, 0x17, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, - 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, - 0x48, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0e, 0x32, 0x2c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, - 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, - 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, - 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, - 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, - 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x16, 0x0a, - 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x0a, 0x0a, 0x06, 0x48, 0x4f, 0x53, - 0x54, 0x45, 0x44, 0x10, 0x00, 0x22, 0x1e, 0x0a, 0x1c, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, - 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x5b, 0x0a, 0x15, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, - 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x42, - 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x22, 0xbc, 0x03, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, - 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, - 0x44, 0x12, 0x26, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, - 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0c, 0x43, 0x6c, 0x69, - 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, - 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, - 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x2e, 0x0a, - 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, - 0x69, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, - 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x24, 0x0a, - 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, - 0x69, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x55, 0x73, 0x65, - 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x55, - 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x34, 0x0a, 0x15, 0x41, 0x75, 0x74, - 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, - 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, - 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, - 0x22, 0x0a, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73, 0x18, - 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, - 0x52, 0x4c, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x50, 0x72, - 0x6f, 0x6d, 0x70, 0x74, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x12, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x4c, 0x6f, - 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x46, 0x6c, 0x61, 0x67, - 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x46, 0x6c, 0x61, - 0x67, 0x22, 0x93, 0x02, 0x0a, 0x05, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, - 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x4e, - 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x4e, 0x65, - 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x20, 0x0a, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, - 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x4e, 0x65, 0x74, 0x77, - 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x4d, - 0x65, 0x74, 0x72, 0x69, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4d, 0x65, 0x74, - 0x72, 0x69, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, - 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, - 0x61, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, - 0x61, 0x69, 0x6e, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, - 0x69, 0x6e, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f, 0x75, 0x74, 0x65, - 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f, 0x75, 0x74, - 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x73, 0x6b, 0x69, 0x70, 0x41, 0x75, 0x74, 0x6f, 0x41, 0x70, 0x70, - 0x6c, 0x79, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x73, 0x6b, 0x69, 0x70, 0x41, 0x75, - 0x74, 0x6f, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x22, 0xde, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x24, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x47, 0x0a, 0x10, 0x4e, - 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, - 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, - 0x75, 0x70, 0x52, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, - 0x6f, 0x75, 0x70, 0x73, 0x12, 0x38, 0x0a, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, - 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, - 0x65, 0x52, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x12, 0x28, - 0x0a, 0x0d, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x65, 0x72, 0x50, 0x6f, 0x72, 0x74, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x03, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0d, 0x46, 0x6f, 0x72, 0x77, 0x61, - 0x72, 0x64, 0x65, 0x72, 0x50, 0x6f, 0x72, 0x74, 0x22, 0xb8, 0x01, 0x0a, 0x0a, 0x43, 0x75, 0x73, - 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, - 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, - 0x32, 0x0a, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x69, - 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x52, 0x65, 0x63, 0x6f, - 0x72, 0x64, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, - 0x61, 0x69, 0x6e, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x44, - 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x2a, 0x0a, 0x10, 0x4e, 0x6f, 0x6e, 0x41, 0x75, - 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x61, 0x74, 0x69, 0x76, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x10, 0x4e, 0x6f, 0x6e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x61, 0x74, - 0x69, 0x76, 0x65, 0x22, 0x74, 0x0a, 0x0c, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, - 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x43, - 0x6c, 0x61, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x43, 0x6c, 0x61, 0x73, - 0x73, 0x12, 0x10, 0x0a, 0x03, 0x54, 0x54, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, - 0x54, 0x54, 0x4c, 0x12, 0x14, 0x0a, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x22, 0xb3, 0x01, 0x0a, 0x0f, 0x4e, 0x61, - 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x38, 0x0a, - 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0b, 0x4e, 0x61, 0x6d, 0x65, - 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, - 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, - 0x79, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, - 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x53, - 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, - 0x6c, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, - 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, - 0x48, 0x0a, 0x0a, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x0e, 0x0a, - 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, - 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4e, - 0x53, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, 0xa7, 0x02, 0x0a, 0x0c, 0x46, 0x69, - 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x50, 0x65, - 0x65, 0x72, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x50, 0x65, 0x65, 0x72, - 0x49, 0x50, 0x12, 0x37, 0x0a, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x06, 0x41, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08, 0x50, - 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, - 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, - 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x30, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, - 0x6f, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x50, - 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x49, 0x44, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x50, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x49, 0x44, 0x22, 0x38, 0x0a, 0x0e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, - 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x12, 0x10, 0x0a, 0x03, 0x6d, - 0x61, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x61, 0x63, 0x22, 0x1e, 0x0a, - 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x22, 0x96, 0x01, - 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x04, 0x70, 0x6f, - 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, - 0x12, 0x32, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, - 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x48, 0x00, 0x52, 0x05, 0x72, - 0x61, 0x6e, 0x67, 0x65, 0x1a, 0x2f, 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x14, 0x0a, - 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x73, 0x74, - 0x61, 0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, - 0x52, 0x03, 0x65, 0x6e, 0x64, 0x42, 0x0f, 0x0a, 0x0d, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x65, 0x6c, - 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x87, 0x03, 0x0a, 0x11, 0x52, 0x6f, 0x75, 0x74, 0x65, - 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, - 0x28, 0x09, 0x52, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, - 0x12, 0x2e, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, - 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x30, 0x0a, 0x08, 0x70, 0x6f, 0x72, 0x74, - 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, - 0x52, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x73, - 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, - 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, - 0x69, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, - 0x6e, 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, - 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x63, 0x75, 0x73, 0x74, - 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x50, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, - 0x44, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x44, - 0x22, 0xf2, 0x01, 0x0a, 0x0e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, - 0x75, 0x6c, 0x65, 0x12, 0x34, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, - 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x3e, 0x0a, 0x0f, 0x64, 0x65, 0x73, - 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x74, 0x72, 0x61, - 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, - 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x3c, 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, - 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, - 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, - 0x64, 0x50, 0x6f, 0x72, 0x74, 0x22, 0x8b, 0x02, 0x0a, 0x14, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, - 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, - 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x70, 0x6f, - 0x72, 0x74, 0x12, 0x36, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, - 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x10, 0x0a, 0x03, 0x70, 0x69, - 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x70, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, - 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, - 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x75, 0x73, 0x65, 0x72, - 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x75, - 0x73, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, - 0x61, 0x69, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, - 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, - 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x50, 0x72, 0x65, 0x66, - 0x69, 0x78, 0x12, 0x1f, 0x0a, 0x0b, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x5f, 0x70, 0x6f, 0x72, - 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x50, - 0x6f, 0x72, 0x74, 0x22, 0xa1, 0x01, 0x0a, 0x15, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, - 0x0c, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, - 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x55, 0x72, - 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x2c, 0x0a, 0x12, 0x70, 0x6f, 0x72, - 0x74, 0x5f, 0x61, 0x75, 0x74, 0x6f, 0x5f, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x70, 0x6f, 0x72, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x41, - 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x22, 0x2c, 0x0a, 0x12, 0x52, 0x65, 0x6e, 0x65, 0x77, + 0x48, 0x41, 0x75, 0x74, 0x68, 0x52, 0x07, 0x73, 0x73, 0x68, 0x41, 0x75, 0x74, 0x68, 0x12, 0x5a, + 0x0a, 0x16, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x50, 0x72, 0x6f, + 0x78, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x54, 0x72, 0x61, 0x6e, + 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x52, 0x16, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x50, + 0x72, 0x6f, 0x78, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x82, 0x02, 0x0a, 0x07, 0x53, + 0x53, 0x48, 0x41, 0x75, 0x74, 0x68, 0x12, 0x20, 0x0a, 0x0b, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, + 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x55, 0x73, 0x65, + 0x72, 0x49, 0x44, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x12, 0x28, 0x0a, 0x0f, 0x41, 0x75, 0x74, 0x68, + 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x55, 0x73, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, + 0x0c, 0x52, 0x0f, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x55, 0x73, 0x65, + 0x72, 0x73, 0x12, 0x4a, 0x0a, 0x0d, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x75, 0x73, + 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x41, 0x75, 0x74, 0x68, 0x2e, 0x4d, + 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x52, 0x0c, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, 0x1a, 0x5f, + 0x0a, 0x11, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x34, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x55, 0x73, 0x65, 0x72, 0x49, 0x6e, 0x64, + 0x65, 0x78, 0x65, 0x73, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, + 0x2e, 0x0a, 0x12, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x55, 0x73, 0x65, 0x72, 0x49, 0x6e, + 0x64, 0x65, 0x78, 0x65, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x0d, 0x52, 0x07, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x22, + 0xbb, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, + 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, 0x18, 0x02, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, + 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x73, 0x73, 0x68, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x22, 0x0a, 0x0c, 0x61, 0x67, 0x65, + 0x6e, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0c, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x7e, 0x0a, + 0x09, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x73, + 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, + 0x73, 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, + 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, + 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x33, 0x0a, 0x09, 0x6a, 0x77, 0x74, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4a, 0x57, 0x54, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x52, 0x09, 0x6a, 0x77, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x20, 0x0a, + 0x1e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, + 0xbf, 0x01, 0x0a, 0x17, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, + 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x48, 0x0a, 0x08, 0x50, + 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2c, 0x2e, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, + 0x6f, 0x77, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x08, 0x50, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, + 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x16, 0x0a, 0x08, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x0a, 0x0a, 0x06, 0x48, 0x4f, 0x53, 0x54, 0x45, 0x44, 0x10, + 0x00, 0x22, 0x1e, 0x0a, 0x1c, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, + 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x22, 0x5b, 0x0a, 0x15, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, + 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, + 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, + 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0xbc, + 0x03, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x12, 0x26, 0x0a, + 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, + 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x1a, 0x0a, + 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x2e, 0x0a, 0x12, 0x44, 0x65, 0x76, + 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, + 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x24, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, + 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, + 0x14, 0x0a, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x55, 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x55, 0x73, 0x65, 0x49, 0x44, + 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x34, 0x0a, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, + 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x09, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x52, + 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73, 0x12, + 0x2e, 0x0a, 0x12, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, + 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x44, 0x69, 0x73, + 0x61, 0x62, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, + 0x1c, 0x0a, 0x09, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x46, 0x6c, 0x61, 0x67, 0x18, 0x0c, 0x20, 0x01, + 0x28, 0x0d, 0x52, 0x09, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x46, 0x6c, 0x61, 0x67, 0x22, 0x93, 0x02, + 0x0a, 0x05, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, + 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x12, 0x20, 0x0a, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, + 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, + 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x12, + 0x1e, 0x0a, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x12, + 0x14, 0x0a, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x4e, 0x65, 0x74, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, + 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, + 0x1c, 0x0a, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x18, 0x09, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x24, 0x0a, + 0x0d, 0x73, 0x6b, 0x69, 0x70, 0x41, 0x75, 0x74, 0x6f, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x18, 0x0a, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x73, 0x6b, 0x69, 0x70, 0x41, 0x75, 0x74, 0x6f, 0x41, 0x70, + 0x70, 0x6c, 0x79, 0x22, 0xde, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x12, 0x24, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, + 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x47, 0x0a, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, + 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x10, + 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, + 0x12, 0x38, 0x0a, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x18, + 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x52, 0x0b, 0x43, + 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x0d, 0x46, 0x6f, + 0x72, 0x77, 0x61, 0x72, 0x64, 0x65, 0x72, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x03, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0d, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x65, 0x72, + 0x50, 0x6f, 0x72, 0x74, 0x22, 0xb8, 0x01, 0x0a, 0x0a, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, + 0x6f, 0x6e, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x32, 0x0a, 0x07, 0x52, + 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, + 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x12, + 0x32, 0x0a, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x44, + 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x53, + 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x44, 0x69, 0x73, 0x61, 0x62, + 0x6c, 0x65, 0x64, 0x12, 0x2a, 0x0a, 0x10, 0x4e, 0x6f, 0x6e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, + 0x69, 0x74, 0x61, 0x74, 0x69, 0x76, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x4e, + 0x6f, 0x6e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x61, 0x74, 0x69, 0x76, 0x65, 0x22, + 0x74, 0x0a, 0x0c, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, + 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, + 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x12, 0x10, 0x0a, + 0x03, 0x54, 0x54, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x54, 0x54, 0x4c, 0x12, + 0x14, 0x0a, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x52, 0x44, 0x61, 0x74, 0x61, 0x22, 0xb3, 0x01, 0x0a, 0x0f, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x38, 0x0a, 0x0b, 0x4e, 0x61, 0x6d, + 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, + 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x18, 0x0a, + 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, + 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, + 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, + 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x48, 0x0a, 0x0a, 0x4e, + 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x4e, 0x53, 0x54, + 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, + 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, 0xa7, 0x02, 0x0a, 0x0c, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, + 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x12, 0x37, + 0x0a, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x19, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, + 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x44, 0x69, + 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, + 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, + 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x12, 0x0a, + 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x6f, 0x72, + 0x74, 0x12, 0x30, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, + 0x6e, 0x66, 0x6f, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x22, + 0x38, 0x0a, 0x0e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, + 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x61, 0x63, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x61, 0x63, 0x22, 0x1e, 0x0a, 0x06, 0x43, 0x68, 0x65, + 0x63, 0x6b, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x08, 0x50, 0x6f, + 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x32, 0x0a, 0x05, + 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, + 0x6f, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x48, 0x00, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, + 0x1a, 0x2f, 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, + 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, + 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x65, 0x6e, + 0x64, 0x42, 0x0f, 0x0a, 0x0d, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x22, 0x87, 0x03, 0x0a, 0x11, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, 0x69, 0x72, 0x65, + 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x06, + 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, + 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, + 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, + 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x30, 0x0a, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x70, 0x6f, + 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, + 0x6d, 0x69, 0x63, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, + 0x61, 0x6d, 0x69, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, + 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x26, + 0x0a, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, + 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, + 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x49, 0x44, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x44, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x44, 0x22, 0xf2, 0x01, 0x0a, + 0x0e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x12, + 0x34, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, + 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x3e, 0x0a, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, + 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, + 0x74, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, + 0x65, 0x73, 0x73, 0x12, 0x3c, 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, + 0x64, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, + 0x6f, 0x52, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x72, + 0x74, 0x22, 0x8b, 0x02, 0x0a, 0x14, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6f, + 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x36, + 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x78, + 0x70, 0x6f, 0x73, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x10, 0x0a, 0x03, 0x70, 0x69, 0x6e, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x70, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, + 0x77, 0x6f, 0x72, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, + 0x77, 0x6f, 0x72, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x67, 0x72, 0x6f, + 0x75, 0x70, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x47, + 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x1f, 0x0a, + 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x1f, + 0x0a, 0x0b, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x08, 0x20, + 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x22, + 0xa1, 0x01, 0x0a, 0x15, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1f, 0x0a, 0x0b, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x55, 0x72, 0x6c, 0x12, 0x16, 0x0a, + 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, + 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x2c, 0x0a, 0x12, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x61, 0x75, + 0x74, 0x6f, 0x5f, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x10, 0x70, 0x6f, 0x72, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x41, 0x73, 0x73, 0x69, 0x67, + 0x6e, 0x65, 0x64, 0x22, 0x2c, 0x0a, 0x12, 0x52, 0x65, 0x6e, 0x65, 0x77, 0x45, 0x78, 0x70, 0x6f, + 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, + 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, + 0x6e, 0x22, 0x15, 0x0a, 0x13, 0x52, 0x65, 0x6e, 0x65, 0x77, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2b, 0x0a, 0x11, 0x53, 0x74, 0x6f, 0x70, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, - 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x22, 0x15, 0x0a, 0x13, 0x52, 0x65, 0x6e, 0x65, 0x77, 0x45, 0x78, - 0x70, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2b, 0x0a, 0x11, - 0x53, 0x74, 0x6f, 0x70, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x22, 0x14, 0x0a, 0x12, 0x53, 0x74, 0x6f, - 0x70, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2a, - 0x3a, 0x0a, 0x09, 0x4a, 0x6f, 0x62, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x0e, - 0x75, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x10, 0x00, - 0x12, 0x0d, 0x0a, 0x09, 0x73, 0x75, 0x63, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x10, 0x01, 0x12, - 0x0a, 0x0a, 0x06, 0x66, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x10, 0x02, 0x2a, 0x4c, 0x0a, 0x0c, 0x52, - 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, - 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x4c, 0x4c, 0x10, - 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, - 0x50, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x43, 0x4d, 0x50, 0x10, 0x04, 0x12, 0x0a, 0x0a, - 0x06, 0x43, 0x55, 0x53, 0x54, 0x4f, 0x4d, 0x10, 0x05, 0x2a, 0x20, 0x0a, 0x0d, 0x52, 0x75, 0x6c, - 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x06, 0x0a, 0x02, 0x49, 0x4e, - 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x2a, 0x22, 0x0a, 0x0a, 0x52, - 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x43, 0x43, - 0x45, 0x50, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x01, 0x2a, - 0x63, 0x0a, 0x0e, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, - 0x6c, 0x12, 0x0f, 0x0a, 0x0b, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x48, 0x54, 0x54, 0x50, - 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x48, 0x54, 0x54, - 0x50, 0x53, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x54, - 0x43, 0x50, 0x10, 0x02, 0x12, 0x0e, 0x0a, 0x0a, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x55, - 0x44, 0x50, 0x10, 0x03, 0x12, 0x0e, 0x0a, 0x0a, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x54, - 0x4c, 0x53, 0x10, 0x04, 0x32, 0xfd, 0x06, 0x0a, 0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, - 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, - 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, - 0x00, 0x12, 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e, 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, - 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, - 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0c, 0x47, 0x65, 0x74, - 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, - 0x09, 0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, - 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, + 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x22, 0x14, 0x0a, 0x12, 0x53, 0x74, 0x6f, 0x70, 0x45, 0x78, 0x70, + 0x6f, 0x73, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x9f, 0x05, 0x0a, 0x16, + 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, + 0x12, 0x34, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x54, 0x72, 0x61, 0x6e, + 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x4d, 0x6f, 0x64, 0x65, + 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x2a, 0x0a, 0x10, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x55, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x10, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x55, + 0x72, 0x6c, 0x12, 0x48, 0x0a, 0x0d, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x41, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, + 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0d, 0x64, + 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x28, 0x0a, 0x0f, + 0x72, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, + 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x72, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x53, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x72, 0x65, 0x64, 0x69, 0x72, 0x65, + 0x63, 0x74, 0x50, 0x6f, 0x72, 0x74, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0d, 0x52, 0x0d, 0x72, + 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x50, 0x6f, 0x72, 0x74, 0x73, 0x12, 0x36, 0x0a, 0x05, + 0x72, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, + 0x72, 0x65, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x05, 0x72, + 0x75, 0x6c, 0x65, 0x73, 0x12, 0x3a, 0x0a, 0x04, 0x69, 0x63, 0x61, 0x70, 0x18, 0x08, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79, + 0x49, 0x43, 0x41, 0x50, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x04, 0x69, 0x63, 0x61, 0x70, + 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x61, 0x43, 0x65, 0x72, 0x74, 0x50, 0x65, 0x6d, 0x18, 0x09, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x09, 0x63, 0x61, 0x43, 0x65, 0x72, 0x74, 0x50, 0x65, 0x6d, 0x12, 0x1a, + 0x0a, 0x08, 0x63, 0x61, 0x4b, 0x65, 0x79, 0x50, 0x65, 0x6d, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x08, 0x63, 0x61, 0x4b, 0x65, 0x79, 0x50, 0x65, 0x6d, 0x12, 0x1e, 0x0a, 0x0a, 0x6c, 0x69, + 0x73, 0x74, 0x65, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, + 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x28, 0x0a, 0x0f, 0x65, 0x6e, + 0x76, 0x6f, 0x79, 0x42, 0x69, 0x6e, 0x61, 0x72, 0x79, 0x50, 0x61, 0x74, 0x68, 0x18, 0x0c, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0f, 0x65, 0x6e, 0x76, 0x6f, 0x79, 0x42, 0x69, 0x6e, 0x61, 0x72, 0x79, + 0x50, 0x61, 0x74, 0x68, 0x12, 0x26, 0x0a, 0x0e, 0x65, 0x6e, 0x76, 0x6f, 0x79, 0x41, 0x64, 0x6d, + 0x69, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x65, 0x6e, + 0x76, 0x6f, 0x79, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x4f, 0x0a, 0x0d, + 0x65, 0x6e, 0x76, 0x6f, 0x79, 0x53, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x73, 0x18, 0x0e, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x78, + 0x79, 0x45, 0x6e, 0x76, 0x6f, 0x79, 0x53, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x73, 0x52, 0x0d, + 0x65, 0x6e, 0x76, 0x6f, 0x79, 0x53, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x73, 0x22, 0xa4, 0x02, + 0x0a, 0x14, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x50, 0x72, 0x6f, + 0x78, 0x79, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, + 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, + 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18, 0x03, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x14, 0x0a, 0x05, + 0x70, 0x6f, 0x72, 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0d, 0x52, 0x05, 0x70, 0x6f, 0x72, + 0x74, 0x73, 0x12, 0x3a, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79, + 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1a, + 0x0a, 0x08, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x08, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x12, 0x42, 0x0a, 0x09, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x24, 0x2e, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, + 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x50, 0x72, 0x6f, 0x74, 0x6f, + 0x63, 0x6f, 0x6c, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x73, 0x12, 0x14, + 0x0a, 0x05, 0x70, 0x61, 0x74, 0x68, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x70, + 0x61, 0x74, 0x68, 0x73, 0x22, 0x82, 0x01, 0x0a, 0x1a, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, + 0x72, 0x65, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x49, 0x43, 0x41, 0x50, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x71, 0x6d, 0x6f, 0x64, 0x55, 0x72, 0x6c, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x6d, 0x6f, 0x64, 0x55, 0x72, + 0x6c, 0x12, 0x1e, 0x0a, 0x0a, 0x72, 0x65, 0x73, 0x70, 0x6d, 0x6f, 0x64, 0x55, 0x72, 0x6c, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x70, 0x6d, 0x6f, 0x64, 0x55, 0x72, + 0x6c, 0x12, 0x26, 0x0a, 0x0e, 0x6d, 0x61, 0x78, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0e, 0x6d, 0x61, 0x78, 0x43, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x85, 0x01, 0x0a, 0x1d, 0x54, 0x72, + 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x45, 0x6e, + 0x76, 0x6f, 0x79, 0x53, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x68, + 0x74, 0x74, 0x70, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0b, 0x68, 0x74, 0x74, 0x70, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x73, 0x12, 0x1a, 0x0a, + 0x08, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x6e, 0x65, 0x74, + 0x77, 0x6f, 0x72, 0x6b, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, + 0x73, 0x2a, 0x3a, 0x0a, 0x09, 0x4a, 0x6f, 0x62, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, + 0x0a, 0x0e, 0x75, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x73, 0x75, 0x63, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x10, + 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x66, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x10, 0x02, 0x2a, 0x4c, 0x0a, + 0x0c, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x0b, 0x0a, + 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x4c, + 0x4c, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, + 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x43, 0x4d, 0x50, 0x10, 0x04, 0x12, + 0x0a, 0x0a, 0x06, 0x43, 0x55, 0x53, 0x54, 0x4f, 0x4d, 0x10, 0x05, 0x2a, 0x20, 0x0a, 0x0d, 0x52, + 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x06, 0x0a, 0x02, + 0x49, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x2a, 0x22, 0x0a, + 0x0a, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x41, + 0x43, 0x43, 0x45, 0x50, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x52, 0x4f, 0x50, 0x10, + 0x01, 0x2a, 0x63, 0x0a, 0x0e, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, + 0x63, 0x6f, 0x6c, 0x12, 0x0f, 0x0a, 0x0b, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x48, 0x54, + 0x54, 0x50, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x48, + 0x54, 0x54, 0x50, 0x53, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, + 0x5f, 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, 0x0e, 0x0a, 0x0a, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, + 0x5f, 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, 0x0e, 0x0a, 0x0a, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, + 0x5f, 0x54, 0x4c, 0x53, 0x10, 0x04, 0x2a, 0x54, 0x0a, 0x14, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, + 0x61, 0x72, 0x65, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x13, + 0x0a, 0x0f, 0x54, 0x50, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x42, 0x55, 0x49, 0x4c, 0x54, 0x49, + 0x4e, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x54, 0x50, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x45, + 0x58, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x54, 0x50, 0x5f, + 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x45, 0x4e, 0x56, 0x4f, 0x59, 0x10, 0x02, 0x2a, 0x59, 0x0a, 0x16, + 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79, + 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x13, 0x0a, 0x0f, 0x54, 0x50, 0x5f, 0x41, 0x43, 0x54, + 0x49, 0x4f, 0x4e, 0x5f, 0x41, 0x4c, 0x4c, 0x4f, 0x57, 0x10, 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x54, + 0x50, 0x5f, 0x41, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x10, 0x01, + 0x12, 0x15, 0x0a, 0x11, 0x54, 0x50, 0x5f, 0x41, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x49, 0x4e, + 0x53, 0x50, 0x45, 0x43, 0x54, 0x10, 0x02, 0x2a, 0xa1, 0x01, 0x0a, 0x18, 0x54, 0x72, 0x61, 0x6e, + 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x50, 0x72, 0x6f, 0x74, + 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x10, 0x0a, 0x0c, 0x54, 0x50, 0x5f, 0x50, 0x52, 0x4f, 0x54, 0x4f, + 0x5f, 0x41, 0x4c, 0x4c, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x54, 0x50, 0x5f, 0x50, 0x52, 0x4f, + 0x54, 0x4f, 0x5f, 0x48, 0x54, 0x54, 0x50, 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x54, 0x50, 0x5f, + 0x50, 0x52, 0x4f, 0x54, 0x4f, 0x5f, 0x48, 0x54, 0x54, 0x50, 0x53, 0x10, 0x02, 0x12, 0x0f, 0x0a, + 0x0b, 0x54, 0x50, 0x5f, 0x50, 0x52, 0x4f, 0x54, 0x4f, 0x5f, 0x48, 0x32, 0x10, 0x03, 0x12, 0x0f, + 0x0a, 0x0b, 0x54, 0x50, 0x5f, 0x50, 0x52, 0x4f, 0x54, 0x4f, 0x5f, 0x48, 0x33, 0x10, 0x04, 0x12, + 0x16, 0x0a, 0x12, 0x54, 0x50, 0x5f, 0x50, 0x52, 0x4f, 0x54, 0x4f, 0x5f, 0x57, 0x45, 0x42, 0x53, + 0x4f, 0x43, 0x4b, 0x45, 0x54, 0x10, 0x05, 0x12, 0x12, 0x0a, 0x0e, 0x54, 0x50, 0x5f, 0x50, 0x52, + 0x4f, 0x54, 0x4f, 0x5f, 0x4f, 0x54, 0x48, 0x45, 0x52, 0x10, 0x06, 0x32, 0xfd, 0x06, 0x0a, 0x11, + 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, + 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e, 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, - 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x58, - 0x0a, 0x18, 0x47, 0x65, 0x74, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, - 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, - 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x08, 0x53, 0x79, 0x6e, 0x63, - 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, - 0x67, 0x65, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x3b, 0x0a, 0x06, 0x4c, 0x6f, 0x67, 0x6f, 0x75, - 0x74, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, + 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, 0x01, + 0x12, 0x42, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, + 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x09, 0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, + 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, + 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65, 0x74, + 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x50, 0x4b, 0x43, 0x45, + 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, + 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, - 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x22, 0x00, 0x12, 0x47, 0x0a, 0x03, 0x4a, 0x6f, 0x62, 0x12, 0x1c, 0x2e, 0x6d, 0x61, + 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, + 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, + 0x3d, 0x0a, 0x08, 0x53, 0x79, 0x6e, 0x63, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, - 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, - 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x12, 0x4c, 0x0a, - 0x0c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x12, 0x1c, 0x2e, + 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x3b, + 0x0a, 0x06, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x47, 0x0a, 0x03, 0x4a, + 0x6f, 0x62, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, + 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, + 0x28, 0x01, 0x30, 0x01, 0x12, 0x4c, 0x0a, 0x0c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x78, + 0x70, 0x6f, 0x73, 0x65, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0b, 0x52, 0x65, 0x6e, 0x65, 0x77, 0x45, 0x78, 0x70, 0x6f, 0x73, + 0x65, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, + 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, + 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, + 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, + 0x4a, 0x0a, 0x0a, 0x53, 0x74, 0x6f, 0x70, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, - 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0b, 0x52, - 0x65, 0x6e, 0x65, 0x77, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, - 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x4a, 0x0a, 0x0a, 0x53, 0x74, 0x6f, 0x70, - 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, - 0x67, 0x65, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -5145,166 +5818,181 @@ func file_management_proto_rawDescGZIP() []byte { return file_management_proto_rawDescData } -var file_management_proto_enumTypes = make([]protoimpl.EnumInfo, 7) -var file_management_proto_msgTypes = make([]protoimpl.MessageInfo, 55) +var file_management_proto_enumTypes = make([]protoimpl.EnumInfo, 10) +var file_management_proto_msgTypes = make([]protoimpl.MessageInfo, 59) var file_management_proto_goTypes = []interface{}{ (JobStatus)(0), // 0: management.JobStatus (RuleProtocol)(0), // 1: management.RuleProtocol (RuleDirection)(0), // 2: management.RuleDirection (RuleAction)(0), // 3: management.RuleAction (ExposeProtocol)(0), // 4: management.ExposeProtocol - (HostConfig_Protocol)(0), // 5: management.HostConfig.Protocol - (DeviceAuthorizationFlowProvider)(0), // 6: management.DeviceAuthorizationFlow.provider - (*EncryptedMessage)(nil), // 7: management.EncryptedMessage - (*JobRequest)(nil), // 8: management.JobRequest - (*JobResponse)(nil), // 9: management.JobResponse - (*BundleParameters)(nil), // 10: management.BundleParameters - (*BundleResult)(nil), // 11: management.BundleResult - (*SyncRequest)(nil), // 12: management.SyncRequest - (*SyncResponse)(nil), // 13: management.SyncResponse - (*SyncMetaRequest)(nil), // 14: management.SyncMetaRequest - (*LoginRequest)(nil), // 15: management.LoginRequest - (*PeerKeys)(nil), // 16: management.PeerKeys - (*Environment)(nil), // 17: management.Environment - (*File)(nil), // 18: management.File - (*Flags)(nil), // 19: management.Flags - (*PeerSystemMeta)(nil), // 20: management.PeerSystemMeta - (*LoginResponse)(nil), // 21: management.LoginResponse - (*ServerKeyResponse)(nil), // 22: management.ServerKeyResponse - (*Empty)(nil), // 23: management.Empty - (*NetbirdConfig)(nil), // 24: management.NetbirdConfig - (*HostConfig)(nil), // 25: management.HostConfig - (*RelayConfig)(nil), // 26: management.RelayConfig - (*FlowConfig)(nil), // 27: management.FlowConfig - (*JWTConfig)(nil), // 28: management.JWTConfig - (*ProtectedHostConfig)(nil), // 29: management.ProtectedHostConfig - (*PeerConfig)(nil), // 30: management.PeerConfig - (*AutoUpdateSettings)(nil), // 31: management.AutoUpdateSettings - (*NetworkMap)(nil), // 32: management.NetworkMap - (*SSHAuth)(nil), // 33: management.SSHAuth - (*MachineUserIndexes)(nil), // 34: management.MachineUserIndexes - (*RemotePeerConfig)(nil), // 35: management.RemotePeerConfig - (*SSHConfig)(nil), // 36: management.SSHConfig - (*DeviceAuthorizationFlowRequest)(nil), // 37: management.DeviceAuthorizationFlowRequest - (*DeviceAuthorizationFlow)(nil), // 38: management.DeviceAuthorizationFlow - (*PKCEAuthorizationFlowRequest)(nil), // 39: management.PKCEAuthorizationFlowRequest - (*PKCEAuthorizationFlow)(nil), // 40: management.PKCEAuthorizationFlow - (*ProviderConfig)(nil), // 41: management.ProviderConfig - (*Route)(nil), // 42: management.Route - (*DNSConfig)(nil), // 43: management.DNSConfig - (*CustomZone)(nil), // 44: management.CustomZone - (*SimpleRecord)(nil), // 45: management.SimpleRecord - (*NameServerGroup)(nil), // 46: management.NameServerGroup - (*NameServer)(nil), // 47: management.NameServer - (*FirewallRule)(nil), // 48: management.FirewallRule - (*NetworkAddress)(nil), // 49: management.NetworkAddress - (*Checks)(nil), // 50: management.Checks - (*PortInfo)(nil), // 51: management.PortInfo - (*RouteFirewallRule)(nil), // 52: management.RouteFirewallRule - (*ForwardingRule)(nil), // 53: management.ForwardingRule - (*ExposeServiceRequest)(nil), // 54: management.ExposeServiceRequest - (*ExposeServiceResponse)(nil), // 55: management.ExposeServiceResponse - (*RenewExposeRequest)(nil), // 56: management.RenewExposeRequest - (*RenewExposeResponse)(nil), // 57: management.RenewExposeResponse - (*StopExposeRequest)(nil), // 58: management.StopExposeRequest - (*StopExposeResponse)(nil), // 59: management.StopExposeResponse - nil, // 60: management.SSHAuth.MachineUsersEntry - (*PortInfo_Range)(nil), // 61: management.PortInfo.Range - (*timestamppb.Timestamp)(nil), // 62: google.protobuf.Timestamp - (*durationpb.Duration)(nil), // 63: google.protobuf.Duration + (TransparentProxyMode)(0), // 5: management.TransparentProxyMode + (TransparentProxyAction)(0), // 6: management.TransparentProxyAction + (TransparentProxyProtocol)(0), // 7: management.TransparentProxyProtocol + (HostConfig_Protocol)(0), // 8: management.HostConfig.Protocol + (DeviceAuthorizationFlowProvider)(0), // 9: management.DeviceAuthorizationFlow.provider + (*EncryptedMessage)(nil), // 10: management.EncryptedMessage + (*JobRequest)(nil), // 11: management.JobRequest + (*JobResponse)(nil), // 12: management.JobResponse + (*BundleParameters)(nil), // 13: management.BundleParameters + (*BundleResult)(nil), // 14: management.BundleResult + (*SyncRequest)(nil), // 15: management.SyncRequest + (*SyncResponse)(nil), // 16: management.SyncResponse + (*SyncMetaRequest)(nil), // 17: management.SyncMetaRequest + (*LoginRequest)(nil), // 18: management.LoginRequest + (*PeerKeys)(nil), // 19: management.PeerKeys + (*Environment)(nil), // 20: management.Environment + (*File)(nil), // 21: management.File + (*Flags)(nil), // 22: management.Flags + (*PeerSystemMeta)(nil), // 23: management.PeerSystemMeta + (*LoginResponse)(nil), // 24: management.LoginResponse + (*ServerKeyResponse)(nil), // 25: management.ServerKeyResponse + (*Empty)(nil), // 26: management.Empty + (*NetbirdConfig)(nil), // 27: management.NetbirdConfig + (*HostConfig)(nil), // 28: management.HostConfig + (*RelayConfig)(nil), // 29: management.RelayConfig + (*FlowConfig)(nil), // 30: management.FlowConfig + (*JWTConfig)(nil), // 31: management.JWTConfig + (*ProtectedHostConfig)(nil), // 32: management.ProtectedHostConfig + (*PeerConfig)(nil), // 33: management.PeerConfig + (*AutoUpdateSettings)(nil), // 34: management.AutoUpdateSettings + (*NetworkMap)(nil), // 35: management.NetworkMap + (*SSHAuth)(nil), // 36: management.SSHAuth + (*MachineUserIndexes)(nil), // 37: management.MachineUserIndexes + (*RemotePeerConfig)(nil), // 38: management.RemotePeerConfig + (*SSHConfig)(nil), // 39: management.SSHConfig + (*DeviceAuthorizationFlowRequest)(nil), // 40: management.DeviceAuthorizationFlowRequest + (*DeviceAuthorizationFlow)(nil), // 41: management.DeviceAuthorizationFlow + (*PKCEAuthorizationFlowRequest)(nil), // 42: management.PKCEAuthorizationFlowRequest + (*PKCEAuthorizationFlow)(nil), // 43: management.PKCEAuthorizationFlow + (*ProviderConfig)(nil), // 44: management.ProviderConfig + (*Route)(nil), // 45: management.Route + (*DNSConfig)(nil), // 46: management.DNSConfig + (*CustomZone)(nil), // 47: management.CustomZone + (*SimpleRecord)(nil), // 48: management.SimpleRecord + (*NameServerGroup)(nil), // 49: management.NameServerGroup + (*NameServer)(nil), // 50: management.NameServer + (*FirewallRule)(nil), // 51: management.FirewallRule + (*NetworkAddress)(nil), // 52: management.NetworkAddress + (*Checks)(nil), // 53: management.Checks + (*PortInfo)(nil), // 54: management.PortInfo + (*RouteFirewallRule)(nil), // 55: management.RouteFirewallRule + (*ForwardingRule)(nil), // 56: management.ForwardingRule + (*ExposeServiceRequest)(nil), // 57: management.ExposeServiceRequest + (*ExposeServiceResponse)(nil), // 58: management.ExposeServiceResponse + (*RenewExposeRequest)(nil), // 59: management.RenewExposeRequest + (*RenewExposeResponse)(nil), // 60: management.RenewExposeResponse + (*StopExposeRequest)(nil), // 61: management.StopExposeRequest + (*StopExposeResponse)(nil), // 62: management.StopExposeResponse + (*TransparentProxyConfig)(nil), // 63: management.TransparentProxyConfig + (*TransparentProxyRule)(nil), // 64: management.TransparentProxyRule + (*TransparentProxyICAPConfig)(nil), // 65: management.TransparentProxyICAPConfig + (*TransparentProxyEnvoySnippets)(nil), // 66: management.TransparentProxyEnvoySnippets + nil, // 67: management.SSHAuth.MachineUsersEntry + (*PortInfo_Range)(nil), // 68: management.PortInfo.Range + (*timestamppb.Timestamp)(nil), // 69: google.protobuf.Timestamp + (*durationpb.Duration)(nil), // 70: google.protobuf.Duration } var file_management_proto_depIdxs = []int32{ - 10, // 0: management.JobRequest.bundle:type_name -> management.BundleParameters + 13, // 0: management.JobRequest.bundle:type_name -> management.BundleParameters 0, // 1: management.JobResponse.status:type_name -> management.JobStatus - 11, // 2: management.JobResponse.bundle:type_name -> management.BundleResult - 20, // 3: management.SyncRequest.meta:type_name -> management.PeerSystemMeta - 24, // 4: management.SyncResponse.netbirdConfig:type_name -> management.NetbirdConfig - 30, // 5: management.SyncResponse.peerConfig:type_name -> management.PeerConfig - 35, // 6: management.SyncResponse.remotePeers:type_name -> management.RemotePeerConfig - 32, // 7: management.SyncResponse.NetworkMap:type_name -> management.NetworkMap - 50, // 8: management.SyncResponse.Checks:type_name -> management.Checks - 20, // 9: management.SyncMetaRequest.meta:type_name -> management.PeerSystemMeta - 20, // 10: management.LoginRequest.meta:type_name -> management.PeerSystemMeta - 16, // 11: management.LoginRequest.peerKeys:type_name -> management.PeerKeys - 49, // 12: management.PeerSystemMeta.networkAddresses:type_name -> management.NetworkAddress - 17, // 13: management.PeerSystemMeta.environment:type_name -> management.Environment - 18, // 14: management.PeerSystemMeta.files:type_name -> management.File - 19, // 15: management.PeerSystemMeta.flags:type_name -> management.Flags - 24, // 16: management.LoginResponse.netbirdConfig:type_name -> management.NetbirdConfig - 30, // 17: management.LoginResponse.peerConfig:type_name -> management.PeerConfig - 50, // 18: management.LoginResponse.Checks:type_name -> management.Checks - 62, // 19: management.ServerKeyResponse.expiresAt:type_name -> google.protobuf.Timestamp - 25, // 20: management.NetbirdConfig.stuns:type_name -> management.HostConfig - 29, // 21: management.NetbirdConfig.turns:type_name -> management.ProtectedHostConfig - 25, // 22: management.NetbirdConfig.signal:type_name -> management.HostConfig - 26, // 23: management.NetbirdConfig.relay:type_name -> management.RelayConfig - 27, // 24: management.NetbirdConfig.flow:type_name -> management.FlowConfig - 5, // 25: management.HostConfig.protocol:type_name -> management.HostConfig.Protocol - 63, // 26: management.FlowConfig.interval:type_name -> google.protobuf.Duration - 25, // 27: management.ProtectedHostConfig.hostConfig:type_name -> management.HostConfig - 36, // 28: management.PeerConfig.sshConfig:type_name -> management.SSHConfig - 31, // 29: management.PeerConfig.autoUpdate:type_name -> management.AutoUpdateSettings - 30, // 30: management.NetworkMap.peerConfig:type_name -> management.PeerConfig - 35, // 31: management.NetworkMap.remotePeers:type_name -> management.RemotePeerConfig - 42, // 32: management.NetworkMap.Routes:type_name -> management.Route - 43, // 33: management.NetworkMap.DNSConfig:type_name -> management.DNSConfig - 35, // 34: management.NetworkMap.offlinePeers:type_name -> management.RemotePeerConfig - 48, // 35: management.NetworkMap.FirewallRules:type_name -> management.FirewallRule - 52, // 36: management.NetworkMap.routesFirewallRules:type_name -> management.RouteFirewallRule - 53, // 37: management.NetworkMap.forwardingRules:type_name -> management.ForwardingRule - 33, // 38: management.NetworkMap.sshAuth:type_name -> management.SSHAuth - 60, // 39: management.SSHAuth.machine_users:type_name -> management.SSHAuth.MachineUsersEntry - 36, // 40: management.RemotePeerConfig.sshConfig:type_name -> management.SSHConfig - 28, // 41: management.SSHConfig.jwtConfig:type_name -> management.JWTConfig - 6, // 42: management.DeviceAuthorizationFlow.Provider:type_name -> management.DeviceAuthorizationFlow.provider - 41, // 43: management.DeviceAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig - 41, // 44: management.PKCEAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig - 46, // 45: management.DNSConfig.NameServerGroups:type_name -> management.NameServerGroup - 44, // 46: management.DNSConfig.CustomZones:type_name -> management.CustomZone - 45, // 47: management.CustomZone.Records:type_name -> management.SimpleRecord - 47, // 48: management.NameServerGroup.NameServers:type_name -> management.NameServer - 2, // 49: management.FirewallRule.Direction:type_name -> management.RuleDirection - 3, // 50: management.FirewallRule.Action:type_name -> management.RuleAction - 1, // 51: management.FirewallRule.Protocol:type_name -> management.RuleProtocol - 51, // 52: management.FirewallRule.PortInfo:type_name -> management.PortInfo - 61, // 53: management.PortInfo.range:type_name -> management.PortInfo.Range - 3, // 54: management.RouteFirewallRule.action:type_name -> management.RuleAction - 1, // 55: management.RouteFirewallRule.protocol:type_name -> management.RuleProtocol - 51, // 56: management.RouteFirewallRule.portInfo:type_name -> management.PortInfo - 1, // 57: management.ForwardingRule.protocol:type_name -> management.RuleProtocol - 51, // 58: management.ForwardingRule.destinationPort:type_name -> management.PortInfo - 51, // 59: management.ForwardingRule.translatedPort:type_name -> management.PortInfo - 4, // 60: management.ExposeServiceRequest.protocol:type_name -> management.ExposeProtocol - 34, // 61: management.SSHAuth.MachineUsersEntry.value:type_name -> management.MachineUserIndexes - 7, // 62: management.ManagementService.Login:input_type -> management.EncryptedMessage - 7, // 63: management.ManagementService.Sync:input_type -> management.EncryptedMessage - 23, // 64: management.ManagementService.GetServerKey:input_type -> management.Empty - 23, // 65: management.ManagementService.isHealthy:input_type -> management.Empty - 7, // 66: management.ManagementService.GetDeviceAuthorizationFlow:input_type -> management.EncryptedMessage - 7, // 67: management.ManagementService.GetPKCEAuthorizationFlow:input_type -> management.EncryptedMessage - 7, // 68: management.ManagementService.SyncMeta:input_type -> management.EncryptedMessage - 7, // 69: management.ManagementService.Logout:input_type -> management.EncryptedMessage - 7, // 70: management.ManagementService.Job:input_type -> management.EncryptedMessage - 7, // 71: management.ManagementService.CreateExpose:input_type -> management.EncryptedMessage - 7, // 72: management.ManagementService.RenewExpose:input_type -> management.EncryptedMessage - 7, // 73: management.ManagementService.StopExpose:input_type -> management.EncryptedMessage - 7, // 74: management.ManagementService.Login:output_type -> management.EncryptedMessage - 7, // 75: management.ManagementService.Sync:output_type -> management.EncryptedMessage - 22, // 76: management.ManagementService.GetServerKey:output_type -> management.ServerKeyResponse - 23, // 77: management.ManagementService.isHealthy:output_type -> management.Empty - 7, // 78: management.ManagementService.GetDeviceAuthorizationFlow:output_type -> management.EncryptedMessage - 7, // 79: management.ManagementService.GetPKCEAuthorizationFlow:output_type -> management.EncryptedMessage - 23, // 80: management.ManagementService.SyncMeta:output_type -> management.Empty - 23, // 81: management.ManagementService.Logout:output_type -> management.Empty - 7, // 82: management.ManagementService.Job:output_type -> management.EncryptedMessage - 7, // 83: management.ManagementService.CreateExpose:output_type -> management.EncryptedMessage - 7, // 84: management.ManagementService.RenewExpose:output_type -> management.EncryptedMessage - 7, // 85: management.ManagementService.StopExpose:output_type -> management.EncryptedMessage - 74, // [74:86] is the sub-list for method output_type - 62, // [62:74] is the sub-list for method input_type - 62, // [62:62] is the sub-list for extension type_name - 62, // [62:62] is the sub-list for extension extendee - 0, // [0:62] is the sub-list for field type_name + 14, // 2: management.JobResponse.bundle:type_name -> management.BundleResult + 23, // 3: management.SyncRequest.meta:type_name -> management.PeerSystemMeta + 27, // 4: management.SyncResponse.netbirdConfig:type_name -> management.NetbirdConfig + 33, // 5: management.SyncResponse.peerConfig:type_name -> management.PeerConfig + 38, // 6: management.SyncResponse.remotePeers:type_name -> management.RemotePeerConfig + 35, // 7: management.SyncResponse.NetworkMap:type_name -> management.NetworkMap + 53, // 8: management.SyncResponse.Checks:type_name -> management.Checks + 23, // 9: management.SyncMetaRequest.meta:type_name -> management.PeerSystemMeta + 23, // 10: management.LoginRequest.meta:type_name -> management.PeerSystemMeta + 19, // 11: management.LoginRequest.peerKeys:type_name -> management.PeerKeys + 52, // 12: management.PeerSystemMeta.networkAddresses:type_name -> management.NetworkAddress + 20, // 13: management.PeerSystemMeta.environment:type_name -> management.Environment + 21, // 14: management.PeerSystemMeta.files:type_name -> management.File + 22, // 15: management.PeerSystemMeta.flags:type_name -> management.Flags + 27, // 16: management.LoginResponse.netbirdConfig:type_name -> management.NetbirdConfig + 33, // 17: management.LoginResponse.peerConfig:type_name -> management.PeerConfig + 53, // 18: management.LoginResponse.Checks:type_name -> management.Checks + 69, // 19: management.ServerKeyResponse.expiresAt:type_name -> google.protobuf.Timestamp + 28, // 20: management.NetbirdConfig.stuns:type_name -> management.HostConfig + 32, // 21: management.NetbirdConfig.turns:type_name -> management.ProtectedHostConfig + 28, // 22: management.NetbirdConfig.signal:type_name -> management.HostConfig + 29, // 23: management.NetbirdConfig.relay:type_name -> management.RelayConfig + 30, // 24: management.NetbirdConfig.flow:type_name -> management.FlowConfig + 8, // 25: management.HostConfig.protocol:type_name -> management.HostConfig.Protocol + 70, // 26: management.FlowConfig.interval:type_name -> google.protobuf.Duration + 28, // 27: management.ProtectedHostConfig.hostConfig:type_name -> management.HostConfig + 39, // 28: management.PeerConfig.sshConfig:type_name -> management.SSHConfig + 34, // 29: management.PeerConfig.autoUpdate:type_name -> management.AutoUpdateSettings + 33, // 30: management.NetworkMap.peerConfig:type_name -> management.PeerConfig + 38, // 31: management.NetworkMap.remotePeers:type_name -> management.RemotePeerConfig + 45, // 32: management.NetworkMap.Routes:type_name -> management.Route + 46, // 33: management.NetworkMap.DNSConfig:type_name -> management.DNSConfig + 38, // 34: management.NetworkMap.offlinePeers:type_name -> management.RemotePeerConfig + 51, // 35: management.NetworkMap.FirewallRules:type_name -> management.FirewallRule + 55, // 36: management.NetworkMap.routesFirewallRules:type_name -> management.RouteFirewallRule + 56, // 37: management.NetworkMap.forwardingRules:type_name -> management.ForwardingRule + 36, // 38: management.NetworkMap.sshAuth:type_name -> management.SSHAuth + 63, // 39: management.NetworkMap.transparentProxyConfig:type_name -> management.TransparentProxyConfig + 67, // 40: management.SSHAuth.machine_users:type_name -> management.SSHAuth.MachineUsersEntry + 39, // 41: management.RemotePeerConfig.sshConfig:type_name -> management.SSHConfig + 31, // 42: management.SSHConfig.jwtConfig:type_name -> management.JWTConfig + 9, // 43: management.DeviceAuthorizationFlow.Provider:type_name -> management.DeviceAuthorizationFlow.provider + 44, // 44: management.DeviceAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig + 44, // 45: management.PKCEAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig + 49, // 46: management.DNSConfig.NameServerGroups:type_name -> management.NameServerGroup + 47, // 47: management.DNSConfig.CustomZones:type_name -> management.CustomZone + 48, // 48: management.CustomZone.Records:type_name -> management.SimpleRecord + 50, // 49: management.NameServerGroup.NameServers:type_name -> management.NameServer + 2, // 50: management.FirewallRule.Direction:type_name -> management.RuleDirection + 3, // 51: management.FirewallRule.Action:type_name -> management.RuleAction + 1, // 52: management.FirewallRule.Protocol:type_name -> management.RuleProtocol + 54, // 53: management.FirewallRule.PortInfo:type_name -> management.PortInfo + 68, // 54: management.PortInfo.range:type_name -> management.PortInfo.Range + 3, // 55: management.RouteFirewallRule.action:type_name -> management.RuleAction + 1, // 56: management.RouteFirewallRule.protocol:type_name -> management.RuleProtocol + 54, // 57: management.RouteFirewallRule.portInfo:type_name -> management.PortInfo + 1, // 58: management.ForwardingRule.protocol:type_name -> management.RuleProtocol + 54, // 59: management.ForwardingRule.destinationPort:type_name -> management.PortInfo + 54, // 60: management.ForwardingRule.translatedPort:type_name -> management.PortInfo + 4, // 61: management.ExposeServiceRequest.protocol:type_name -> management.ExposeProtocol + 5, // 62: management.TransparentProxyConfig.mode:type_name -> management.TransparentProxyMode + 6, // 63: management.TransparentProxyConfig.defaultAction:type_name -> management.TransparentProxyAction + 64, // 64: management.TransparentProxyConfig.rules:type_name -> management.TransparentProxyRule + 65, // 65: management.TransparentProxyConfig.icap:type_name -> management.TransparentProxyICAPConfig + 66, // 66: management.TransparentProxyConfig.envoySnippets:type_name -> management.TransparentProxyEnvoySnippets + 6, // 67: management.TransparentProxyRule.action:type_name -> management.TransparentProxyAction + 7, // 68: management.TransparentProxyRule.protocols:type_name -> management.TransparentProxyProtocol + 37, // 69: management.SSHAuth.MachineUsersEntry.value:type_name -> management.MachineUserIndexes + 10, // 70: management.ManagementService.Login:input_type -> management.EncryptedMessage + 10, // 71: management.ManagementService.Sync:input_type -> management.EncryptedMessage + 26, // 72: management.ManagementService.GetServerKey:input_type -> management.Empty + 26, // 73: management.ManagementService.isHealthy:input_type -> management.Empty + 10, // 74: management.ManagementService.GetDeviceAuthorizationFlow:input_type -> management.EncryptedMessage + 10, // 75: management.ManagementService.GetPKCEAuthorizationFlow:input_type -> management.EncryptedMessage + 10, // 76: management.ManagementService.SyncMeta:input_type -> management.EncryptedMessage + 10, // 77: management.ManagementService.Logout:input_type -> management.EncryptedMessage + 10, // 78: management.ManagementService.Job:input_type -> management.EncryptedMessage + 10, // 79: management.ManagementService.CreateExpose:input_type -> management.EncryptedMessage + 10, // 80: management.ManagementService.RenewExpose:input_type -> management.EncryptedMessage + 10, // 81: management.ManagementService.StopExpose:input_type -> management.EncryptedMessage + 10, // 82: management.ManagementService.Login:output_type -> management.EncryptedMessage + 10, // 83: management.ManagementService.Sync:output_type -> management.EncryptedMessage + 25, // 84: management.ManagementService.GetServerKey:output_type -> management.ServerKeyResponse + 26, // 85: management.ManagementService.isHealthy:output_type -> management.Empty + 10, // 86: management.ManagementService.GetDeviceAuthorizationFlow:output_type -> management.EncryptedMessage + 10, // 87: management.ManagementService.GetPKCEAuthorizationFlow:output_type -> management.EncryptedMessage + 26, // 88: management.ManagementService.SyncMeta:output_type -> management.Empty + 26, // 89: management.ManagementService.Logout:output_type -> management.Empty + 10, // 90: management.ManagementService.Job:output_type -> management.EncryptedMessage + 10, // 91: management.ManagementService.CreateExpose:output_type -> management.EncryptedMessage + 10, // 92: management.ManagementService.RenewExpose:output_type -> management.EncryptedMessage + 10, // 93: management.ManagementService.StopExpose:output_type -> management.EncryptedMessage + 82, // [82:94] is the sub-list for method output_type + 70, // [70:82] is the sub-list for method input_type + 70, // [70:70] is the sub-list for extension type_name + 70, // [70:70] is the sub-list for extension extendee + 0, // [0:70] is the sub-list for field type_name } func init() { file_management_proto_init() } @@ -5949,7 +6637,55 @@ func file_management_proto_init() { return nil } } + file_management_proto_msgTypes[53].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TransparentProxyConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } file_management_proto_msgTypes[54].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TransparentProxyRule); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_management_proto_msgTypes[55].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TransparentProxyICAPConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_management_proto_msgTypes[56].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TransparentProxyEnvoySnippets); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_management_proto_msgTypes[58].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*PortInfo_Range); i { case 0: return &v.state @@ -5977,8 +6713,8 @@ func file_management_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_management_proto_rawDesc, - NumEnums: 7, - NumMessages: 55, + NumEnums: 10, + NumMessages: 59, NumExtensions: 0, NumServices: 1, }, diff --git a/shared/management/proto/management.proto b/shared/management/proto/management.proto index 70a530679..3fae2a5ee 100644 --- a/shared/management/proto/management.proto +++ b/shared/management/proto/management.proto @@ -387,6 +387,9 @@ message NetworkMap { // SSHAuth represents SSH authorization configuration SSHAuth sshAuth = 13; + + // TransparentProxyConfig represents transparent proxy configuration for this peer + TransparentProxyConfig transparentProxyConfig = 14; } message SSHAuth { @@ -684,3 +687,90 @@ message StopExposeRequest { } message StopExposeResponse {} + +// TransparentProxyConfig configures the transparent forward proxy on a routing peer. +message TransparentProxyConfig { + bool enabled = 1; + TransparentProxyMode mode = 2; + // External proxy URL for MODE_EXTERNAL (http:// or socks5://) + string externalProxyUrl = 3; + TransparentProxyAction defaultAction = 4; + + // L3/L4 interception: which traffic gets redirected to the proxy. + // Admin decides: activate for these users/subnets on these ports. + // Used for both kernel TPROXY rules and userspace forwarder source filtering. + repeated string redirectSources = 5; + // Destination ports to intercept. Empty means all ports. + repeated uint32 redirectPorts = 6; + + // L7 inspection rules: what the proxy does with intercepted traffic. + repeated TransparentProxyRule rules = 7; + + TransparentProxyICAPConfig icap = 8; + // MITM CA certificate in PEM format + bytes caCertPem = 9; + // MITM CA private key in PEM format + bytes caKeyPem = 10; + // TPROXY listen port for kernel mode. 0 means auto-assign. + uint32 listenPort = 11; + + // Envoy sidecar configuration (MODE_ENVOY only) + string envoyBinaryPath = 12; + uint32 envoyAdminPort = 13; + TransparentProxyEnvoySnippets envoySnippets = 14; +} + +enum TransparentProxyMode { + TP_MODE_BUILTIN = 0; + TP_MODE_EXTERNAL = 1; + TP_MODE_ENVOY = 2; +} + +enum TransparentProxyAction { + TP_ACTION_ALLOW = 0; + TP_ACTION_BLOCK = 1; + TP_ACTION_INSPECT = 2; +} + +enum TransparentProxyProtocol { + TP_PROTO_ALL = 0; + TP_PROTO_HTTP = 1; + TP_PROTO_HTTPS = 2; + TP_PROTO_H2 = 3; + TP_PROTO_H3 = 4; + TP_PROTO_WEBSOCKET = 5; + TP_PROTO_OTHER = 6; +} + +// TransparentProxyRule is an L7 inspection rule evaluated by the proxy engine. +message TransparentProxyRule { + string id = 1; + // Domain patterns to match via SNI or Host header (e.g., *.example.com) + repeated string domains = 2; + // Destination CIDRs for optional L7 destination filtering + repeated string networks = 3; + // Destination ports for optional per-rule port filtering + repeated uint32 ports = 4; + TransparentProxyAction action = 5; + int32 priority = 6; + // Protocols to match. Empty means all protocols. + repeated TransparentProxyProtocol protocols = 7; + // URL path patterns to match (HTTP only, requires inspect for HTTPS). + // Supports prefix ("/api/"), exact ("/login"), and wildcard ("/admin/*"). + repeated string paths = 8; +} + +message TransparentProxyICAPConfig { + string reqmodUrl = 1; + string respmodUrl = 2; + int32 maxConnections = 3; +} + +message TransparentProxyEnvoySnippets { + // YAML injected into the HCM filter chain before the router filter. + string httpFilters = 1; + // YAML for additional upstream clusters referenced by filters. + string clusters = 2; + // YAML injected into the TLS filter chain before tcp_proxy (L4 filters). + string networkFilters = 3; +}