mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-09 16:31:29 +02:00
* MDM Android mobile wiring * Removes dead code * Removes static vars * Now we need to apply MDM in the GetConfig * You now need to explicitly call these around * Adds iOS wiring * Resolve merge conflicts from main - login.go: keep both new imports (mdm + nbnet + server) - ios/NetBirdSDK/client.go: additive struct-field merge (mdmLoader + stateMu/connectClient/config) - setconfig_mdm_test.go: adopt new withMDMPolicy(t, s, policy) signature; fix stray old-signature call in TestSetConfig_MDMAllow_ManagementURLPortNormalized * Convey MDM overlay config to Debug Bundle output Aligns to other clients OSes behavior * Solved conflict in client.go * Fixup helper withMDMPolicy -> configWithMDM * Fixup after merge * Resolve merge conflicts * [client] Move MDM enforcement logic into a shared Go layer (#7319) The mobile bridges only carried the policy fetcher, leaving every enforcement decision to the native apps: the desktop derived its UI restrictions in the Wails service layer, the daemon kept the conflict machinery in the server package, and both mobile bridges duplicated the JSON fetch adapter. Anything the native side had to reimplement was a place for iOS and Android to drift apart. Enforcement now lives in client/mdm and is consumed identically by all three platforms: - conflicts.go holds the value-aware conflict checks lifted out of the daemon, so the same normalization (canonical URLs, PSK sentinel echo) applies wherever a config change is validated. - restrictions.go derives the UI enforcement snapshot from a policy and renders it in the JSON shape the desktop frontend already consumes. The service-layer types become aliases, keeping one source of truth. - jsonloader.go replaces the adapter that was copy-pasted into both bridges. - changedetector.go moves change detection off the native side: the caller forwards the OS notification and asks whether the managed configuration actually changed, instead of diffing dictionaries itself. The mobile bridges gain the enforcement the daemon already had. The Preferences getters resolve managed keys from the policy, so a naive UI shows the enforced value; Commit rejects a staged change that diverges from a managed key; NewAuth resolves the managed management URL before persisting the config and overlays the policy on it, so a login can no longer run against a URL the policy forbids. Android's profile mutations fail closed when disableProfiles is set. NewAuth takes the fetcher as a required argument rather than keeping a policy-blind overload: the apps consume this code as a submodule, so a compile error at the bump is the point. The mobile PSK getter is replaced by a presence check — the key has no reason to cross the bridge, and not returning it means the native side needs no redaction sentinel of its own. * [client] Resolve the main merge conflicts in the MDM integration The merge commit was recorded with the conflict markers still in the tree. Resolve them so the branch builds again: - client/ios/NetBirdSDK: keep both the mdm and mobile imports, and keep the mdmLoader/mdmDetector fields next to main's stateMu documentation. - client/server/mdm.go: drop the conflict helpers main added locally, they already live in the client/mdm package on this branch, and keep the new checks main introduced (allowRemoteJobs, enableLocalMetrics, localMetricsAddress) as calls into the package-level helpers. - client/mdm/conflicts.go: add ConflictStringPtr, the presence-aware string check main needs for the optional localMetricsAddress field. - Port the two tests main added over the per-Server loader helper and the configWithMDM helper, both of which replaced the package-level policy injection this branch removed. * [client] Reject explicit empty PSK when MDM enforces a pre-shared key The SetConfig, Login and mobile Commit conflict checks collapsed the PSK to a plain string, so an explicit empty value was indistinguishable from an unset field and slipped past the MDM gate, clearing the persisted key. Carry the optional field as a pointer through ConflictStringPtr, treating only the redaction sentinel as a no-op echo. ConflictString had no other callers and is removed. * [client] Apply MDM overlay on the preloaded iOS config in Run Run only overlaid the MDM policy when the config was loaded from file, so the tvOS path fed by SetConfigFromJSON started with unmanaged settings. Apply the overlay after the config source is selected, as the other resolution sites already do. * [client] Gate non-active profile logout behind the MDM profiles switch The mobile ProfileManager let LogoutProfile clear credentials of any profile even when disableProfiles was enforced. Follow the daemon's validateProfileLogout semantics: logging out of the active profile is a plain logout and stays allowed, logging out of any other profile is profile management and is rejected under the policy. * [client] Resolve the managed management URL through the MDM overlay on mobile NewAuth on Android and iOS replaced the caller URL with the raw policy value before persisting, so a malformed managed URL failed config validation and blocked the login instead of being skipped with a warning like the overlay does. Preferences.GetManagementURL likewise echoed the raw policy string to the native UI even when the overlay had rejected it. Follow the daemon: persist the caller URL, overlay the policy on the resolved config, and report the overlaid ManagementURL as the effective value. * [client] Clean up MDM review leftovers Drop the unused ChangeDetector.Current, point the stale LoadPolicy comment references at Loader.Load, and move the profileEmail godoc back above its function. * [client] Check remote jobs and local metrics keys in the mobile MDM conflict gate MDMConflicts skipped allowRemoteJobs, enableLocalMetrics and localMetricsAddress even though the overlay applies all three and the daemon gate already checks them, so a mobile Commit could persist values diverging from the enforced policy. Align the list with the daemon. * [client] Silence the deprecated PreSharedKey lint in the login conflict test The legacy LoginRequest.PreSharedKey field is deliberately exercised by the test, matching the nolint already carried by the production path. * [client] Publish the mobile MDM loader and detector atomically SetMDMPolicyFetcher wrote the loader and change detector as two plain fields that Run, the OS-change callback and the restrictions getter read from other threads without synchronization. Hold both behind a single atomic pointer so a registration is published as one unit and readers always observe a matching loader and detector pair; Preferences gets the same treatment for its loader. Exported signatures are unchanged. * [client] Report the MDM-overlaid remote jobs value from mobile Preferences GetRemoteJobsAllowed returned the staged or persisted value even when the policy manages allowRemoteJobs, so the native settings UI could show a value the Commit gate would reject. Resolve it through the overlay like GetManagementURL does. * [client] Stop persisting the MDM-overlaid config after mobile logins NewAuth already writes the config through UpdateOrCreateConfig before the MDM policy is overlaid, and the login itself never mutates the Config. The post-login WriteOutConfig calls therefore only rewrote the same file with the enforced ManagementURL and PreSharedKey in it, so a removed or changed policy kept acting through the persisted values. * [client] Document that the MDM overlay on Config is not reversible ApplyMDMPolicy promised that an empty Policy clears a prior overlay, but applyMDMPolicy only resets the enforcement metadata and the runtime-only upload URL; the enforced ManagementURL, PreSharedKey and flags stay. Every lifecycle owner resolves the base Config again before applying, so state that contract instead of the reversibility that was never implemented. * [client] Re-resolve the tvOS preloaded config before every MDM overlay The iOS Client kept the config parsed from SetConfigFromJSON and applied the MDM overlay onto that same instance on every Run, IsLoginRequired and DebugBundle, so a key removed from the policy stayed enforced. Store the JSON instead and parse it per load through one loadConfig path. Auth serialized the overlaid config from GetConfigJSON, which tvOS then persisted to UserDefaults and fed back as the preload. Keep the resolved config as the base, run the login on a JSON round-trip copy with the overlay, and return the base from GetConfigJSON. * [client] Serve the MDM-managed management URL without touching the config file on mobile Preferences.GetManagementURL resolved a managed URL by reading and overlaying the persisted config, so a corrupt file or the tvOS sandbox turned an enforced URL into a read error. Return the canonical managed value directly, the same string BuildRestrictions already hands to the UI, and only fall back to the staged or persisted value when MDM does not manage the key. NewAuth validated the caller-supplied management URL before the overlay ran, so a malformed or echoed value blocked or persisted under an MDM policy that already dictates the URL. Ignore the caller value while the key is managed; the login runs against the overlay either way. * [client] Align the MDM loader docs with the fetcher precedence and make disableAdvancedView a tristate NewLoader, PolicyFetcher and the darwin/windows loadPlatform docs claimed the fetcher is unused on desktop, while every loader returns its values when one is injected. That precedence is the seam the server tests rely on across platforms, so the docs now describe it; production desktop callers still pass nil and keep the registry / plist authoritative. Fields.DisableAdvancedView collapsed "managed and false" into the same JSON as "not managed", unlike AllowServerSSH and the daemon's optional proto field. Carry it as a *bool so the UIs can tell the two apart; the desktop reflect loop skips pointer fields already, and the mobile decoders treat null as not managed. * [client] Clean up MDM review nits - ResolveConflicts treats a managed key whose ConflictCheck has no Check as a conflict instead of dereferencing nil. - Ticker.Run and ChangeDetector.Changed share policyChanged so the diff semantics and the log line cannot drift apart. - TestLoader_NilFetcherReturnsEmpty skips on windows/darwin, where a nil fetcher reads the real registry / plist. - The profilemanager test loader checks GetInt before GetBool so integer keys survive the round trip, and the PSK tests use the exported redaction sentinel. * [client] Fix int policy values coercing to bool in the MDM test helper withMDMPolicy rebuilt the policy map by trying GetString, then GetBool, then GetInt. Policy.GetBool accepts native ints (non-zero means true), so an int-valued key such as wireguardPort round-tripped through the helper as the bool true and GetInt was never reached. Try GetInt before GetBool, as the profilemanager helper already does; GetInt does not coerce bools, so booleans still fall through to GetBool. No test sets an int key today, so this was latent: the first test to exercise the wireguardPort conflict gate would have seen ConflictInt64 report a conflict for every value, including a matching one. --------- Co-authored-by: Zoltan Papp <zoltan.pmail@gmail.com>
658 lines
19 KiB
Go
658 lines
19 KiB
Go
package embed
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/netip"
|
|
"os"
|
|
"sync"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
wgdevice "golang.zx2c4.com/wireguard/device"
|
|
wgnetstack "golang.zx2c4.com/wireguard/tun/netstack"
|
|
|
|
"github.com/netbirdio/netbird/client/iface"
|
|
"github.com/netbirdio/netbird/client/iface/netstack"
|
|
"github.com/netbirdio/netbird/client/internal"
|
|
"github.com/netbirdio/netbird/client/internal/auth"
|
|
"github.com/netbirdio/netbird/client/internal/peer"
|
|
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
|
"github.com/netbirdio/netbird/client/mdm"
|
|
nbssh "github.com/netbirdio/netbird/client/ssh"
|
|
"github.com/netbirdio/netbird/client/system"
|
|
"github.com/netbirdio/netbird/shared/management/domain"
|
|
mgmProto "github.com/netbirdio/netbird/shared/management/proto"
|
|
"github.com/netbirdio/netbird/util/capture"
|
|
)
|
|
|
|
var (
|
|
ErrClientAlreadyStarted = errors.New("client already started")
|
|
ErrClientNotStarted = errors.New("client not started")
|
|
ErrEngineNotStarted = errors.New("engine not started")
|
|
ErrConfigNotInitialized = errors.New("config not initialized")
|
|
)
|
|
|
|
const (
|
|
// PeerStatusConnected indicates the peer is in connected state.
|
|
PeerStatusConnected = peer.StatusConnected
|
|
)
|
|
|
|
// PeerConnStatus is a peer's connection status.
|
|
type PeerConnStatus = peer.ConnStatus
|
|
|
|
// Client manages a netbird embedded client instance.
|
|
type Client struct {
|
|
deviceName string
|
|
config *profilemanager.Config
|
|
mu sync.Mutex
|
|
cancel context.CancelFunc
|
|
setupKey string
|
|
jwtToken string
|
|
connect *internal.ConnectClient
|
|
recorder *peer.Status
|
|
}
|
|
|
|
// Options configures a new Client.
|
|
type Options struct {
|
|
// DeviceName is this peer's name in the network
|
|
DeviceName string
|
|
// SetupKey is used for authentication
|
|
SetupKey string
|
|
// JWTToken is used for JWT-based authentication
|
|
JWTToken string
|
|
// PrivateKey is used for direct private key authentication
|
|
PrivateKey string
|
|
// ManagementURL overrides the default management server URL
|
|
ManagementURL string
|
|
// PreSharedKey is the pre-shared key for the tunnel interface
|
|
PreSharedKey string
|
|
// LogOutput is the output destination for logs (defaults to os.Stderr if nil)
|
|
LogOutput io.Writer
|
|
// LogLevel sets the logging level (defaults to info if empty)
|
|
LogLevel string
|
|
// NoUserspace disables the userspace networking mode. Needs admin/root privileges
|
|
NoUserspace bool
|
|
// ConfigPath is the path to the netbird config file. If empty, the config will be stored in memory and not persisted.
|
|
ConfigPath string
|
|
// StatePath is the path to the netbird state file
|
|
StatePath string
|
|
// DisableClientRoutes disables the client routes
|
|
DisableClientRoutes bool
|
|
// DisableIPv6 disables IPv6 overlay addressing
|
|
DisableIPv6 bool
|
|
// BlockInbound blocks all inbound connections from peers
|
|
BlockInbound bool
|
|
// EnableRosenpass enables the Rosenpass post-quantum key exchange.
|
|
EnableRosenpass bool
|
|
// RosenpassPermissive lets a Rosenpass-enabled peer still connect to peers
|
|
// that do not run Rosenpass (falling back to the plain WireGuard PSK).
|
|
RosenpassPermissive bool
|
|
// BlockLANAccess blocks the embedded peer from reaching the host's
|
|
// LAN (RFC 1918, link-local, loopback) when it's used as a routing
|
|
// peer. Mirrors profilemanager.ConfigInput.BlockLANAccess. Useful
|
|
// when the embedded client must never act as a stepping stone into
|
|
// the host's local network (e.g. the proxy's overlay peer).
|
|
BlockLANAccess bool
|
|
// LazyConnectionEnabled is a tri-state local override for lazy connections,
|
|
// mirroring the NB_LAZY_CONN env var. Nil defers to the management feature
|
|
// flag; a set value overrides it in both directions. A short-lived client
|
|
// that reaches only a few known peers can set this to false, so its peers
|
|
// connect eagerly and the first request does not wait for the connection to
|
|
// be established.
|
|
LazyConnectionEnabled *bool
|
|
// WireguardPort is the port for the tunnel interface. Use 0 for a random port.
|
|
WireguardPort *int
|
|
// MTU is the MTU for the tunnel interface.
|
|
// Valid values are in the range 576..8192 bytes.
|
|
// If non-nil, this value overrides any value stored in the config file.
|
|
// If nil, the existing config MTU (if non-zero) is preserved; otherwise it defaults to 1280.
|
|
// Set to a higher value (e.g. 1400) if carrying QUIC or other protocols that require larger datagrams.
|
|
MTU *uint16
|
|
// DNSLabels defines additional DNS labels configured in the peer.
|
|
DNSLabels []string
|
|
// Performance configures the tunnel's buffer pool cap and batch size.
|
|
Performance Performance
|
|
}
|
|
|
|
// Performance configures the embedded client's tunnel memory/throughput knobs.
|
|
//
|
|
// These settings are process-global: any non-nil field also becomes the
|
|
// default for Clients constructed by later embed.New calls in the same
|
|
// process. Nil fields are ignored.
|
|
type Performance struct {
|
|
// PreallocatedBuffersPerPool caps the per-tunnel buffer pool. Zero
|
|
// leaves the pool unbounded. Lower values trade throughput for a
|
|
// tighter memory ceiling. May also be changed on a running Client via
|
|
// Client.SetPerformance, provided this field was nonzero at construction.
|
|
PreallocatedBuffersPerPool *uint32
|
|
// MaxBatchSize overrides the number of packets the tunnel reads or
|
|
// writes per syscall, which also bounds eager buffer allocation per
|
|
// worker. Zero uses the platform default. Applied at construction
|
|
// only; ignored by Client.SetPerformance.
|
|
MaxBatchSize *uint32
|
|
}
|
|
|
|
// validateCredentials checks that exactly one credential type is provided
|
|
func (opts *Options) validateCredentials() error {
|
|
credentialsProvided := 0
|
|
if opts.SetupKey != "" {
|
|
credentialsProvided++
|
|
}
|
|
if opts.JWTToken != "" {
|
|
credentialsProvided++
|
|
}
|
|
if opts.PrivateKey != "" {
|
|
credentialsProvided++
|
|
}
|
|
|
|
if credentialsProvided == 0 {
|
|
return fmt.Errorf("one of SetupKey, JWTToken, or PrivateKey must be provided")
|
|
}
|
|
if credentialsProvided > 1 {
|
|
return fmt.Errorf("only one of SetupKey, JWTToken, or PrivateKey can be specified")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// New creates a new netbird embedded client.
|
|
func New(opts Options) (*Client, error) {
|
|
if err := opts.validateCredentials(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if opts.MTU != nil {
|
|
if err := iface.ValidateMTU(*opts.MTU); err != nil {
|
|
return nil, fmt.Errorf("invalid MTU: %w", err)
|
|
}
|
|
}
|
|
|
|
if opts.LogOutput != nil {
|
|
logrus.SetOutput(opts.LogOutput)
|
|
}
|
|
|
|
if opts.LogLevel != "" {
|
|
level, err := logrus.ParseLevel(opts.LogLevel)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse log level: %w", err)
|
|
}
|
|
logrus.SetLevel(level)
|
|
}
|
|
|
|
if !opts.NoUserspace {
|
|
if err := os.Setenv(netstack.EnvUseNetstackMode, "true"); err != nil {
|
|
return nil, fmt.Errorf("setenv: %w", err)
|
|
}
|
|
if err := os.Setenv(netstack.EnvSkipProxy, "true"); err != nil {
|
|
return nil, fmt.Errorf("setenv: %w", err)
|
|
}
|
|
}
|
|
|
|
if opts.StatePath != "" {
|
|
// TODO: Disable state if path not provided
|
|
if err := os.Setenv("NB_DNS_STATE_FILE", opts.StatePath); err != nil {
|
|
return nil, fmt.Errorf("setenv: %w", err)
|
|
}
|
|
}
|
|
|
|
var err error
|
|
var parsedLabels domain.List
|
|
if parsedLabels, err = domain.FromStringList(opts.DNSLabels); err != nil {
|
|
return nil, fmt.Errorf("invalid dns labels: %w", err)
|
|
}
|
|
|
|
t := true
|
|
var config *profilemanager.Config
|
|
input := profilemanager.ConfigInput{
|
|
ConfigPath: opts.ConfigPath,
|
|
ManagementURL: opts.ManagementURL,
|
|
PreSharedKey: &opts.PreSharedKey,
|
|
DisableServerRoutes: &t,
|
|
DisableClientRoutes: &opts.DisableClientRoutes,
|
|
DisableIPv6: &opts.DisableIPv6,
|
|
BlockInbound: &opts.BlockInbound,
|
|
BlockLANAccess: &opts.BlockLANAccess,
|
|
RosenpassEnabled: &opts.EnableRosenpass,
|
|
RosenpassPermissive: &opts.RosenpassPermissive,
|
|
WireguardPort: opts.WireguardPort,
|
|
MTU: opts.MTU,
|
|
DNSLabels: parsedLabels,
|
|
}
|
|
if opts.ConfigPath != "" {
|
|
config, err = profilemanager.UpdateOrCreateConfig(input)
|
|
} else {
|
|
config, err = profilemanager.CreateInMemoryConfig(input)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create config: %w", err)
|
|
}
|
|
// Embedded path runs without the daemon Server: apply the active
|
|
// MDM policy explicitly so a forced ManagementURL / PSK / other
|
|
// managed key takes effect on this embedded engine instance.
|
|
config.ApplyMDMPolicy(mdm.NewLoader(nil).Load())
|
|
|
|
if opts.PrivateKey != "" {
|
|
config.PrivateKey = opts.PrivateKey
|
|
}
|
|
|
|
if opts.LazyConnectionEnabled != nil {
|
|
// Runtime-only override, read back through lazyconn.ParseState; a set value
|
|
// wins over the management feature flag in both directions.
|
|
config.LazyConnection = "off"
|
|
if *opts.LazyConnectionEnabled {
|
|
config.LazyConnection = "on"
|
|
}
|
|
}
|
|
|
|
if opts.Performance.PreallocatedBuffersPerPool != nil {
|
|
wgdevice.SetPreallocatedBuffersPerPool(*opts.Performance.PreallocatedBuffersPerPool)
|
|
}
|
|
if opts.Performance.MaxBatchSize != nil {
|
|
wgdevice.SetMaxBatchSizeOverride(*opts.Performance.MaxBatchSize)
|
|
}
|
|
|
|
return &Client{
|
|
deviceName: opts.DeviceName,
|
|
setupKey: opts.SetupKey,
|
|
jwtToken: opts.JWTToken,
|
|
config: config,
|
|
recorder: peer.NewRecorder(config.ManagementURL.String()),
|
|
}, nil
|
|
}
|
|
|
|
// Start begins client operation and blocks until the engine has been started successfully or a startup error occurs.
|
|
// Pass a context with a deadline to limit the time spent waiting for the engine to start.
|
|
func (c *Client) Start(startCtx context.Context) error {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if c.connect != nil {
|
|
return ErrClientAlreadyStarted
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(internal.CtxInitState(context.Background()))
|
|
defer func() {
|
|
if c.connect == nil {
|
|
cancel()
|
|
}
|
|
}()
|
|
|
|
// nolint:staticcheck
|
|
ctx = context.WithValue(ctx, system.DeviceNameCtxKey, c.deviceName)
|
|
|
|
authClient, err := auth.NewAuth(ctx, c.config.PrivateKey, c.config.ManagementURL, c.config)
|
|
if err != nil {
|
|
return fmt.Errorf("create auth client: %w", err)
|
|
}
|
|
defer authClient.Close()
|
|
|
|
if err, _ := authClient.Login(ctx, c.setupKey, c.jwtToken); err != nil {
|
|
return fmt.Errorf("login: %w", err)
|
|
}
|
|
client := internal.NewConnectClient(ctx, c.config, c.recorder)
|
|
client.SetSyncResponsePersistence(true)
|
|
|
|
// either startup error (permanent backoff err) or nil err (successful engine up)
|
|
// TODO: make after-startup backoff err available
|
|
run := make(chan struct{})
|
|
clientErr := make(chan error, 1)
|
|
go func() {
|
|
if err := client.Run(run, ""); err != nil {
|
|
clientErr <- err
|
|
}
|
|
}()
|
|
|
|
select {
|
|
case <-startCtx.Done():
|
|
// ConnectClient.Stop now cancels its own run context and waits for the
|
|
// run loop to tear the engine down, so this cancel() is no longer
|
|
// required to break the deadlock and could be removed. It is kept as a
|
|
// defensive belt-and-suspenders: cancelling the parent context first
|
|
// guarantees the run loop is unblocked even if Stop's contract regresses.
|
|
cancel()
|
|
if stopErr := client.Stop(); stopErr != nil {
|
|
return fmt.Errorf("stop error after context done. Stop error: %w. Context done: %w", stopErr, startCtx.Err())
|
|
}
|
|
return startCtx.Err()
|
|
case err := <-clientErr:
|
|
return fmt.Errorf("startup: %w", err)
|
|
case <-run:
|
|
}
|
|
|
|
c.connect = client
|
|
c.cancel = cancel
|
|
|
|
return nil
|
|
}
|
|
|
|
// Stop gracefully stops the client.
|
|
// Pass a context with a deadline to limit the time spent waiting for the engine to stop.
|
|
func (c *Client) Stop(ctx context.Context) error {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
if c.connect == nil {
|
|
return ErrClientNotStarted
|
|
}
|
|
|
|
if c.cancel != nil {
|
|
c.cancel()
|
|
c.cancel = nil
|
|
}
|
|
|
|
done := make(chan error, 1)
|
|
connect := c.connect
|
|
go func() {
|
|
done <- connect.Stop()
|
|
}()
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
c.connect = nil
|
|
return ctx.Err()
|
|
case err := <-done:
|
|
c.connect = nil
|
|
if err != nil {
|
|
return fmt.Errorf("stop: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// GetConfig returns a copy of the internal client config.
|
|
func (c *Client) GetConfig() (profilemanager.Config, error) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if c.config == nil {
|
|
return profilemanager.Config{}, ErrConfigNotInitialized
|
|
}
|
|
return *c.config, nil
|
|
}
|
|
|
|
// Dial dials a network address in the netbird network.
|
|
// Not applicable if the userspace networking mode is disabled.
|
|
func (c *Client) Dial(ctx context.Context, network, address string) (net.Conn, error) {
|
|
engine, err := c.getEngine()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
nsnet, err := engine.GetNet()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get net: %w", err)
|
|
}
|
|
|
|
return nsnet.DialContext(ctx, network, address)
|
|
}
|
|
|
|
// DialContext dials a network address in the netbird network with context
|
|
func (c *Client) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
|
return c.Dial(ctx, network, address)
|
|
}
|
|
|
|
// ListenTCP listens on the given address in the netbird network.
|
|
// Not applicable if the userspace networking mode is disabled.
|
|
func (c *Client) ListenTCP(address string) (net.Listener, error) {
|
|
nsnet, addr, err := c.getNet()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_, port, err := net.SplitHostPort(address)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("split host port: %w", err)
|
|
}
|
|
listenAddr := net.JoinHostPort(addr.String(), port)
|
|
|
|
tcpAddr, err := net.ResolveTCPAddr("tcp", listenAddr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("resolve: %w", err)
|
|
}
|
|
return nsnet.ListenTCP(tcpAddr)
|
|
}
|
|
|
|
// ListenUDP listens on the given address in the netbird network.
|
|
// Not applicable if the userspace networking mode is disabled.
|
|
func (c *Client) ListenUDP(address string) (net.PacketConn, error) {
|
|
nsnet, addr, err := c.getNet()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_, port, err := net.SplitHostPort(address)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("split host port: %w", err)
|
|
}
|
|
listenAddr := net.JoinHostPort(addr.String(), port)
|
|
|
|
udpAddr, err := net.ResolveUDPAddr("udp", listenAddr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("resolve: %w", err)
|
|
}
|
|
|
|
return nsnet.ListenUDP(udpAddr)
|
|
}
|
|
|
|
// NewHTTPClient returns a configured http.Client that uses the netbird network for requests.
|
|
// Not applicable if the userspace networking mode is disabled.
|
|
func (c *Client) NewHTTPClient() *http.Client {
|
|
transport := &http.Transport{
|
|
DialContext: c.Dial,
|
|
}
|
|
|
|
return &http.Client{
|
|
Transport: transport,
|
|
}
|
|
}
|
|
|
|
// Expose exposes a local service via the NetBird reverse proxy, making it accessible through a public URL.
|
|
// It returns an ExposeSession. Call Wait on the session to keep it alive.
|
|
func (c *Client) Expose(ctx context.Context, req ExposeRequest) (*ExposeSession, error) {
|
|
engine, err := c.getEngine()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
mgr := engine.GetExposeManager()
|
|
if mgr == nil {
|
|
return nil, fmt.Errorf("expose manager not available")
|
|
}
|
|
|
|
resp, err := mgr.Expose(ctx, req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("expose: %w", err)
|
|
}
|
|
|
|
return &ExposeSession{
|
|
Domain: resp.Domain,
|
|
ServiceName: resp.ServiceName,
|
|
ServiceURL: resp.ServiceURL,
|
|
mgr: mgr,
|
|
}, nil
|
|
}
|
|
|
|
// IdentityForIP looks up a remote peer by its tunnel IP using the
|
|
// embedded client's status recorder. Returns the peer's WireGuard public
|
|
// key and FQDN. ok=false means the IP doesn't belong to an active peer
|
|
// — offline roster peers are treated as unknown, same as foreign IPs.
|
|
func (c *Client) IdentityForIP(ip netip.Addr) (pubKey, fqdn string, ok bool) {
|
|
if !ip.IsValid() || c.recorder == nil {
|
|
return "", "", false
|
|
}
|
|
state, found := c.recorder.PeerStateByIP(ip.String())
|
|
if !found {
|
|
return "", "", false
|
|
}
|
|
return state.PubKey, state.FQDN, true
|
|
}
|
|
|
|
// Status returns the current status of the client.
|
|
func (c *Client) Status() (peer.FullStatus, error) {
|
|
c.mu.Lock()
|
|
connect := c.connect
|
|
c.mu.Unlock()
|
|
|
|
if connect != nil {
|
|
engine := connect.Engine()
|
|
if engine != nil {
|
|
_ = engine.RunHealthProbes(context.Background(), false)
|
|
}
|
|
}
|
|
|
|
return c.recorder.GetFullStatus(), nil
|
|
}
|
|
|
|
// GetLatestSyncResponse returns the latest sync response from the management server.
|
|
func (c *Client) GetLatestSyncResponse() (*mgmProto.SyncResponse, error) {
|
|
engine, err := c.getEngine()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
syncResp, err := engine.GetLatestSyncResponse()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get sync response: %w", err)
|
|
}
|
|
|
|
return syncResp, nil
|
|
}
|
|
|
|
// SetLogLevel sets the logging level for the client and its components.
|
|
func (c *Client) SetLogLevel(levelStr string) error {
|
|
level, err := logrus.ParseLevel(levelStr)
|
|
if err != nil {
|
|
return fmt.Errorf("parse log level: %w", err)
|
|
}
|
|
|
|
logrus.SetLevel(level)
|
|
|
|
c.mu.Lock()
|
|
connect := c.connect
|
|
c.mu.Unlock()
|
|
|
|
if connect != nil {
|
|
connect.SetLogLevel(level)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// VerifySSHHostKey verifies an SSH host key against stored peer keys.
|
|
// Returns nil if the key matches, ErrPeerNotFound if peer is not in network,
|
|
// ErrNoStoredKey if peer has no stored key, or an error for verification failures.
|
|
func (c *Client) VerifySSHHostKey(peerAddress string, key []byte) error {
|
|
engine, err := c.getEngine()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nbssh.PeerKeyLookup(engine.GetPeerSSHKey).VerifySSHHostKey(peerAddress, key)
|
|
}
|
|
|
|
// SetPerformance retunes a running Client. Only PreallocatedBuffersPerPool
|
|
// takes effect, and only when it was nonzero at construction;
|
|
// MaxBatchSize is construction-only and returns an error if set here.
|
|
//
|
|
// Returns ErrClientNotStarted / ErrEngineNotStarted if the Client is not
|
|
// running yet.
|
|
func (c *Client) SetPerformance(t Performance) error {
|
|
if t.MaxBatchSize != nil {
|
|
return errors.New("MaxBatchSize is construction-only and cannot be changed at runtime")
|
|
}
|
|
engine, err := c.getEngine()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return engine.SetPerformance(internal.Performance{
|
|
PreallocatedBuffersPerPool: t.PreallocatedBuffersPerPool,
|
|
})
|
|
}
|
|
|
|
// StartCapture begins capturing packets on this client's tunnel device.
|
|
// Only one capture can be active at a time; starting a new one stops the previous.
|
|
// Call StopCapture (or CaptureSession.Stop) to end it.
|
|
func (c *Client) StartCapture(opts CaptureOptions) (*CaptureSession, error) {
|
|
engine, err := c.getEngine()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var matcher capture.Matcher
|
|
if opts.Filter != "" {
|
|
m, err := capture.ParseFilter(opts.Filter)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse filter: %w", err)
|
|
}
|
|
matcher = m
|
|
}
|
|
|
|
sess, err := capture.NewSession(capture.Options{
|
|
Output: opts.Output,
|
|
TextOutput: opts.TextOutput,
|
|
Matcher: matcher,
|
|
Verbose: opts.Verbose,
|
|
ASCII: opts.ASCII,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create capture session: %w", err)
|
|
}
|
|
|
|
if err := engine.SetCapture(sess); err != nil {
|
|
sess.Stop()
|
|
return nil, fmt.Errorf("set capture: %w", err)
|
|
}
|
|
|
|
return &CaptureSession{sess: sess, engine: engine}, nil
|
|
}
|
|
|
|
// StopCapture stops the active capture session if one is running.
|
|
func (c *Client) StopCapture() error {
|
|
engine, err := c.getEngine()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return engine.SetCapture(nil)
|
|
}
|
|
|
|
// getEngine safely retrieves the engine from the client with proper locking.
|
|
// Returns ErrClientNotStarted if the client is not started.
|
|
// Returns ErrEngineNotStarted if the engine is not available.
|
|
func (c *Client) getEngine() (*internal.Engine, error) {
|
|
c.mu.Lock()
|
|
connect := c.connect
|
|
c.mu.Unlock()
|
|
|
|
if connect == nil {
|
|
return nil, ErrClientNotStarted
|
|
}
|
|
|
|
engine := connect.Engine()
|
|
if engine == nil {
|
|
return nil, ErrEngineNotStarted
|
|
}
|
|
|
|
return engine, nil
|
|
}
|
|
|
|
func (c *Client) getNet() (*wgnetstack.Net, netip.Addr, error) {
|
|
engine, err := c.getEngine()
|
|
if err != nil {
|
|
return nil, netip.Addr{}, err
|
|
}
|
|
|
|
addr, err := engine.Address()
|
|
if err != nil {
|
|
return nil, netip.Addr{}, fmt.Errorf("engine address: %w", err)
|
|
}
|
|
|
|
nsnet, err := engine.GetNet()
|
|
if err != nil {
|
|
return nil, netip.Addr{}, fmt.Errorf("get net: %w", err)
|
|
}
|
|
|
|
return nsnet, addr, nil
|
|
}
|