mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-14 17:39:12 +02:00
Merge remote-tracking branch 'origin/main' into loopback-wg-proxy
# Conflicts: # client/firewall/iptables/manager_linux.go # client/firewall/nftables/router_linux.go # client/firewall/uspfilter/filter.go
This commit is contained in:
+78
-61
@@ -59,7 +59,7 @@ import (
|
||||
"github.com/netbirdio/netbird/client/internal/syncstore"
|
||||
"github.com/netbirdio/netbird/client/internal/updater"
|
||||
"github.com/netbirdio/netbird/client/jobexec"
|
||||
"github.com/netbirdio/netbird/client/netstate"
|
||||
"github.com/netbirdio/netbird/client/netevents"
|
||||
cProto "github.com/netbirdio/netbird/client/proto"
|
||||
"github.com/netbirdio/netbird/client/system"
|
||||
nbdns "github.com/netbirdio/netbird/dns"
|
||||
@@ -137,6 +137,7 @@ type EngineConfig struct {
|
||||
RosenpassPermissive bool
|
||||
|
||||
ServerSSHAllowed bool
|
||||
RemoteJobsAllowed bool
|
||||
EnableSSHRoot *bool
|
||||
EnableSSHSFTP *bool
|
||||
EnableSSHLocalPortForwarding *bool
|
||||
@@ -182,9 +183,9 @@ type EngineServices struct {
|
||||
UpdateManager *updater.Manager
|
||||
ClientMetrics *metrics.ClientMetrics
|
||||
MetricsCtx context.Context
|
||||
// NetState gates the reconnection loops on OS-reported network
|
||||
// NetMgr gates the reconnection loops on OS-reported network
|
||||
// availability; nil disables gating.
|
||||
NetState *netstate.State
|
||||
NetMgr *netevents.Manager
|
||||
}
|
||||
|
||||
// Engine is a mechanism responsible for reacting on Signal and Management stream events and managing connections to the remote peers.
|
||||
@@ -208,9 +209,9 @@ type Engine struct {
|
||||
config *EngineConfig
|
||||
mobileDep MobileDependency
|
||||
|
||||
// netState gates the peer reconnection guards on OS-reported network
|
||||
// netMgr gates the peer reconnection guards on OS-reported network
|
||||
// availability; nil disables gating.
|
||||
netState *netstate.State
|
||||
netMgr *netevents.Manager
|
||||
|
||||
// STUNs is a list of STUN servers used by ICE
|
||||
STUNs []*stun.URI
|
||||
@@ -345,7 +346,7 @@ func NewEngine(
|
||||
syncMsgMux: &sync.Mutex{},
|
||||
config: config,
|
||||
mobileDep: mobileDep,
|
||||
netState: services.NetState,
|
||||
netMgr: services.NetMgr,
|
||||
STUNs: []*stun.URI{},
|
||||
TURNs: []*stun.URI{},
|
||||
networkSerial: 0,
|
||||
@@ -740,6 +741,11 @@ func (e *Engine) initFirewall() error {
|
||||
return fmt.Errorf("set firewall: %w", err)
|
||||
}
|
||||
|
||||
// TODO: the firewall backends dedup filter rules by content, so a
|
||||
// management route ACL with identical content would collapse onto the
|
||||
// untracked drop rules installed here, and a later management delete
|
||||
// could remove them. Needs backend refcounting or per-consumer key
|
||||
// namespacing.
|
||||
if e.config.BlockLANAccess {
|
||||
e.blockLanAccess()
|
||||
}
|
||||
@@ -752,14 +758,14 @@ func (e *Engine) initFirewall() error {
|
||||
port := firewallManager.Port{Values: []uint16{uint16(rosenpassPort)}}
|
||||
|
||||
// IPv4-only: rosenpass peers connect via AllowedIps[0] which is always v4.
|
||||
if _, err := e.firewall.AddPeerFiltering(
|
||||
if _, err := e.firewall.AddFilterRule(
|
||||
nil,
|
||||
net.IP{0, 0, 0, 0},
|
||||
[]netip.Prefix{netip.PrefixFrom(netip.IPv4Unspecified(), 0)},
|
||||
firewallManager.Network{},
|
||||
firewallManager.ProtocolUDP,
|
||||
nil,
|
||||
&port,
|
||||
firewallManager.ActionAccept,
|
||||
"",
|
||||
); err != nil {
|
||||
log.Errorf("failed to allow rosenpass interface traffic: %v", err)
|
||||
return nil
|
||||
@@ -792,7 +798,7 @@ func (e *Engine) blockLanAccess() {
|
||||
if network.Addr().Is6() {
|
||||
source = v6
|
||||
}
|
||||
if _, err := e.firewall.AddRouteFiltering(
|
||||
if _, err := e.firewall.AddFilterRule(
|
||||
nil,
|
||||
[]netip.Prefix{source},
|
||||
firewallManager.Network{Prefix: network},
|
||||
@@ -851,8 +857,7 @@ func (e *Engine) modifyPeers(peersUpdate []*mgmProto.RemotePeerConfig) error {
|
||||
}
|
||||
// third, add the peer connections again
|
||||
for _, p := range modified {
|
||||
err := e.addNewPeer(p)
|
||||
if err != nil {
|
||||
if err := e.addNewPeer(p); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -1239,6 +1244,7 @@ func (e *Engine) applyInfoFlags(info *system.Info) {
|
||||
e.config.EnableSSHLocalPortForwarding,
|
||||
e.config.EnableSSHRemotePortForwarding,
|
||||
e.config.DisableSSHAuth,
|
||||
&e.config.RemoteJobsAllowed,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1324,6 +1330,13 @@ func (e *Engine) receiveJobEvents() {
|
||||
ID: msg.ID,
|
||||
Status: mgmProto.JobStatus_failed,
|
||||
}
|
||||
// Remote jobs are an explicit opt-in. When not enabled on this
|
||||
// peer, every job is refused before any work is done.
|
||||
if !e.config.RemoteJobsAllowed {
|
||||
log.Warnf("refusing remote job: remote jobs are not enabled on this peer (enable with --allow-remote-jobs)")
|
||||
resp.Reason = []byte("remote jobs are not enabled on this peer")
|
||||
return &resp
|
||||
}
|
||||
switch params := msg.WorkloadParameters.(type) {
|
||||
case *mgmProto.JobRequest_Bundle:
|
||||
bundleResult, err := e.handleBundle(params.Bundle)
|
||||
@@ -1353,7 +1366,25 @@ func (e *Engine) receiveJobEvents() {
|
||||
}
|
||||
|
||||
func (e *Engine) handleBundle(params *mgmProto.BundleParameters) (*mgmProto.JobResponse_Bundle, error) {
|
||||
log.Infof("handle remote debug bundle request: %s", params.String())
|
||||
// The upload URL can carry a host, credentials, or query tokens, so it is
|
||||
// kept out of the info-level line; the full parameters stay available at
|
||||
// debug level for troubleshooting.
|
||||
log.Infof("handle remote debug bundle request: anonymize=%v anonymize_level=%q log_file_count=%d bundle_for=%v bundle_for_time=%d",
|
||||
params.GetAnonymize(), params.GetAnonymizeLevel(), params.GetLogFileCount(), params.GetBundleFor(), params.GetBundleForTime())
|
||||
log.Debugf("remote debug bundle request parameters: %s", params.String())
|
||||
|
||||
// Resolve the upload destination: an MDM override, when set, takes
|
||||
// precedence over the management-supplied URL. Both are validated the same
|
||||
// way; an empty result falls back to the default upload server downstream.
|
||||
uploadURL := params.GetUploadUrl()
|
||||
if override := e.config.ProfileConfig.DebugBundleUploadURL; override != "" {
|
||||
log.Infof("using MDM debug bundle upload URL override instead of the management-supplied value")
|
||||
uploadURL = override
|
||||
}
|
||||
if err := validateBundleUploadURL(uploadURL); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
syncResponse, err := e.GetLatestSyncResponse()
|
||||
if err != nil {
|
||||
log.Warnf("get latest sync response: %v", err)
|
||||
@@ -1381,7 +1412,7 @@ func (e *Engine) handleBundle(params *mgmProto.BundleParameters) (*mgmProto.JobR
|
||||
|
||||
waitFor := time.Duration(params.BundleForTime) * time.Minute
|
||||
|
||||
uploadKey, err := e.jobExecutor.BundleJob(e.ctx, bundleDeps, bundleJobParams, waitFor, e.config.ProfileConfig.ManagementURL.String())
|
||||
uploadKey, err := e.jobExecutor.BundleJob(e.ctx, bundleDeps, bundleJobParams, waitFor, e.config.ProfileConfig.ManagementURL.String(), uploadURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1394,6 +1425,16 @@ func (e *Engine) handleBundle(params *mgmProto.BundleParameters) (*mgmProto.JobR
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// validateBundleUploadURL sanity-checks a management-supplied upload URL for a
|
||||
// remote debug bundle job. It delegates to profilemanager.ValidateBundleUploadURL
|
||||
// so the executor and the MDM policy override share one definition of the rule
|
||||
// (empty accepted; otherwise a well-formed https URL with a host) and cannot
|
||||
// drift. The host is deliberately left unconstrained pending a decision on
|
||||
// management-directed uploads.
|
||||
func validateBundleUploadURL(raw string) error {
|
||||
return profilemanager.ValidateBundleUploadURL(raw)
|
||||
}
|
||||
|
||||
// receiveManagementEvents connects to the Management Service event stream to receive updates from the management service
|
||||
// E.g. when a new peer has been registered and we are allowed to connect to it.
|
||||
func (e *Engine) receiveManagementEvents() {
|
||||
@@ -1474,8 +1515,12 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := e.connMgr.UpdatedRemoteFeatureFlag(e.ctx, networkMap.GetPeerConfig().GetLazyConnectionEnabled()); err != nil {
|
||||
log.Errorf("failed to update lazy connection feature flag: %v", err)
|
||||
// Only update the flag when the sync carries a peer config; a nil peer config
|
||||
// (e.g. a partial update) must not reset the cached flag to false.
|
||||
if peerConfig := networkMap.GetPeerConfig(); peerConfig != nil {
|
||||
if err := e.connMgr.UpdatedRemoteFeatureFlag(e.ctx, peerConfig.GetLazyConnectionEnabled()); err != nil {
|
||||
log.Errorf("failed to update lazy connection feature flag: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if e.firewall != nil {
|
||||
@@ -1541,8 +1586,7 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error {
|
||||
|
||||
// Ingress forward rules
|
||||
done = e.phase("forward_rules")
|
||||
forwardingRules, err := e.updateForwardRules(networkMap.GetForwardingRules())
|
||||
if err != nil {
|
||||
if _, err := e.updateForwardRules(networkMap.GetForwardingRules()); err != nil {
|
||||
log.Errorf("failed to update forward rules, err: %v", err)
|
||||
}
|
||||
done()
|
||||
@@ -1560,8 +1604,7 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error {
|
||||
|
||||
// must set the exclude list after the peers are added. Without it the manager can not figure out the peers parameters from the store
|
||||
done = e.phase("lazy_exclude")
|
||||
excludedLazyPeers := e.toExcludedLazyPeers(forwardingRules, remotePeers)
|
||||
e.connMgr.SetExcludeList(e.ctx, excludedLazyPeers)
|
||||
e.connMgr.SetExcludeList(e.ctx, e.toExcludedLazyPeers(remotePeers))
|
||||
done()
|
||||
|
||||
e.networkSerial = serial
|
||||
@@ -1805,15 +1848,15 @@ func addrToString(addr netip.Addr) string {
|
||||
// addNewPeers adds peers that were not know before but arrived from the Management service with the update
|
||||
func (e *Engine) addNewPeers(peersUpdate []*mgmProto.RemotePeerConfig) error {
|
||||
for _, p := range peersUpdate {
|
||||
err := e.addNewPeer(p)
|
||||
if err != nil {
|
||||
if err := e.addNewPeer(p); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// addNewPeer add peer if connection doesn't exist
|
||||
// addNewPeer add peer if connection doesn't exist. A peer that is not lazy by
|
||||
// policy gets an always-active connection instead.
|
||||
func (e *Engine) addNewPeer(peerConfig *mgmProto.RemotePeerConfig) error {
|
||||
peerKey := peerConfig.GetWgPubKey()
|
||||
peerIPs := make([]netip.Prefix, 0, len(peerConfig.GetAllowedIps()))
|
||||
@@ -1848,7 +1891,8 @@ func (e *Engine) addNewPeer(peerConfig *mgmProto.RemotePeerConfig) error {
|
||||
log.Warnf("error adding peer %s to status recorder, got error: %v", peerKey, err)
|
||||
}
|
||||
|
||||
if exists := e.connMgr.AddPeerConn(e.ctx, peerKey, conn); exists {
|
||||
permanent := !e.connMgr.PeerLazyDefault(peerConfig.GetLazyState())
|
||||
if exists := e.connMgr.AddPeerConn(e.ctx, peerKey, conn, permanent); exists {
|
||||
conn.Close(false)
|
||||
return fmt.Errorf("peer already exists: %s", peerKey)
|
||||
}
|
||||
@@ -1881,8 +1925,8 @@ func (e *Engine) createPeerConn(pubKey string, allowedIPs []netip.Prefix, agentV
|
||||
Addr: e.getRosenpassAddr(),
|
||||
PermissiveMode: e.config.RosenpassPermissive,
|
||||
},
|
||||
ICEConfig: e.createICEConfig(),
|
||||
NetworkState: e.netState,
|
||||
ICEConfig: e.createICEConfig(),
|
||||
NetMgr: e.netMgr,
|
||||
}
|
||||
|
||||
serviceDependencies := peer.ServiceDependencies{
|
||||
@@ -2598,7 +2642,7 @@ func (e *Engine) updateForwardRules(rules []*mgmProto.ForwardingRule) ([]firewal
|
||||
var merr *multierror.Error
|
||||
forwardingRules := make([]firewallManager.ForwardRule, 0, len(rules))
|
||||
for _, rule := range rules {
|
||||
proto, err := convertToFirewallProtocol(rule.GetProtocol())
|
||||
proto, err := acl.ConvertToFirewallProtocol(rule.GetProtocol())
|
||||
if err != nil {
|
||||
merr = multierror.Append(merr, fmt.Errorf("failed to convert protocol '%s': %w", rule.GetProtocol(), err))
|
||||
continue
|
||||
@@ -2640,46 +2684,19 @@ func (e *Engine) updateForwardRules(rules []*mgmProto.ForwardingRule) ([]firewal
|
||||
return forwardingRules, nberrors.FormatErrorOrNil(merr)
|
||||
}
|
||||
|
||||
func (e *Engine) toExcludedLazyPeers(rules []firewallManager.ForwardRule, peers []*mgmProto.RemotePeerConfig) map[string]bool {
|
||||
// toExcludedLazyPeers returns the peers that must have an always-active
|
||||
// connection: those that are not lazy by policy (the per-peer lazy state or the
|
||||
// account flag, subject to the local override).
|
||||
func (e *Engine) toExcludedLazyPeers(peers []*mgmProto.RemotePeerConfig) map[string]bool {
|
||||
excludedPeers := make(map[string]bool)
|
||||
|
||||
// Ingress forward targets: inbound forwarded traffic is initiated remotely and
|
||||
// cannot wake a lazy connection, so the peer routing the target must stay
|
||||
// permanently connected. AllowedIPs are already parsed on the peer conn, so
|
||||
// reuse those typed prefixes instead of re-parsing the network map strings.
|
||||
for _, r := range rules {
|
||||
for _, p := range peers {
|
||||
if e.peerRoutesAddr(p, r.TranslatedAddress) {
|
||||
log.Infof("exclude forwarder peer from lazy connection: %s", p.GetWgPubKey())
|
||||
excludedPeers[p.GetWgPubKey()] = true
|
||||
}
|
||||
for _, p := range peers {
|
||||
if !e.connMgr.PeerLazyDefault(p.GetLazyState()) {
|
||||
excludedPeers[p.GetWgPubKey()] = true
|
||||
}
|
||||
}
|
||||
|
||||
return excludedPeers
|
||||
}
|
||||
|
||||
// peerRoutesAddr reports whether the peer is a router for addr, matched against
|
||||
// the peer's already-parsed AllowedIPs from the store (the same typed value the
|
||||
// lazy manager consumes) rather than re-parsing the network map strings.
|
||||
func (e *Engine) peerRoutesAddr(p *mgmProto.RemotePeerConfig, addr netip.Addr) bool {
|
||||
prefixes, ok := e.peerStore.AllowedIPs(p.GetWgPubKey())
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return prefixesContain(prefixes, addr)
|
||||
}
|
||||
|
||||
// prefixesContain reports whether addr falls within any of the prefixes.
|
||||
func prefixesContain(prefixes []netip.Prefix, addr netip.Addr) bool {
|
||||
for _, prefix := range prefixes {
|
||||
if prefix.Contains(addr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isChecksEqual checks if two slices of checks are equal.
|
||||
func isChecksEqual(checks1, checks2 []*mgmProto.Checks) bool {
|
||||
normalize := func(checks []*mgmProto.Checks) []string {
|
||||
|
||||
Reference in New Issue
Block a user