mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-09 08:21: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>
961 lines
30 KiB
Go
961 lines
30 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/spf13/cobra"
|
|
"google.golang.org/grpc/codes"
|
|
|
|
gstatus "google.golang.org/grpc/status"
|
|
"google.golang.org/protobuf/types/known/durationpb"
|
|
|
|
"github.com/netbirdio/netbird/client/iface"
|
|
"github.com/netbirdio/netbird/client/internal"
|
|
"github.com/netbirdio/netbird/client/internal/peer"
|
|
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
|
"github.com/netbirdio/netbird/client/mdm"
|
|
nbnet "github.com/netbirdio/netbird/client/net"
|
|
"github.com/netbirdio/netbird/client/proto"
|
|
"github.com/netbirdio/netbird/client/server"
|
|
"github.com/netbirdio/netbird/client/system"
|
|
"github.com/netbirdio/netbird/shared/management/domain"
|
|
"github.com/netbirdio/netbird/util"
|
|
)
|
|
|
|
const (
|
|
invalidInputType int = iota
|
|
ipInputType
|
|
interfaceInputType
|
|
)
|
|
|
|
const (
|
|
dnsLabelsFlag = "extra-dns-labels"
|
|
|
|
noBrowserFlag = "no-browser"
|
|
noBrowserDesc = "do not open the browser for SSO login"
|
|
|
|
showQRFlag = "qr"
|
|
showQRDesc = "show QR code for the SSO login URL (useful for headless machines without browser access)"
|
|
|
|
profileNameFlag = "profile"
|
|
profileNameDesc = "profile name to use for the login. If not specified, the last used profile will be used."
|
|
)
|
|
|
|
var errDaemonActiveProfileUnsupported = errors.New("daemon does not support active profile lookup")
|
|
|
|
var (
|
|
foregroundMode bool
|
|
dnsLabels []string
|
|
dnsLabelsValidated domain.List
|
|
noBrowser bool
|
|
showQR bool
|
|
profileName string
|
|
configPath string
|
|
|
|
upCmd = &cobra.Command{
|
|
Use: "up",
|
|
Short: "Connect to the NetBird network",
|
|
Long: "Connect to the NetBird network using the provided setup key or SSO auth. This command will bring up the WireGuard interface, connect to the management server, and establish peer-to-peer connections with other peers in the network if required.",
|
|
RunE: upFunc,
|
|
}
|
|
)
|
|
|
|
func init() {
|
|
upCmd.PersistentFlags().BoolVarP(&foregroundMode, "foreground-mode", "F", false, "start service in foreground")
|
|
upCmd.PersistentFlags().StringVar(&interfaceName, interfaceNameFlag, iface.WgInterfaceDefault, "WireGuard interface name")
|
|
upCmd.PersistentFlags().Uint16Var(&wireguardPort, wireguardPortFlag, iface.DefaultWgPort, "WireGuard interface listening port")
|
|
upCmd.PersistentFlags().Uint16Var(&mtu, mtuFlag, iface.DefaultMTU, "Set MTU (Maximum Transmission Unit) for the WireGuard interface")
|
|
upCmd.PersistentFlags().BoolVarP(&networkMonitor, networkMonitorFlag, "N", networkMonitor,
|
|
`Manage network monitoring. Defaults to true on Windows and macOS, false on Linux and FreeBSD. `+
|
|
`E.g. --network-monitor=false to disable or --network-monitor=true to enable.`,
|
|
)
|
|
upCmd.PersistentFlags().StringSliceVar(&extraIFaceBlackList, extraIFaceBlackListFlag, nil, "Extra list of default interfaces to ignore for listening")
|
|
upCmd.PersistentFlags().DurationVar(&dnsRouteInterval, dnsRouteIntervalFlag, time.Minute, "DNS route update interval")
|
|
|
|
upCmd.PersistentFlags().StringSliceVar(&dnsLabels, dnsLabelsFlag, nil,
|
|
`Sets DNS labels`+
|
|
`You can specify a comma-separated list of up to 32 labels. `+
|
|
`An empty string "" clears the previous configuration. `+
|
|
`E.g. --extra-dns-labels vpc1 or --extra-dns-labels vpc1,mgmt1 `+
|
|
`or --extra-dns-labels ""`,
|
|
)
|
|
|
|
upCmd.PersistentFlags().BoolVar(&noBrowser, noBrowserFlag, false, noBrowserDesc)
|
|
upCmd.PersistentFlags().BoolVar(&showQR, showQRFlag, false, showQRDesc)
|
|
upCmd.PersistentFlags().StringVar(&profileName, profileNameFlag, "", profileNameDesc)
|
|
upCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "(DEPRECATED) NetBird config file location. ")
|
|
|
|
}
|
|
|
|
func upFunc(cmd *cobra.Command, args []string) error {
|
|
SetFlagsFromEnvVars(rootCmd)
|
|
SetFlagsFromEnvVars(cmd)
|
|
|
|
cmd.SetOut(cmd.OutOrStdout())
|
|
|
|
err := util.InitLog(logLevel, util.LogConsole)
|
|
if err != nil {
|
|
return fmt.Errorf("failed initializing log %v", err)
|
|
}
|
|
|
|
err = validateNATExternalIPs(natExternalIPs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
dnsLabelsValidated, err = validateDnsLabels(dnsLabels)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx := internal.CtxInitState(cmd.Context())
|
|
|
|
if hostName != "" {
|
|
// nolint
|
|
ctx = context.WithValue(ctx, system.DeviceNameCtxKey, hostName)
|
|
}
|
|
|
|
pm := profilemanager.NewProfileManager()
|
|
|
|
username, err := profilemanager.InvokingUser()
|
|
if err != nil {
|
|
return fmt.Errorf("get current user: %v", err)
|
|
}
|
|
|
|
var activeProf *profilemanager.Profile
|
|
var profileSwitched bool
|
|
// switch profile if provided
|
|
if profileName != "" {
|
|
activeProf, err = switchOrCreateProfile(cmd.Context(), pm, profileName, username.Username)
|
|
if err != nil {
|
|
return fmt.Errorf("switch profile: %v", err)
|
|
}
|
|
profileSwitched = true
|
|
} else {
|
|
activeProf, err = pm.GetActiveProfile()
|
|
if err != nil {
|
|
return fmt.Errorf("get active profile: %v", err)
|
|
}
|
|
}
|
|
|
|
if foregroundMode {
|
|
return runInForegroundMode(ctx, cmd, activeProf)
|
|
}
|
|
return runInDaemonMode(ctx, cmd, pm, activeProf, profileSwitched)
|
|
}
|
|
|
|
// switchOrCreateProfile switches the active profile to the one identified by
|
|
// handle, creating it first when it does not exist yet. This restores the
|
|
// pre-0.73 behaviour where `netbird up --profile <name>` auto-creates a
|
|
// missing profile instead of failing. Returns the daemon-resolved profile so
|
|
// callers act on it directly instead of re-reading the local state, which is
|
|
// not updated under sudo.
|
|
func switchOrCreateProfile(ctx context.Context, pm *profilemanager.ProfileManager, handle, username string) (*profilemanager.Profile, error) {
|
|
resolvedID, err := switchProfile(ctx, handle, username)
|
|
if err != nil {
|
|
st, ok := gstatus.FromError(err)
|
|
if !ok || st.Code() != codes.NotFound {
|
|
return nil, err
|
|
}
|
|
// Don't fail immediately on a create error: a concurrent run may
|
|
// have created the profile between the NotFound above and this
|
|
// call, in which case the retried switch still succeeds. Only
|
|
// surface the create error if the switch also fails.
|
|
_, createErr := createProfile(ctx, handle, username)
|
|
if resolvedID, err = switchProfile(ctx, handle, username); err != nil {
|
|
if createErr != nil {
|
|
return nil, fmt.Errorf("create profile: %w", createErr)
|
|
}
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if err := pm.SwitchProfile(resolvedID); err != nil {
|
|
return nil, err
|
|
}
|
|
return &profilemanager.Profile{ID: resolvedID}, nil
|
|
}
|
|
|
|
// createProfile dials the daemon and creates a new profile with the given
|
|
// display name, returning its generated ID. Use addProfileOnDaemon directly
|
|
// when a daemon client is already available to reuse the connection.
|
|
func createProfile(ctx context.Context, profileName, username string) (profilemanager.ID, error) {
|
|
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
|
if err != nil {
|
|
//nolint
|
|
return "", fmt.Errorf("failed to connect to daemon error: %v\n"+
|
|
"If the daemon is not running please run: "+
|
|
"\nnetbird service install \nnetbird service start\n", err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
return addProfileOnDaemon(ctx, proto.NewDaemonServiceClient(conn), profileName, username)
|
|
}
|
|
|
|
func runInForegroundMode(ctx context.Context, cmd *cobra.Command, activeProf *profilemanager.Profile) error {
|
|
// override the default profile filepath if provided
|
|
if configPath != "" {
|
|
_ = profilemanager.NewServiceManager(configPath)
|
|
}
|
|
|
|
err := handleRebrand(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
customDNSAddressConverted, err := parseCustomDNSAddress(cmd.Flag(dnsResolverAddress).Changed)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
configFilePath, err := activeProf.FilePath()
|
|
if err != nil {
|
|
return fmt.Errorf("get active profile file path: %v", err)
|
|
}
|
|
|
|
ic, err := setupConfig(customDNSAddressConverted, cmd, configFilePath)
|
|
if err != nil {
|
|
return fmt.Errorf("setup config: %v", err)
|
|
}
|
|
|
|
providedSetupKey, err := getSetupKey()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
config, err := profilemanager.UpdateOrCreateConfig(*ic)
|
|
if err != nil {
|
|
return fmt.Errorf("get config file: %v", err)
|
|
}
|
|
// CLI foreground path runs without the daemon Server: layer in the
|
|
// active MDM policy explicitly so a forced ManagementURL / PSK /
|
|
// other managed key actually takes effect on this run.
|
|
config.ApplyMDMPolicy(mdm.NewLoader(nil).Load())
|
|
|
|
_, _ = profilemanager.UpdateOldManagementURL(ctx, config, configFilePath)
|
|
|
|
// Restore residual state left by a previous run that did not shut down
|
|
// cleanly, mirroring what the daemon does before connecting: it recovers
|
|
// DNS config (a stale resolv.conf takeover can make the management
|
|
// hostname unresolvable), firewall rules, ssh config and legacy routing.
|
|
// Route cleanup itself happens at engine start; nbnet.Init() below lets
|
|
// the management dial bypass a leftover fwmark rule until then.
|
|
// Foreground mode is particularly exposed in containers: a crashed
|
|
// container restarts inside the same (pod) network namespace, so stale
|
|
// state survives while the process does not.
|
|
if err := server.RestoreResidualState(ctx, profilemanager.NewServiceManager(configPath).GetStatePath()); err != nil {
|
|
log.Warnf("failed to restore residual state: %v", err)
|
|
}
|
|
|
|
// Enable advanced routing (as the daemon does on startup) so the
|
|
// management dial bypasses a leftover fwmark rule instead of being
|
|
// shunted into a stale routing table.
|
|
nbnet.Init()
|
|
|
|
err = foregroundLogin(ctx, cmd, config, providedSetupKey, activeProf.ID)
|
|
if err != nil {
|
|
return fmt.Errorf("foreground login failed: %v", err)
|
|
}
|
|
|
|
var cancel context.CancelFunc
|
|
ctx, cancel = context.WithCancel(ctx)
|
|
SetupCloseHandler(ctx, cancel)
|
|
|
|
r := peer.NewRecorder(config.ManagementURL.String())
|
|
r.GetFullStatus()
|
|
|
|
connectClient := internal.NewConnectClient(ctx, config, r)
|
|
SetupDebugHandler(ctx, config, r, connectClient, "")
|
|
|
|
return connectClient.Run(nil, util.FindFirstLogPath(logFiles))
|
|
}
|
|
|
|
func runInDaemonMode(ctx context.Context, cmd *cobra.Command, pm *profilemanager.ProfileManager, activeProf *profilemanager.Profile, profileSwitched bool) error {
|
|
// Check if deprecated config flag is set and show warning
|
|
if cmd.Flag("config").Changed && configPath != "" {
|
|
cmd.PrintErrf("Warning: Config flag is deprecated on up command, it should be set as a service argument with $NB_CONFIG environment or with \"-config\" flag; netbird service reconfigure --service-env=\"NB_CONFIG=<file_path>\" or netbird service run --config=<file_path>\n")
|
|
}
|
|
|
|
customDNSAddressConverted, err := parseCustomDNSAddress(cmd.Flag(dnsResolverAddress).Changed)
|
|
if err != nil {
|
|
return fmt.Errorf("parse custom DNS address: %v", err)
|
|
}
|
|
|
|
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
|
if err != nil {
|
|
//nolint
|
|
return fmt.Errorf("failed to connect to daemon error: %v\n"+
|
|
"If the daemon is not running please run: "+
|
|
"\nnetbird service install \nnetbird service start\n", err)
|
|
}
|
|
defer func() {
|
|
err := conn.Close()
|
|
if err != nil {
|
|
log.Warnf("failed closing daemon gRPC client connection %v", err)
|
|
return
|
|
}
|
|
}()
|
|
|
|
client := proto.NewDaemonServiceClient(conn)
|
|
|
|
status, err := client.Status(ctx, &proto.StatusRequest{
|
|
WaitForReady: func() *bool { b := true; return &b }(),
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("unable to get daemon status: %v", err)
|
|
}
|
|
|
|
// Under sudo the invoking user's local active-profile mirror is never
|
|
// written (the SwitchProfile write is a no-op), and plain root has no
|
|
// invoking user at all — so the mirror read into activeProf above is stale
|
|
// or defaulted and must not drive the daemon. With no --profile to make the
|
|
// choice explicit, take the profile the daemon already holds for this user
|
|
// instead: it stays on the user's current profile rather than silently
|
|
// switching to the mirror's default, and refuses when the daemon is on
|
|
// another user's profile.
|
|
if profileName == "" && !profilemanager.MirrorIsAuthoritative() {
|
|
u, err := profilemanager.InvokingUser()
|
|
if err != nil {
|
|
return fmt.Errorf("get current user: %v", err)
|
|
}
|
|
resolved, err := daemonActiveProfileForUser(ctx, client, u.Username)
|
|
switch {
|
|
case errors.Is(err, errDaemonActiveProfileUnsupported):
|
|
log.Warnf("keeping the locally resolved profile: %v", err)
|
|
case err != nil:
|
|
return err
|
|
default:
|
|
activeProf = resolved
|
|
}
|
|
}
|
|
|
|
if status.Status == string(internal.StatusConnected) {
|
|
if !profileSwitched {
|
|
cmd.Println("Already connected")
|
|
return nil
|
|
}
|
|
|
|
if _, err := client.Down(ctx, &proto.DownRequest{}); err != nil {
|
|
log.Errorf("call service down method: %v", err)
|
|
return err
|
|
}
|
|
}
|
|
|
|
username, err := profilemanager.InvokingUser()
|
|
if err != nil {
|
|
return fmt.Errorf("get current user: %v", err)
|
|
}
|
|
|
|
// set the new config
|
|
req := setupSetConfigReq(customDNSAddressConverted, cmd, activeProf.ID.String(), username.Username)
|
|
if _, err := client.SetConfig(ctx, req); err != nil {
|
|
if st, ok := gstatus.FromError(err); ok && st.Code() == codes.Unavailable {
|
|
log.Warnf("setConfig method is not available in the daemon: %s", st.Message())
|
|
} else {
|
|
return daemonCallError("call service setConfig method", err)
|
|
}
|
|
}
|
|
|
|
if err := doDaemonUp(ctx, cmd, client, pm, activeProf, customDNSAddressConverted, username.Username); err != nil {
|
|
return fmt.Errorf("daemon up failed: %v", err)
|
|
}
|
|
cmd.Println("Connected")
|
|
return nil
|
|
}
|
|
|
|
func doDaemonUp(ctx context.Context, cmd *cobra.Command, client proto.DaemonServiceClient, pm *profilemanager.ProfileManager, activeProf *profilemanager.Profile, customDNSAddressConverted []byte, username string) error {
|
|
|
|
providedSetupKey, err := getSetupKey()
|
|
if err != nil {
|
|
return fmt.Errorf("get setup key: %v", err)
|
|
}
|
|
|
|
loginRequest, err := setupLoginRequest(providedSetupKey, customDNSAddressConverted, cmd)
|
|
if err != nil {
|
|
return fmt.Errorf("setup login request: %v", err)
|
|
}
|
|
|
|
profileID := activeProf.ID.String()
|
|
loginRequest.ProfileName = &profileID
|
|
loginRequest.Username = &username
|
|
|
|
profileState, err := pm.GetProfileState(activeProf.ID)
|
|
if err != nil {
|
|
log.Debugf("failed to get profile state for login hint: %v", err)
|
|
} else if profileState.Email != "" {
|
|
loginRequest.Hint = &profileState.Email
|
|
}
|
|
|
|
var loginErr error
|
|
var loginResp *proto.LoginResponse
|
|
|
|
err = WithBackOff(func() error {
|
|
var backOffErr error
|
|
loginResp, backOffErr = client.Login(ctx, loginRequest)
|
|
if s, ok := gstatus.FromError(backOffErr); ok && (s.Code() == codes.InvalidArgument ||
|
|
s.Code() == codes.PermissionDenied ||
|
|
s.Code() == codes.NotFound ||
|
|
s.Code() == codes.Unimplemented) {
|
|
loginErr = backOffErr
|
|
return nil
|
|
}
|
|
return backOffErr
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("login backoff cycle failed: %v", err)
|
|
}
|
|
|
|
if loginErr != nil {
|
|
return daemonCallError("login failed", loginErr)
|
|
}
|
|
|
|
if loginResp.NeedsSSOLogin {
|
|
if err := handleSSOLogin(ctx, cmd, loginResp, client, pm); err != nil {
|
|
return fmt.Errorf("sso login failed: %v", err)
|
|
}
|
|
}
|
|
|
|
if _, err := client.Up(ctx, &proto.UpRequest{
|
|
ProfileName: &profileID,
|
|
Username: &username,
|
|
}); err != nil {
|
|
return daemonCallError("call service up method", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// setBoolPtrIfChanged points dst at a copy of val when the named bool flag was
|
|
// explicitly set on cmd. It collapses the repeated
|
|
// "if cmd.Flag(x).Changed { field = &val }" pattern in the request builders into
|
|
// a single call, keeping their cognitive complexity within bounds.
|
|
func setBoolPtrIfChanged(cmd *cobra.Command, name string, dst **bool, val bool) {
|
|
if cmd.Flag(name).Changed {
|
|
dst2 := val
|
|
*dst = &dst2
|
|
}
|
|
}
|
|
|
|
// setSSHSetConfigFields copies the SSH server flags the user actually
|
|
// passed into req, leaving the rest unset so the daemon keeps the
|
|
// persisted values.
|
|
func setSSHSetConfigFields(req *proto.SetConfigRequest, cmd *cobra.Command) {
|
|
if cmd.Flag(serverSSHAllowedFlag).Changed {
|
|
req.ServerSSHAllowed = &serverSSHAllowed
|
|
}
|
|
if cmd.Flag(enableSSHRootFlag).Changed {
|
|
req.EnableSSHRoot = &enableSSHRoot
|
|
}
|
|
if cmd.Flag(enableSSHSFTPFlag).Changed {
|
|
req.EnableSSHSFTP = &enableSSHSFTP
|
|
}
|
|
if cmd.Flag(enableSSHLocalPortForwardFlag).Changed {
|
|
req.EnableSSHLocalPortForwarding = &enableSSHLocalPortForward
|
|
}
|
|
if cmd.Flag(enableSSHRemotePortForwardFlag).Changed {
|
|
req.EnableSSHRemotePortForwarding = &enableSSHRemotePortForward
|
|
}
|
|
if cmd.Flag(disableSSHAuthFlag).Changed {
|
|
req.DisableSSHAuth = &disableSSHAuth
|
|
}
|
|
if cmd.Flag(sshJWTCacheTTLFlag).Changed {
|
|
sshJWTCacheTTL32 := int32(sshJWTCacheTTL)
|
|
req.SshJWTCacheTTL = &sshJWTCacheTTL32
|
|
}
|
|
}
|
|
|
|
func setupSetConfigReq(customDNSAddressConverted []byte, cmd *cobra.Command, profileName, username string) *proto.SetConfigRequest {
|
|
var req proto.SetConfigRequest
|
|
req.ProfileName = profileName
|
|
req.Username = username
|
|
|
|
req.ManagementUrl = managementURL
|
|
req.AdminURL = adminURL
|
|
req.NatExternalIPs = natExternalIPs
|
|
req.CustomDNSAddress = customDNSAddressConverted
|
|
req.ExtraIFaceBlacklist = extraIFaceBlackList
|
|
req.DnsLabels = dnsLabelsValidated.ToPunycodeList()
|
|
req.CleanDNSLabels = dnsLabels != nil && len(dnsLabels) == 0
|
|
req.CleanNATExternalIPs = natExternalIPs != nil && len(natExternalIPs) == 0
|
|
|
|
if cmd.Flag(enableRosenpassFlag).Changed {
|
|
req.RosenpassEnabled = &rosenpassEnabled
|
|
}
|
|
if cmd.Flag(rosenpassPermissiveFlag).Changed {
|
|
req.RosenpassPermissive = &rosenpassPermissive
|
|
}
|
|
setSSHSetConfigFields(&req, cmd)
|
|
setBoolPtrIfChanged(cmd, remoteJobsAllowedFlag, &req.RemoteJobsAllowed, remoteJobsAllowed)
|
|
|
|
if cmd.Flag(interfaceNameFlag).Changed {
|
|
if err := parseInterfaceName(interfaceName); err != nil {
|
|
log.Errorf("parse interface name: %v", err)
|
|
return nil
|
|
}
|
|
req.InterfaceName = &interfaceName
|
|
}
|
|
if cmd.Flag(wireguardPortFlag).Changed {
|
|
p := int64(wireguardPort)
|
|
req.WireguardPort = &p
|
|
}
|
|
|
|
if cmd.Flag(mtuFlag).Changed {
|
|
m := int64(mtu)
|
|
req.Mtu = &m
|
|
}
|
|
|
|
if cmd.Flag(networkMonitorFlag).Changed {
|
|
req.NetworkMonitor = &networkMonitor
|
|
}
|
|
if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) {
|
|
req.OptionalPreSharedKey = &preSharedKey
|
|
}
|
|
if cmd.Flag(disableAutoConnectFlag).Changed {
|
|
req.DisableAutoConnect = &autoConnectDisabled
|
|
}
|
|
|
|
if cmd.Flag(dnsRouteIntervalFlag).Changed {
|
|
req.DnsRouteInterval = durationpb.New(dnsRouteInterval)
|
|
}
|
|
|
|
if cmd.Flag(disableClientRoutesFlag).Changed {
|
|
req.DisableClientRoutes = &disableClientRoutes
|
|
}
|
|
|
|
if cmd.Flag(disableServerRoutesFlag).Changed {
|
|
req.DisableServerRoutes = &disableServerRoutes
|
|
}
|
|
|
|
if cmd.Flag(disableDNSFlag).Changed {
|
|
req.DisableDns = &disableDNS
|
|
}
|
|
|
|
if cmd.Flag(disableFirewallFlag).Changed {
|
|
req.DisableFirewall = &disableFirewall
|
|
}
|
|
|
|
if cmd.Flag(blockLANAccessFlag).Changed {
|
|
req.BlockLanAccess = &blockLANAccess
|
|
}
|
|
|
|
if cmd.Flag(blockInboundFlag).Changed {
|
|
req.BlockInbound = &blockInbound
|
|
}
|
|
|
|
if cmd.Flag(disableIPv6Flag).Changed {
|
|
req.DisableIpv6 = &disableIPv6
|
|
}
|
|
|
|
if cmd.Flag(enableLocalMetricsFlag).Changed {
|
|
req.EnableLocalMetrics = &localMetricsEnabled
|
|
}
|
|
if cmd.Flag(localMetricsAddressFlag).Changed {
|
|
req.LocalMetricsAddress = &localMetricsAddr
|
|
}
|
|
|
|
return &req
|
|
}
|
|
|
|
func setupConfig(customDNSAddressConverted []byte, cmd *cobra.Command, configFilePath string) (*profilemanager.ConfigInput, error) {
|
|
ic := profilemanager.ConfigInput{
|
|
ManagementURL: managementURL,
|
|
ConfigPath: configFilePath,
|
|
NATExternalIPs: natExternalIPs,
|
|
CustomDNSAddress: customDNSAddressConverted,
|
|
ExtraIFaceBlackList: extraIFaceBlackList,
|
|
DNSLabels: dnsLabelsValidated,
|
|
}
|
|
|
|
if cmd.Flag(enableRosenpassFlag).Changed {
|
|
ic.RosenpassEnabled = &rosenpassEnabled
|
|
}
|
|
|
|
if cmd.Flag(rosenpassPermissiveFlag).Changed {
|
|
ic.RosenpassPermissive = &rosenpassPermissive
|
|
}
|
|
|
|
if cmd.Flag(serverSSHAllowedFlag).Changed {
|
|
ic.ServerSSHAllowed = &serverSSHAllowed
|
|
}
|
|
setBoolPtrIfChanged(cmd, remoteJobsAllowedFlag, &ic.RemoteJobsAllowed, remoteJobsAllowed)
|
|
|
|
if cmd.Flag(enableSSHRootFlag).Changed {
|
|
ic.EnableSSHRoot = &enableSSHRoot
|
|
}
|
|
|
|
if cmd.Flag(enableSSHSFTPFlag).Changed {
|
|
ic.EnableSSHSFTP = &enableSSHSFTP
|
|
}
|
|
|
|
if cmd.Flag(enableSSHLocalPortForwardFlag).Changed {
|
|
ic.EnableSSHLocalPortForwarding = &enableSSHLocalPortForward
|
|
}
|
|
|
|
if cmd.Flag(enableSSHRemotePortForwardFlag).Changed {
|
|
ic.EnableSSHRemotePortForwarding = &enableSSHRemotePortForward
|
|
}
|
|
|
|
if cmd.Flag(disableSSHAuthFlag).Changed {
|
|
ic.DisableSSHAuth = &disableSSHAuth
|
|
}
|
|
|
|
if cmd.Flag(sshJWTCacheTTLFlag).Changed {
|
|
ic.SSHJWTCacheTTL = &sshJWTCacheTTL
|
|
}
|
|
|
|
if cmd.Flag(interfaceNameFlag).Changed {
|
|
if err := parseInterfaceName(interfaceName); err != nil {
|
|
return nil, err
|
|
}
|
|
ic.InterfaceName = &interfaceName
|
|
}
|
|
|
|
if cmd.Flag(wireguardPortFlag).Changed {
|
|
p := int(wireguardPort)
|
|
ic.WireguardPort = &p
|
|
}
|
|
|
|
if cmd.Flag(mtuFlag).Changed {
|
|
if err := iface.ValidateMTU(mtu); err != nil {
|
|
return nil, err
|
|
}
|
|
ic.MTU = &mtu
|
|
}
|
|
|
|
if cmd.Flag(networkMonitorFlag).Changed {
|
|
ic.NetworkMonitor = &networkMonitor
|
|
}
|
|
|
|
if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) {
|
|
ic.PreSharedKey = &preSharedKey
|
|
}
|
|
|
|
if cmd.Flag(disableAutoConnectFlag).Changed {
|
|
ic.DisableAutoConnect = &autoConnectDisabled
|
|
|
|
if autoConnectDisabled {
|
|
cmd.Println("Autoconnect has been disabled. The client won't connect automatically when the service starts.")
|
|
}
|
|
|
|
if !autoConnectDisabled {
|
|
cmd.Println("Autoconnect has been enabled. The client will connect automatically when the service starts.")
|
|
}
|
|
}
|
|
|
|
if cmd.Flag(dnsRouteIntervalFlag).Changed {
|
|
ic.DNSRouteInterval = &dnsRouteInterval
|
|
}
|
|
|
|
if cmd.Flag(disableClientRoutesFlag).Changed {
|
|
ic.DisableClientRoutes = &disableClientRoutes
|
|
}
|
|
if cmd.Flag(disableServerRoutesFlag).Changed {
|
|
ic.DisableServerRoutes = &disableServerRoutes
|
|
}
|
|
if cmd.Flag(disableDNSFlag).Changed {
|
|
ic.DisableDNS = &disableDNS
|
|
}
|
|
if cmd.Flag(disableFirewallFlag).Changed {
|
|
ic.DisableFirewall = &disableFirewall
|
|
}
|
|
|
|
if cmd.Flag(blockLANAccessFlag).Changed {
|
|
ic.BlockLANAccess = &blockLANAccess
|
|
}
|
|
|
|
if cmd.Flag(blockInboundFlag).Changed {
|
|
ic.BlockInbound = &blockInbound
|
|
}
|
|
|
|
if cmd.Flag(disableIPv6Flag).Changed {
|
|
ic.DisableIPv6 = &disableIPv6
|
|
}
|
|
|
|
if cmd.Flag(enableLocalMetricsFlag).Changed {
|
|
ic.LocalMetricsEnabled = &localMetricsEnabled
|
|
}
|
|
|
|
if cmd.Flag(localMetricsAddressFlag).Changed {
|
|
ic.LocalMetricsAddress = &localMetricsAddr
|
|
}
|
|
|
|
return &ic, nil
|
|
}
|
|
|
|
// setSSHLoginFields copies the SSH server flags the user actually passed
|
|
// into req, leaving the rest unset so the daemon keeps the persisted
|
|
// values.
|
|
func setSSHLoginFields(req *proto.LoginRequest, cmd *cobra.Command) {
|
|
if cmd.Flag(serverSSHAllowedFlag).Changed {
|
|
req.ServerSSHAllowed = &serverSSHAllowed
|
|
}
|
|
if cmd.Flag(enableSSHRootFlag).Changed {
|
|
req.EnableSSHRoot = &enableSSHRoot
|
|
}
|
|
if cmd.Flag(enableSSHSFTPFlag).Changed {
|
|
req.EnableSSHSFTP = &enableSSHSFTP
|
|
}
|
|
if cmd.Flag(enableSSHLocalPortForwardFlag).Changed {
|
|
req.EnableSSHLocalPortForwarding = &enableSSHLocalPortForward
|
|
}
|
|
if cmd.Flag(enableSSHRemotePortForwardFlag).Changed {
|
|
req.EnableSSHRemotePortForwarding = &enableSSHRemotePortForward
|
|
}
|
|
if cmd.Flag(disableSSHAuthFlag).Changed {
|
|
req.DisableSSHAuth = &disableSSHAuth
|
|
}
|
|
if cmd.Flag(sshJWTCacheTTLFlag).Changed {
|
|
sshJWTCacheTTL32 := int32(sshJWTCacheTTL)
|
|
req.SshJWTCacheTTL = &sshJWTCacheTTL32
|
|
}
|
|
}
|
|
|
|
func setupLoginRequest(providedSetupKey string, customDNSAddressConverted []byte, cmd *cobra.Command) (*proto.LoginRequest, error) {
|
|
loginRequest := proto.LoginRequest{
|
|
SetupKey: providedSetupKey,
|
|
ManagementUrl: managementURL,
|
|
NatExternalIPs: natExternalIPs,
|
|
CleanNATExternalIPs: natExternalIPs != nil && len(natExternalIPs) == 0,
|
|
CustomDNSAddress: customDNSAddressConverted,
|
|
IsUnixDesktopClient: util.HasGraphicalSession(),
|
|
Hostname: hostName,
|
|
ExtraIFaceBlacklist: extraIFaceBlackList,
|
|
DnsLabels: dnsLabels,
|
|
CleanDNSLabels: dnsLabels != nil && len(dnsLabels) == 0,
|
|
}
|
|
|
|
if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) {
|
|
loginRequest.OptionalPreSharedKey = &preSharedKey
|
|
}
|
|
|
|
if cmd.Flag(enableRosenpassFlag).Changed {
|
|
loginRequest.RosenpassEnabled = &rosenpassEnabled
|
|
}
|
|
|
|
if cmd.Flag(rosenpassPermissiveFlag).Changed {
|
|
loginRequest.RosenpassPermissive = &rosenpassPermissive
|
|
}
|
|
|
|
setSSHLoginFields(&loginRequest, cmd)
|
|
setBoolPtrIfChanged(cmd, remoteJobsAllowedFlag, &loginRequest.RemoteJobsAllowed, remoteJobsAllowed)
|
|
|
|
if cmd.Flag(disableAutoConnectFlag).Changed {
|
|
loginRequest.DisableAutoConnect = &autoConnectDisabled
|
|
}
|
|
|
|
if cmd.Flag(enableLocalMetricsFlag).Changed {
|
|
loginRequest.EnableLocalMetrics = &localMetricsEnabled
|
|
}
|
|
|
|
if cmd.Flag(localMetricsAddressFlag).Changed {
|
|
loginRequest.LocalMetricsAddress = &localMetricsAddr
|
|
}
|
|
|
|
if cmd.Flag(interfaceNameFlag).Changed {
|
|
if err := parseInterfaceName(interfaceName); err != nil {
|
|
return nil, err
|
|
}
|
|
loginRequest.InterfaceName = &interfaceName
|
|
}
|
|
|
|
if cmd.Flag(wireguardPortFlag).Changed {
|
|
wp := int64(wireguardPort)
|
|
loginRequest.WireguardPort = &wp
|
|
}
|
|
|
|
if cmd.Flag(mtuFlag).Changed {
|
|
if err := iface.ValidateMTU(mtu); err != nil {
|
|
return nil, err
|
|
}
|
|
m := int64(mtu)
|
|
loginRequest.Mtu = &m
|
|
}
|
|
|
|
if cmd.Flag(networkMonitorFlag).Changed {
|
|
loginRequest.NetworkMonitor = &networkMonitor
|
|
}
|
|
|
|
if cmd.Flag(dnsRouteIntervalFlag).Changed {
|
|
loginRequest.DnsRouteInterval = durationpb.New(dnsRouteInterval)
|
|
}
|
|
|
|
if cmd.Flag(disableClientRoutesFlag).Changed {
|
|
loginRequest.DisableClientRoutes = &disableClientRoutes
|
|
}
|
|
if cmd.Flag(disableServerRoutesFlag).Changed {
|
|
loginRequest.DisableServerRoutes = &disableServerRoutes
|
|
}
|
|
if cmd.Flag(disableDNSFlag).Changed {
|
|
loginRequest.DisableDns = &disableDNS
|
|
}
|
|
if cmd.Flag(disableFirewallFlag).Changed {
|
|
loginRequest.DisableFirewall = &disableFirewall
|
|
}
|
|
|
|
if cmd.Flag(blockLANAccessFlag).Changed {
|
|
loginRequest.BlockLanAccess = &blockLANAccess
|
|
}
|
|
|
|
if cmd.Flag(blockInboundFlag).Changed {
|
|
loginRequest.BlockInbound = &blockInbound
|
|
}
|
|
|
|
if cmd.Flag(disableIPv6Flag).Changed {
|
|
loginRequest.DisableIpv6 = &disableIPv6
|
|
}
|
|
|
|
return &loginRequest, nil
|
|
}
|
|
|
|
func validateNATExternalIPs(list []string) error {
|
|
for _, element := range list {
|
|
if element == "" {
|
|
return fmt.Errorf("empty string is not a valid input for %s", externalIPMapFlag)
|
|
}
|
|
|
|
subElements := strings.Split(element, "/")
|
|
if len(subElements) > 2 {
|
|
return fmt.Errorf("%s is not a valid input for %s. it should be formatted as \"String\" or \"String/String\"", element, externalIPMapFlag)
|
|
}
|
|
|
|
if len(subElements) == 1 && !isValidIP(subElements[0]) {
|
|
return fmt.Errorf("%s is not a valid input for %s. it should be formatted as \"IP\" or \"IP/IP\", or \"IP/Interface Name\"", element, externalIPMapFlag)
|
|
}
|
|
|
|
last := 0
|
|
for _, singleElement := range subElements {
|
|
inputType, err := validateElement(singleElement)
|
|
if err != nil {
|
|
return fmt.Errorf("%s is not a valid input for %s. it should be an IP string or a network name", singleElement, externalIPMapFlag)
|
|
}
|
|
if last == interfaceInputType && inputType == interfaceInputType {
|
|
return fmt.Errorf("%s is not a valid input for %s. it should not contain two interface names", element, externalIPMapFlag)
|
|
}
|
|
last = inputType
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func parseInterfaceName(name string) error {
|
|
if runtime.GOOS != "darwin" {
|
|
return nil
|
|
}
|
|
|
|
if strings.HasPrefix(name, "utun") {
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("invalid interface name %s. Please use the prefix utun followed by a number on MacOS. e.g., utun1 or utun199", name)
|
|
}
|
|
|
|
func validateElement(element string) (int, error) {
|
|
if isValidIP(element) {
|
|
return ipInputType, nil
|
|
}
|
|
validIface, err := isValidInterface(element)
|
|
if err != nil {
|
|
return invalidInputType, fmt.Errorf("unable to validate the network interface name, error: %s", err)
|
|
}
|
|
|
|
if validIface {
|
|
return interfaceInputType, nil
|
|
}
|
|
|
|
return interfaceInputType, fmt.Errorf("invalid IP or network interface name not found")
|
|
}
|
|
|
|
func isValidIP(ip string) bool {
|
|
return net.ParseIP(ip) != nil
|
|
}
|
|
|
|
func isValidInterface(name string) (bool, error) {
|
|
netInterfaces, err := net.Interfaces()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
for _, iface := range netInterfaces {
|
|
if iface.Name == name {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func parseCustomDNSAddress(modified bool) ([]byte, error) {
|
|
var parsed []byte
|
|
if modified {
|
|
if !isValidAddrPort(customDNSAddress) {
|
|
return nil, fmt.Errorf("%s is invalid, it should be formatted as IP:Port string or as an empty string like \"\"", customDNSAddress)
|
|
}
|
|
if customDNSAddress == "" && util.FindFirstLogPath(logFiles) != "" {
|
|
parsed = []byte("empty")
|
|
} else {
|
|
parsed = []byte(customDNSAddress)
|
|
}
|
|
}
|
|
return parsed, nil
|
|
}
|
|
|
|
func validateDnsLabels(labels []string) (domain.List, error) {
|
|
var (
|
|
domains domain.List
|
|
err error
|
|
)
|
|
|
|
if len(labels) == 0 {
|
|
return domains, nil
|
|
}
|
|
|
|
domains, err = domain.ValidateDomains(labels)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to validate dns labels: %v", err)
|
|
}
|
|
|
|
return domains, nil
|
|
}
|
|
|
|
func isValidAddrPort(input string) bool {
|
|
if input == "" {
|
|
return true
|
|
}
|
|
_, err := netip.ParseAddrPort(input)
|
|
return err == nil
|
|
}
|
|
|
|
// daemonActiveProfileForUser returns the profile the daemon currently holds for
|
|
// username, for the no --profile case where the local mirror is not
|
|
// authoritative (sudo or plain root). It returns that profile when the daemon
|
|
// owns it for this user or when the profile is unowned (empty username, as on a
|
|
// fresh install), so the caller acts on the daemon's real state instead of the
|
|
// stale mirror. It denies with a --profile hint when the daemon is on another
|
|
// user's profile, when the lookup fails, or when the daemon reports no active
|
|
// profile. Returns errDaemonActiveProfileUnsupported when the daemon predates
|
|
// the RPC; the caller keeps the mirror-derived profile in that case.
|
|
func daemonActiveProfileForUser(ctx context.Context, client proto.DaemonServiceClient, username string) (*profilemanager.Profile, error) {
|
|
active, err := client.GetActiveProfile(ctx, &proto.GetActiveProfileRequest{})
|
|
if err != nil {
|
|
if st, ok := gstatus.FromError(err); ok && st.Code() == codes.Unimplemented {
|
|
return nil, fmt.Errorf("%w: %v", errDaemonActiveProfileUnsupported, err)
|
|
}
|
|
return nil, fmt.Errorf("pass --profile to choose the profile explicitly: the daemon's active profile could not be verified: %v", err)
|
|
}
|
|
if active.GetId() == "" {
|
|
return nil, fmt.Errorf("pass --profile to choose the profile explicitly: the daemon reported no active profile")
|
|
}
|
|
if active.GetUsername() != "" && active.GetUsername() != username {
|
|
return nil, fmt.Errorf(
|
|
"pass --profile to choose the profile explicitly: the daemon's active profile is %q (user %q) but this invocation runs for %q",
|
|
active.GetProfileName(), active.GetUsername(), username)
|
|
}
|
|
return &profilemanager.Profile{ID: profilemanager.ID(active.GetId())}, nil
|
|
}
|