From 1f3239ea00e1eba17c594fa33b7d99fc94da972d Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 24 Sep 2026 17:25:27 -0400 Subject: [PATCH 01/14] Implement exit node selection --- cmd/select/exitnode/exitnode.go | 199 ++++++++++++++++++++++++++++++ cmd/select/select.go | 2 + cmd/up/client/client.go | 17 +++ docs/pangolin_select.md | 1 + docs/pangolin_select_exit-node.md | 27 ++++ docs/pangolin_up.md | 47 +++---- docs/pangolin_up_client.md | 47 +++---- go.mod | 9 +- go.sum | 10 +- internal/api/client.go | 34 +++++ internal/api/types.go | 26 ++++ internal/config/config.go | 10 ++ internal/olm/client.go | 56 +++++++++ 13 files changed, 433 insertions(+), 52 deletions(-) create mode 100644 cmd/select/exitnode/exitnode.go create mode 100644 docs/pangolin_select_exit-node.md diff --git a/cmd/select/exitnode/exitnode.go b/cmd/select/exitnode/exitnode.go new file mode 100644 index 0000000..5745322 --- /dev/null +++ b/cmd/select/exitnode/exitnode.go @@ -0,0 +1,199 @@ +package exitnode + +import ( + "fmt" + "os" + "strings" + + "github.com/charmbracelet/huh" + "github.com/fosrl/cli/internal/api" + "github.com/fosrl/cli/internal/config" + "github.com/fosrl/cli/internal/logger" + "github.com/fosrl/cli/internal/olm" + "github.com/fosrl/cli/internal/utils" + "github.com/spf13/cobra" +) + +type ExitNodeCmdOpts struct { + ExitNode string +} + +// disableChoice is the menu value for the "disable gateway" option. +const disableChoice = -1 + +func ExitNodeCmd() *cobra.Command { + opts := ExitNodeCmdOpts{} + + cmd := &cobra.Command{ + Use: "exit-node", + Short: "Route all traffic through an exit node", + Long: `List the exit nodes in your organization and select one to route all +tunnel traffic (full tunnel) through the sites backing it. + +While an exit node is active, a "None" option is shown to turn it off. +Requires a running client.`, + Run: func(cmd *cobra.Command, args []string) { + if err := exitNodeMain(cmd, &opts); err != nil { + os.Exit(1) + } + }, + } + + cmd.Flags().StringVar(&opts.ExitNode, "exit-node", "", "Exit node `NICE-ID` to select") + + return cmd +} + +func exitNodeMain(cmd *cobra.Command, opts *ExitNodeCmdOpts) error { + olmClient := olm.NewClient("") + if !olmClient.IsRunning() { + err := fmt.Errorf("no client is currently running; start one with 'pangolin up'") + logger.Error("%v", err) + return err + } + + status, err := olmClient.GetStatus() + if err != nil { + logger.Error("Failed to get client status: %v", err) + return err + } + + cfg := config.ConfigFromContext(cmd.Context()) + apiClient := api.FromContext(cmd.Context()) + accountStore := config.AccountStoreFromContext(cmd.Context()) + + orgID, err := utils.ResolveOrgID(accountStore, "") + if err != nil { + logger.Error("%v", err) + return err + } + + gateways, err := apiClient.ListGatewayResources(orgID) + if err != nil { + logger.Error("Failed to list exit nodes: %v", err) + return err + } + + usable := gateways[:0] + for _, g := range gateways { + if g.Enabled && len(g.SiteIDs) > 0 { + usable = append(usable, g) + } + } + // A saved exit node can outlive the active one (e.g. its sites weren't + // connected on startup), so offer to clear it either way. + hasGateway := status.GatewayActive || len(cfg.Up.GatewaySiteIDs) > 0 + if len(usable) == 0 && !hasGateway { + err := fmt.Errorf("no exit nodes available in this organization") + logger.Error("%v", err) + return err + } + + choice := disableChoice + if opts.ExitNode != "" { + choice = -2 + for i, g := range usable { + if g.NiceID == opts.ExitNode { + choice = i + break + } + } + if choice == -2 { + err := fmt.Errorf("exit node '%s' not found or not available", opts.ExitNode) + logger.Error("%v", err) + return err + } + } else { + choice, err = selectExitNodeForm(usable, status, hasGateway) + if err != nil { + logger.Error("%v", err) + return err + } + } + + if choice == disableChoice { + if status.GatewayActive { + if _, err := olmClient.DisableGateway(); err != nil { + logger.Error("Failed to disable exit node: %v", err) + return err + } + } + saveGateway(cfg, []int{}) + logger.Success("Exit node disabled") + return nil + } + + selected := usable[choice] + if _, err := olmClient.SelectGateway(selected.SiteIDs); err != nil { + logger.Error("Failed to select exit node: %v", err) + return err + } + saveGateway(cfg, selected.SiteIDs) + + logger.Success("Routing all traffic through exit node: %s", selected.Name) + return nil +} + +// saveGateway persists the exit node so the next `pangolin up` re-applies it. +// A failure is only a warning: the change is already live on the client. +func saveGateway(cfg *config.Config, siteIDs []int) { + cfg.Up.GatewaySiteIDs = siteIDs + if err := cfg.Save(); err != nil { + logger.Warning("Exit node applied but could not be saved for the next start: %v", err) + } +} + +// selectExitNodeForm returns the index of the chosen gateway, or disableChoice. +func selectExitNodeForm(gateways []api.SiteResource, status *olm.StatusResponse, hasGateway bool) (int, error) { + options := make([]huh.Option[int], 0, len(gateways)+1) + if hasGateway { + options = append(options, huh.NewOption("None (disable exit node)", disableChoice)) + } + for i, g := range gateways { + label := fmt.Sprintf("%s (%s)", g.Name, g.NiceID) + if len(g.SiteNames) > 0 { + label += " - " + strings.Join(g.SiteNames, ", ") + } + if status.GatewayActive && sameSites(g.SiteIDs, status.GatewaySiteIDs) { + label += " [active]" + } + options = append(options, huh.NewOption(label, i)) + } + + // huh starts the cursor on the option matching this value, so default to + // "None" when it's offered; otherwise it would start on the first exit + // node with "None" scrolled out of view above it. + selected := 0 + if hasGateway { + selected = disableChoice + } + form := huh.NewForm( + huh.NewGroup( + huh.NewSelect[int](). + Title("Select an exit node"). + Options(options...). + Value(&selected), + ), + ) + if err := form.Run(); err != nil { + return 0, fmt.Errorf("error selecting exit node: %w", err) + } + + return selected, nil +} + +func sameSites(a, b []int) bool { + if len(a) != len(b) { + return false + } + set := make(map[int]struct{}, len(a)) + for _, id := range a { + set[id] = struct{}{} + } + for _, id := range b { + if _, ok := set[id]; !ok { + return false + } + } + return true +} diff --git a/cmd/select/select.go b/cmd/select/select.go index f2a21d4..400cc03 100644 --- a/cmd/select/select.go +++ b/cmd/select/select.go @@ -2,6 +2,7 @@ package selectcmd import ( "github.com/fosrl/cli/cmd/select/account" + "github.com/fosrl/cli/cmd/select/exitnode" "github.com/fosrl/cli/cmd/select/org" "github.com/spf13/cobra" ) @@ -15,6 +16,7 @@ func SelectCmd() *cobra.Command { cmd.AddCommand(account.AccountCmd()) cmd.AddCommand(org.OrgCmd()) + cmd.AddCommand(exitnode.ExitNodeCmd()) return cmd } diff --git a/cmd/up/client/client.go b/cmd/up/client/client.go index 9d6ff83..eed179c 100644 --- a/cmd/up/client/client.go +++ b/cmd/up/client/client.go @@ -11,6 +11,7 @@ import ( "os/signal" "path/filepath" "runtime" + "strconv" "strings" "syscall" "time" @@ -56,6 +57,7 @@ type ClientUpCmdOpts struct { PreferLocalRoutes bool DisableRelay bool SubnetRouter bool + GatewaySiteIDs []int } // validateDNSIP ensures the given DNS server string is a valid IP address. @@ -191,6 +193,7 @@ logs, and removes the service again when you press Ctrl+C.`, cmd.Flags().BoolVar(&opts.PreferLocalRoutes, "prefer-local-routes", false, "Add tunnel routes with a high metric so overlapping local/connected routes take precedence (default false)") cmd.Flags().BoolVar(&opts.DisableRelay, "disable-relay", false, "Disable relay connections (default false)") cmd.Flags().BoolVar(&opts.SubnetRouter, "subnet-router", false, "Enable this client to act as a subnet router: traffic forwarded from the local network is NATed to this client's own tunnel IP before going out over the tunnel. Linux only, requires CAP_NET_ADMIN. (default false)") + cmd.Flags().IntSliceVar(&opts.GatewaySiteIDs, "exit-node-site-ids", nil, "Site IDs to route all traffic through as an exit node (default: the exit node saved by 'pangolin select exit-node', if any)") cmd.Flags().BoolVar(&opts.Attached, "attach", false, "Run in attached (foreground) mode, (default: detached (background) mode)") cmd.Flags().BoolVar(&opts.Silent, "silent", false, "Disable TUI and run silently when detached") @@ -305,6 +308,12 @@ func clientUpMain(cmd *cobra.Command, opts *ClientUpCmdOpts, extraArgs []string) opts.MatchDomains = cfg.GetStringSlice("up.match_domains_dns") } + // Same as match-domains: resolved here so it can be forwarded to the + // subprocess, which may not have access to the user's config. + if !cmd.Flags().Changed("exit-node-site-ids") && len(cfg.Up.GatewaySiteIDs) > 0 { + opts.GatewaySiteIDs = cfg.Up.GatewaySiteIDs + } + // Check if a client is already running olmClient := olm.NewClient("") if olmClient.IsRunning() { @@ -521,6 +530,13 @@ func clientUpMain(cmd *cobra.Command, opts *ClientUpCmdOpts, extraArgs []string) if opts.SubnetRouter { cmdArgs = append(cmdArgs, "--subnet-router") } + if len(opts.GatewaySiteIDs) > 0 { + ids := make([]string, len(opts.GatewaySiteIDs)) + for i, id := range opts.GatewaySiteIDs { + ids[i] = strconv.Itoa(id) + } + cmdArgs = append(cmdArgs, "--exit-node-site-ids", strings.Join(ids, ",")) + } // Add positional args if any cmdArgs = append(cmdArgs, extraArgs...) @@ -766,6 +782,7 @@ func clientUpMain(cmd *cobra.Command, opts *ClientUpCmdOpts, extraArgs []string) MatchDomains: opts.MatchDomains, PreferLocalRoutes: opts.PreferLocalRoutes, DisableRelay: opts.DisableRelay, + GatewaySiteIds: opts.GatewaySiteIDs, // SubnetRouter: opts.SubnetRouter, UserToken: userToken, InitialFingerprint: initialFingerprint, diff --git a/docs/pangolin_select.md b/docs/pangolin_select.md index 30c8694..9f1c629 100644 --- a/docs/pangolin_select.md +++ b/docs/pangolin_select.md @@ -16,5 +16,6 @@ Select account information to use * [pangolin](pangolin.md) - Pangolin CLI * [pangolin select account](pangolin_select_account.md) - Select an account +* [pangolin select exit-node](pangolin_select_exit-node.md) - Route all traffic through an exit node * [pangolin select org](pangolin_select_org.md) - Select an organization diff --git a/docs/pangolin_select_exit-node.md b/docs/pangolin_select_exit-node.md new file mode 100644 index 0000000..0b963ce --- /dev/null +++ b/docs/pangolin_select_exit-node.md @@ -0,0 +1,27 @@ +## pangolin select exit-node + +Route all traffic through an exit node + +### Synopsis + +List the exit nodes in your organization and select one to route all +tunnel traffic (full tunnel) through the sites backing it. + +While an exit node is active, a "None" option is shown to turn it off. +Requires a running client. + +``` +pangolin select exit-node [flags] +``` + +### Options + +``` + --exit-node NICE-ID Exit node NICE-ID to select + -h, --help help for exit-node +``` + +### SEE ALSO + +* [pangolin select](pangolin_select.md) - Select account information to use + diff --git a/docs/pangolin_up.md b/docs/pangolin_up.md index 00d428f..c05b63e 100644 --- a/docs/pangolin_up.md +++ b/docs/pangolin_up.md @@ -16,29 +16,30 @@ pangolin up [flags] ### Options ``` - --attach Run in attached (foreground) mode, (default: detached (background) mode) - --disable-relay Disable relay connections (default false) - --endpoint string Client endpoint (required if not logged in) - -h, --help help for up - --holepunch Enable holepunching (default true) - --http-addr string HTTP address for API server - --id string Client ID (optional, will use user info if not provided) - --interface-name name Interface name (default "pangolin") - --log-level string Log level (default "info") - --match-domains strings FQDN wildcard patterns (e.g. '*.proxy.internal') to check against local records/upstream DNS; queries for non-matching domains go directly to the system's DNS servers (default: match all domains, or the value from config if set) - --mtu int Maximum transmission unit (default 1280) - --netstack-dns server DNS server to use for Netstack. This handles DNS resolution outside of the upstream servers. - --org string Organization ID (default: selected organization if logged in) - --override-dns When enabled, the client uses custom DNS servers to resolve internal resources and aliases. This overrides your system's default DNS settings. Queries that cannot be resolved as a Pangolin resource will be forwarded to your configured Upstream DNS Server. (default true) - --ping-interval interval Ping interval (default 5s) - --ping-timeout timeout Ping timeout (default 5s) - --prefer-local-routes Add tunnel routes with a high metric so overlapping local/connected routes take precedence (default false) - --secret string Client secret (optional, will use user info if not provided) - --silent Disable TUI and run silently when detached - --subnet-router Enable this client to act as a subnet router: traffic forwarded from the local network is NATed to this client's own tunnel IP before going out over the tunnel. Linux only, requires CAP_NET_ADMIN. (default false) - --tls-client-cert path TLS client certificate path - --tunnel-dns When enabled, DNS queries are routed through the tunnel for remote resolution. To ensure queries are tunneled correctly, you must define the DNS server as a Pangolin resource and enter its address as an Upstream DNS Server. - --upstream-dns strings List of DNS servers to use for external DNS resolution if overriding system DNS + --attach Run in attached (foreground) mode, (default: detached (background) mode) + --disable-relay Disable relay connections (default false) + --endpoint string Client endpoint (required if not logged in) + --exit-node-site-ids ints Site IDs to route all traffic through as an exit node (default: the exit node saved by 'pangolin select exit-node', if any) + -h, --help help for up + --holepunch Enable holepunching (default true) + --http-addr string HTTP address for API server + --id string Client ID (optional, will use user info if not provided) + --interface-name name Interface name (default "pangolin") + --log-level string Log level (default "info") + --match-domains strings FQDN wildcard patterns (e.g. '*.proxy.internal') to check against local records/upstream DNS; queries for non-matching domains go directly to the system's DNS servers (default: match all domains, or the value from config if set) + --mtu int Maximum transmission unit (default 1280) + --netstack-dns server DNS server to use for Netstack. This handles DNS resolution outside of the upstream servers. + --org string Organization ID (default: selected organization if logged in) + --override-dns When enabled, the client uses custom DNS servers to resolve internal resources and aliases. This overrides your system's default DNS settings. Queries that cannot be resolved as a Pangolin resource will be forwarded to your configured Upstream DNS Server. (default true) + --ping-interval interval Ping interval (default 5s) + --ping-timeout timeout Ping timeout (default 5s) + --prefer-local-routes Add tunnel routes with a high metric so overlapping local/connected routes take precedence (default false) + --secret string Client secret (optional, will use user info if not provided) + --silent Disable TUI and run silently when detached + --subnet-router Enable this client to act as a subnet router: traffic forwarded from the local network is NATed to this client's own tunnel IP before going out over the tunnel. Linux only, requires CAP_NET_ADMIN. (default false) + --tls-client-cert path TLS client certificate path + --tunnel-dns When enabled, DNS queries are routed through the tunnel for remote resolution. To ensure queries are tunneled correctly, you must define the DNS server as a Pangolin resource and enter its address as an Upstream DNS Server. + --upstream-dns strings List of DNS servers to use for external DNS resolution if overriding system DNS ``` ### SEE ALSO diff --git a/docs/pangolin_up_client.md b/docs/pangolin_up_client.md index 9750ea0..dd079b8 100644 --- a/docs/pangolin_up_client.md +++ b/docs/pangolin_up_client.md @@ -20,29 +20,30 @@ pangolin up client [flags] ### Options ``` - --attach Run in attached (foreground) mode, (default: detached (background) mode) - --disable-relay Disable relay connections (default false) - --endpoint string Client endpoint (required if not logged in) - -h, --help help for client - --holepunch Enable holepunching (default true) - --http-addr string HTTP address for API server - --id string Client ID (optional, will use user info if not provided) - --interface-name name Interface name (default "pangolin") - --log-level string Log level (default "info") - --match-domains strings FQDN wildcard patterns (e.g. '*.proxy.internal') to check against local records/upstream DNS; queries for non-matching domains go directly to the system's DNS servers (default: match all domains, or the value from config if set) - --mtu int Maximum transmission unit (default 1280) - --netstack-dns server DNS server to use for Netstack. This handles DNS resolution outside of the upstream servers. - --org string Organization ID (default: selected organization if logged in) - --override-dns When enabled, the client uses custom DNS servers to resolve internal resources and aliases. This overrides your system's default DNS settings. Queries that cannot be resolved as a Pangolin resource will be forwarded to your configured Upstream DNS Server. (default true) - --ping-interval interval Ping interval (default 5s) - --ping-timeout timeout Ping timeout (default 5s) - --prefer-local-routes Add tunnel routes with a high metric so overlapping local/connected routes take precedence (default false) - --secret string Client secret (optional, will use user info if not provided) - --silent Disable TUI and run silently when detached - --subnet-router Enable this client to act as a subnet router: traffic forwarded from the local network is NATed to this client's own tunnel IP before going out over the tunnel. Linux only, requires CAP_NET_ADMIN. (default false) - --tls-client-cert path TLS client certificate path - --tunnel-dns When enabled, DNS queries are routed through the tunnel for remote resolution. To ensure queries are tunneled correctly, you must define the DNS server as a Pangolin resource and enter its address as an Upstream DNS Server. - --upstream-dns strings List of DNS servers to use for external DNS resolution if overriding system DNS + --attach Run in attached (foreground) mode, (default: detached (background) mode) + --disable-relay Disable relay connections (default false) + --endpoint string Client endpoint (required if not logged in) + --exit-node-site-ids ints Site IDs to route all traffic through as an exit node (default: the exit node saved by 'pangolin select exit-node', if any) + -h, --help help for client + --holepunch Enable holepunching (default true) + --http-addr string HTTP address for API server + --id string Client ID (optional, will use user info if not provided) + --interface-name name Interface name (default "pangolin") + --log-level string Log level (default "info") + --match-domains strings FQDN wildcard patterns (e.g. '*.proxy.internal') to check against local records/upstream DNS; queries for non-matching domains go directly to the system's DNS servers (default: match all domains, or the value from config if set) + --mtu int Maximum transmission unit (default 1280) + --netstack-dns server DNS server to use for Netstack. This handles DNS resolution outside of the upstream servers. + --org string Organization ID (default: selected organization if logged in) + --override-dns When enabled, the client uses custom DNS servers to resolve internal resources and aliases. This overrides your system's default DNS settings. Queries that cannot be resolved as a Pangolin resource will be forwarded to your configured Upstream DNS Server. (default true) + --ping-interval interval Ping interval (default 5s) + --ping-timeout timeout Ping timeout (default 5s) + --prefer-local-routes Add tunnel routes with a high metric so overlapping local/connected routes take precedence (default false) + --secret string Client secret (optional, will use user info if not provided) + --silent Disable TUI and run silently when detached + --subnet-router Enable this client to act as a subnet router: traffic forwarded from the local network is NATed to this client's own tunnel IP before going out over the tunnel. Linux only, requires CAP_NET_ADMIN. (default false) + --tls-client-cert path TLS client certificate path + --tunnel-dns When enabled, DNS queries are routed through the tunnel for remote resolution. To ensure queries are tunneled correctly, you must define the DNS server as a Pangolin resource and enter its address as an Upstream DNS Server. + --upstream-dns strings List of DNS servers to use for external DNS resolution if overriding system DNS ``` ### SEE ALSO diff --git a/go.mod b/go.mod index 2b9d1bf..24c6a0f 100644 --- a/go.mod +++ b/go.mod @@ -57,6 +57,8 @@ require ( github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/godbus/dbus/v5 v5.2.2 // indirect github.com/google/btree v1.1.3 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/nftables v0.3.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 // indirect @@ -64,6 +66,8 @@ require ( github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect + github.com/mdlayher/socket v0.5.1 // indirect github.com/miekg/dns v1.1.70 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect @@ -127,5 +131,6 @@ require ( // If changes to Olm or Newt are required, use these // replace directives during development. // -//replace github.com/fosrl/olm => ../olm -//replace github.com/fosrl/newt => ../newt +replace github.com/fosrl/olm => ../olm + +replace github.com/fosrl/newt => ../newt diff --git a/go.sum b/go.sum index 9b52c2b..657104a 100644 --- a/go.sum +++ b/go.sum @@ -74,10 +74,6 @@ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= -github.com/fosrl/newt v1.17.0 h1:bI0bfE3rmEbmKq2yoiUw0ZHRGFIWIQ7ywYfG1GMo5lM= -github.com/fosrl/newt v1.17.0/go.mod h1:CwcuQtifgDQeSWSEB3yfqOgheFv9yltVBYQNgDB/DoM= -github.com/fosrl/olm v1.9.1 h1:mCZ+rVJnRHSJqcF8ThDJgcDaJicCmHF1lSh03/VoSno= -github.com/fosrl/olm v1.9.1/go.mod h1:8xS3GYjDuPuVbt7ANROC6sIOm2HWAM85wIad/ii7xko= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= @@ -103,6 +99,8 @@ github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/nftables v0.3.0 h1:bkyZ0cbpVeMHXOrtlFc8ISmfVqq5gPJukoYieyVmITg= +github.com/google/nftables v0.3.0/go.mod h1:BCp9FsrbF1Fn/Yu6CLUc9GGZFw/+hsxfluNXXmxBfRM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= @@ -127,6 +125,10 @@ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2J github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg= +github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o= +github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= +github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= github.com/miekg/dns v1.1.70 h1:DZ4u2AV35VJxdD9Fo9fIWm119BsQL5cZU1cQ9s0LkqA= github.com/miekg/dns v1.1.70/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= diff --git a/internal/api/client.go b/internal/api/client.go index 82e4632..f8ac1c5 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -436,6 +436,40 @@ func (c *Client) ListLauncherResources(orgID string, opts ListLauncherResourcesO return &data, nil } +// ListGatewayResources returns every gateway-mode site resource in the org +// (with the IDs of the sites backing each one), fetching all pages. +func (c *Client) ListGatewayResources(orgID string) ([]SiteResource, error) { + const pageSize = 100 + path := fmt.Sprintf("/org/%s/site-resources", url.PathEscape(orgID)) + + var gateways []SiteResource + for page := 1; ; page++ { + var data ListSiteResourcesData + err := c.Get(path, &data, RequestOptions{Query: map[string]string{ + "mode": "gateway", + "page": strconv.Itoa(page), + "pageSize": strconv.Itoa(pageSize), + }}) + if err != nil { + return nil, err + } + + // Servers that predate gateway mode ignore the unknown filter value and + // return every resource, so filter again here. + for _, r := range data.SiteResources { + if r.Mode == "gateway" { + gateways = append(gateways, r) + } + } + + if len(data.SiteResources) < pageSize { + break + } + } + + return gateways, nil +} + // GetResourceByNiceID fetches a resource's full details (including resourceGuid, // not present on LauncherResource) by org + niceId. func (c *Client) GetResourceByNiceID(orgID, niceID string) (*GetResourceData, error) { diff --git a/internal/api/types.go b/internal/api/types.go index 4c00d78..e6c8d4b 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -376,6 +376,32 @@ type ListLauncherResourcesData struct { Pagination LauncherPagination `json:"pagination"` } +// SiteResource is the (partial) shape of an entry returned by +// GET /org/:orgId/site-resources. Only the fields the CLI needs are modeled. +type SiteResource struct { + SiteResourceID int `json:"siteResourceId"` + NiceID string `json:"niceId"` + Name string `json:"name"` + Mode string `json:"mode"` + Enabled bool `json:"enabled"` + SiteIDs []int `json:"siteIds"` + SiteNames []string `json:"siteNames"` + SiteOnlines []bool `json:"siteOnlines"` +} + +// SiteResourcePagination matches the paginated API envelope for site resources. +type SiteResourcePagination struct { + Total int `json:"total"` + Page int `json:"page"` + PageSize int `json:"pageSize"` +} + +// ListSiteResourcesData is the inner `data` of GET /org/:orgId/site-resources. +type ListSiteResourcesData struct { + SiteResources []SiteResource `json:"siteResources"` + Pagination SiteResourcePagination `json:"pagination"` +} + // GetResourceData is the (partial) inner `data` of GET /org/:orgId/resource/:niceId. // Only the fields the CLI needs are modeled; the server returns more, which // json.Unmarshal simply ignores. diff --git a/internal/config/config.go b/internal/config/config.go index b7baf74..bb07738 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -51,6 +51,11 @@ type UpConfig struct { // --prefer-local-routes flag when the flag isn't passed explicitly. // Defaults to false. PreferLocalRoutes *bool `mapstructure:"prefer_local_routes" json:"prefer_local_routes,omitempty"` + + // GatewaySiteIDs are the site IDs of the exit node selected with + // `pangolin select exit-node`. `pangolin up` re-applies them at connect time + // so the same exit node is used across restarts. Empty means no exit node. + GatewaySiteIDs []int `mapstructure:"exit_node_site_ids" json:"exit_node_site_ids,omitempty"` } // CompanionAppDataDirs holds per-platform overrides for the desktop app data directory. @@ -362,6 +367,11 @@ func (c *Config) Save() error { if c.Up.PreferLocalRoutes != nil { c.v.Set("up.prefer_local_routes", *c.Up.PreferLocalRoutes) } + // A non-nil empty slice is written as [] so a cleared exit node overrides + // a previously persisted one. + if c.Up.GatewaySiteIDs != nil { + c.v.Set("up.exit_node_site_ids", c.Up.GatewaySiteIDs) + } dir, err := GetPangolinConfigDir() if err != nil { diff --git a/internal/olm/client.go b/internal/olm/client.go index fd9577c..d33a264 100644 --- a/internal/olm/client.go +++ b/internal/olm/client.go @@ -37,6 +37,8 @@ type StatusResponse struct { NetworkSettings map[string]interface{} `json:"networkSettings,omitempty"` Error *StatusError `json:"error,omitempty"` ExitNode *OLMExitNodeStatus `json:"exitNode,omitempty"` + GatewayActive bool `json:"gatewayActive,omitempty"` + GatewaySiteIDs []int `json:"gatewaySiteIds,omitempty"` } // OLMExitNodeStatus represents the connectivity status of the client's own exit @@ -88,6 +90,16 @@ type JITConnectionResponse struct { Status string `json:"status"` } +// GatewayRequest represents a select-gateway request +type GatewayRequest struct { + SiteIDs []int `json:"siteIds"` +} + +// GatewayResponse represents the response from a gateway select/disable request +type GatewayResponse struct { + Status string `json:"status"` +} + // NewClient creates a new OLM socket client func NewClient(socketPath string) *Client { if socketPath == "" { @@ -232,6 +244,50 @@ func (c *Client) JITConnectByResourceID(resourceID string) (*JITConnectionRespon return c.jitConnect(JITConnectionRequest{Resource: resourceID}) } +// SelectGateway routes all tunnel traffic through the given sites (full tunnel). +// Every site must already be a connected peer of the running client. +func (c *Client) SelectGateway(siteIDs []int) (*GatewayResponse, error) { + if len(siteIDs) == 0 { + return nil, fmt.Errorf("at least one site ID is required") + } + + jsonData, err := json.Marshal(GatewayRequest{SiteIDs: siteIDs}) + if err != nil { + return nil, fmt.Errorf("failed to marshal request: %w", err) + } + + resp, err := c.doRequestExpecting("POST", "/gateway/select", bytes.NewBuffer(jsonData), map[string]string{ + "Content-Type": "application/json", + }, http.StatusAccepted) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var gwResp GatewayResponse + if err := json.NewDecoder(resp.Body).Decode(&gwResp); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + return &gwResp, nil +} + +// DisableGateway stops routing all tunnel traffic through a gateway +func (c *Client) DisableGateway() (*GatewayResponse, error) { + resp, err := c.doRequest("POST", "/gateway/disable", nil, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var gwResp GatewayResponse + if err := json.NewDecoder(resp.Body).Decode(&gwResp); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + return &gwResp, nil +} + // IsRunning checks if the OLM process is running by checking if the socket exists // and making a health check request to verify the service is responding func (c *Client) IsRunning() bool { From 3a68064c3e19f884c856b778ccf150e62a9a487d Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 24 Sep 2026 17:28:30 -0400 Subject: [PATCH 02/14] Make the none preselected --- cmd/select/exitnode/exitnode.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/cmd/select/exitnode/exitnode.go b/cmd/select/exitnode/exitnode.go index 5745322..726927e 100644 --- a/cmd/select/exitnode/exitnode.go +++ b/cmd/select/exitnode/exitnode.go @@ -160,19 +160,22 @@ func selectExitNodeForm(gateways []api.SiteResource, status *olm.StatusResponse, options = append(options, huh.NewOption(label, i)) } - // huh starts the cursor on the option matching this value, so default to - // "None" when it's offered; otherwise it would start on the first exit - // node with "None" scrolled out of view above it. + // huh starts the cursor on the option matching this value, so preselect + // "None" when it's offered. selected := 0 if hasGateway { selected = disableChoice } form := huh.NewForm( huh.NewGroup( + // Value must come before Options: Options positions the scroll + // offset from the value bound at that moment, and Value doesn't + // reposition it afterwards, which would leave "None" hidden above + // the visible list. huh.NewSelect[int](). Title("Select an exit node"). - Options(options...). - Value(&selected), + Value(&selected). + Options(options...), ), ) if err := form.Run(); err != nil { From 2a3c161d93319b729983053212a289f17d40a2e7 Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 25 Sep 2026 11:18:29 -0400 Subject: [PATCH 03/14] Handle exit nodes with nice id --- cmd/select/exitnode/exitnode.go | 35 ++++++++++++++++------ cmd/up/client/client.go | 18 ++++++++---- cmd/up/client/exitnode.go | 51 +++++++++++++++++++++++++++++++++ cmd/up/client/exitnode_test.go | 44 ++++++++++++++++++++++++++++ internal/config/config.go | 32 +++++++++++++++------ 5 files changed, 157 insertions(+), 23 deletions(-) create mode 100644 cmd/up/client/exitnode.go create mode 100644 cmd/up/client/exitnode_test.go diff --git a/cmd/select/exitnode/exitnode.go b/cmd/select/exitnode/exitnode.go index 726927e..ebc9ba0 100644 --- a/cmd/select/exitnode/exitnode.go +++ b/cmd/select/exitnode/exitnode.go @@ -80,9 +80,16 @@ func exitNodeMain(cmd *cobra.Command, opts *ExitNodeCmdOpts) error { usable = append(usable, g) } } + // The saved exit node, if it was selected in this org. It identifies the + // active one by niceId, since resources can share sites. + savedNiceID := "" + if cfg.Up.ExitNodeNiceID != "" && (cfg.Up.ExitNodeOrgID == "" || cfg.Up.ExitNodeOrgID == orgID) { + savedNiceID = cfg.Up.ExitNodeNiceID + } + // A saved exit node can outlive the active one (e.g. its sites weren't // connected on startup), so offer to clear it either way. - hasGateway := status.GatewayActive || len(cfg.Up.GatewaySiteIDs) > 0 + hasGateway := status.GatewayActive || cfg.Up.ExitNodeNiceID != "" if len(usable) == 0 && !hasGateway { err := fmt.Errorf("no exit nodes available in this organization") logger.Error("%v", err) @@ -104,7 +111,7 @@ func exitNodeMain(cmd *cobra.Command, opts *ExitNodeCmdOpts) error { return err } } else { - choice, err = selectExitNodeForm(usable, status, hasGateway) + choice, err = selectExitNodeForm(usable, status, hasGateway, savedNiceID) if err != nil { logger.Error("%v", err) return err @@ -118,7 +125,8 @@ func exitNodeMain(cmd *cobra.Command, opts *ExitNodeCmdOpts) error { return err } } - saveGateway(cfg, []int{}) + cfg.ClearExitNode() + saveExitNode(cfg) logger.Success("Exit node disabled") return nil } @@ -128,23 +136,23 @@ func exitNodeMain(cmd *cobra.Command, opts *ExitNodeCmdOpts) error { logger.Error("Failed to select exit node: %v", err) return err } - saveGateway(cfg, selected.SiteIDs) + cfg.SetExitNode(orgID, selected.NiceID) + saveExitNode(cfg) logger.Success("Routing all traffic through exit node: %s", selected.Name) return nil } -// saveGateway persists the exit node so the next `pangolin up` re-applies it. +// saveExitNode persists the exit node so the next `pangolin up` re-applies it. // A failure is only a warning: the change is already live on the client. -func saveGateway(cfg *config.Config, siteIDs []int) { - cfg.Up.GatewaySiteIDs = siteIDs +func saveExitNode(cfg *config.Config) { if err := cfg.Save(); err != nil { logger.Warning("Exit node applied but could not be saved for the next start: %v", err) } } // selectExitNodeForm returns the index of the chosen gateway, or disableChoice. -func selectExitNodeForm(gateways []api.SiteResource, status *olm.StatusResponse, hasGateway bool) (int, error) { +func selectExitNodeForm(gateways []api.SiteResource, status *olm.StatusResponse, hasGateway bool, savedNiceID string) (int, error) { options := make([]huh.Option[int], 0, len(gateways)+1) if hasGateway { options = append(options, huh.NewOption("None (disable exit node)", disableChoice)) @@ -154,7 +162,7 @@ func selectExitNodeForm(gateways []api.SiteResource, status *olm.StatusResponse, if len(g.SiteNames) > 0 { label += " - " + strings.Join(g.SiteNames, ", ") } - if status.GatewayActive && sameSites(g.SiteIDs, status.GatewaySiteIDs) { + if status.GatewayActive && isActive(g, status, savedNiceID) { label += " [active]" } options = append(options, huh.NewOption(label, i)) @@ -185,6 +193,15 @@ func selectExitNodeForm(gateways []api.SiteResource, status *olm.StatusResponse, return selected, nil } +// isActive matches by niceId when this CLI saved the selection, since two exit +// nodes can share sites; otherwise (selected by other means) by site IDs. +func isActive(g api.SiteResource, status *olm.StatusResponse, savedNiceID string) bool { + if savedNiceID != "" { + return g.NiceID == savedNiceID + } + return sameSites(g.SiteIDs, status.GatewaySiteIDs) +} + func sameSites(a, b []int) bool { if len(a) != len(b) { return false diff --git a/cmd/up/client/client.go b/cmd/up/client/client.go index eed179c..7c7527a 100644 --- a/cmd/up/client/client.go +++ b/cmd/up/client/client.go @@ -308,12 +308,6 @@ func clientUpMain(cmd *cobra.Command, opts *ClientUpCmdOpts, extraArgs []string) opts.MatchDomains = cfg.GetStringSlice("up.match_domains_dns") } - // Same as match-domains: resolved here so it can be forwarded to the - // subprocess, which may not have access to the user's config. - if !cmd.Flags().Changed("exit-node-site-ids") && len(cfg.Up.GatewaySiteIDs) > 0 { - opts.GatewaySiteIDs = cfg.Up.GatewaySiteIDs - } - // Check if a client is already running olmClient := olm.NewClient("") if olmClient.IsRunning() { @@ -427,6 +421,18 @@ func clientUpMain(cmd *cobra.Command, opts *ClientUpCmdOpts, extraArgs []string) orgID = activeAccount.OrgID } + // Same as match-domains: resolved here so it can be forwarded to the + // subprocess, which may not have access to the user's config. Verified + // against the server when we have a user session, so a deleted exit node + // isn't applied. + if !cmd.Flags().Changed("exit-node-site-ids") { + var list func(string) ([]api.SiteResource, error) + if credentialsFromKeyring { + list = apiClient.ListGatewayResources + } + opts.GatewaySiteIDs = resolveSavedExitNode(cfg.Up, orgID, list) + } + // Handle log file setup - if detached mode, always use log file var logFile string if !opts.Attached { diff --git a/cmd/up/client/exitnode.go b/cmd/up/client/exitnode.go new file mode 100644 index 0000000..81c307a --- /dev/null +++ b/cmd/up/client/exitnode.go @@ -0,0 +1,51 @@ +package client + +import ( + "github.com/fosrl/cli/internal/api" + "github.com/fosrl/cli/internal/config" + "github.com/fosrl/cli/internal/logger" +) + +// resolveSavedExitNode returns the current site IDs of the exit node saved by +// `pangolin select exit-node`, or nil if none should be applied. Only the +// resource is saved, so its sites always come from the server and can't be +// stale. +// +// list is nil when there's no user session to query the server with. If the +// saved exit node can't be resolved for any reason, the client connects +// without one and says why. +func resolveSavedExitNode(up config.UpConfig, orgID string, list func(orgID string) ([]api.SiteResource, error)) []int { + if up.ExitNodeNiceID == "" { + return nil + } + + if orgID != "" && up.ExitNodeOrgID != "" && up.ExitNodeOrgID != orgID { + logger.Info("Saved exit node '%s' belongs to a different organization; not using it", up.ExitNodeNiceID) + return nil + } + + if list == nil || orgID == "" { + logger.Info("Saved exit node '%s' needs a logged-in session to look up; not using it (pass --exit-node-site-ids to set one explicitly)", up.ExitNodeNiceID) + return nil + } + + gateways, err := list(orgID) + if err != nil { + logger.Warning("Could not look up saved exit node '%s' (%v); connecting without it", up.ExitNodeNiceID, err) + return nil + } + + for _, g := range gateways { + if g.NiceID != up.ExitNodeNiceID { + continue + } + if !g.Enabled || len(g.SiteIDs) == 0 { + logger.Warning("Saved exit node '%s' is disabled or has no sites; not using it", g.NiceID) + return nil + } + return g.SiteIDs + } + + logger.Warning("Saved exit node '%s' no longer exists; not using it. Run 'pangolin select exit-node' and choose None to forget it", up.ExitNodeNiceID) + return nil +} diff --git a/cmd/up/client/exitnode_test.go b/cmd/up/client/exitnode_test.go new file mode 100644 index 0000000..e86e540 --- /dev/null +++ b/cmd/up/client/exitnode_test.go @@ -0,0 +1,44 @@ +package client + +import ( + "errors" + "reflect" + "testing" + + "github.com/fosrl/cli/internal/api" + "github.com/fosrl/cli/internal/config" +) + +func TestResolveSavedExitNode(t *testing.T) { + saved := config.UpConfig{ExitNodeNiceID: "gw-a", ExitNodeOrgID: "org1"} + lister := func(gws ...api.SiteResource) func(string) ([]api.SiteResource, error) { + return func(string) ([]api.SiteResource, error) { return gws, nil } + } + + tests := []struct { + name string + up config.UpConfig + org string + list func(string) ([]api.SiteResource, error) + want []int + }{ + {"nothing saved", config.UpConfig{}, "org1", lister(), nil}, + {"uses the resource's current sites", saved, "org1", + lister(api.SiteResource{NiceID: "gw-b", Enabled: true, SiteIDs: []int{1, 2}}, api.SiteResource{NiceID: "gw-a", Enabled: true, SiteIDs: []int{2, 3}}), []int{2, 3}}, + {"resource deleted", saved, "org1", lister(api.SiteResource{NiceID: "gw-b", Enabled: true, SiteIDs: []int{1, 2}}), nil}, + {"same sites but different resource is not a match", saved, "org1", lister(api.SiteResource{NiceID: "gw-other", Enabled: true, SiteIDs: []int{1, 2}}), nil}, + {"resource disabled", saved, "org1", lister(api.SiteResource{NiceID: "gw-a", Enabled: false, SiteIDs: []int{1, 2}}), nil}, + {"resource has no sites", saved, "org1", lister(api.SiteResource{NiceID: "gw-a", Enabled: true}), nil}, + {"different org", saved, "org2", lister(api.SiteResource{NiceID: "gw-a", Enabled: true, SiteIDs: []int{5}}), nil}, + {"lookup fails: connect without it", saved, "org1", func(string) ([]api.SiteResource, error) { return nil, errors.New("boom") }, nil}, + {"no session to look it up with", saved, "org1", nil, nil}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := resolveSavedExitNode(tt.up, tt.org, tt.list); !reflect.DeepEqual(got, tt.want) { + t.Fatalf("got %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/config/config.go b/internal/config/config.go index bb07738..adb7ecb 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -52,10 +52,23 @@ type UpConfig struct { // Defaults to false. PreferLocalRoutes *bool `mapstructure:"prefer_local_routes" json:"prefer_local_routes,omitempty"` - // GatewaySiteIDs are the site IDs of the exit node selected with - // `pangolin select exit-node`. `pangolin up` re-applies them at connect time - // so the same exit node is used across restarts. Empty means no exit node. - GatewaySiteIDs []int `mapstructure:"exit_node_site_ids" json:"exit_node_site_ids,omitempty"` + // The exit node selected with `pangolin select exit-node`, re-applied by + // `pangolin up`. Only the resource is stored (niceId is unique per org); + // its sites are looked up from the server on every start so they can't + // go stale. Use SetExitNode / ClearExitNode. + ExitNodeNiceID string `mapstructure:"exit_node_nice_id" json:"exit_node_nice_id,omitempty"` + ExitNodeOrgID string `mapstructure:"exit_node_org_id" json:"exit_node_org_id,omitempty"` +} + +// SetExitNode records the selected exit node (a gateway resource). +func (c *Config) SetExitNode(orgID, niceID string) { + c.Up.ExitNodeOrgID = orgID + c.Up.ExitNodeNiceID = niceID +} + +// ClearExitNode forgets the selected exit node. +func (c *Config) ClearExitNode() { + c.SetExitNode("", "") } // CompanionAppDataDirs holds per-platform overrides for the desktop app data directory. @@ -367,10 +380,13 @@ func (c *Config) Save() error { if c.Up.PreferLocalRoutes != nil { c.v.Set("up.prefer_local_routes", *c.Up.PreferLocalRoutes) } - // A non-nil empty slice is written as [] so a cleared exit node overrides - // a previously persisted one. - if c.Up.GatewaySiteIDs != nil { - c.v.Set("up.exit_node_site_ids", c.Up.GatewaySiteIDs) + // Written even when empty once they're in the file, so clearing the exit + // node overwrites the previous value. + if c.Up.ExitNodeNiceID != "" || c.v.IsSet("up.exit_node_nice_id") { + c.v.Set("up.exit_node_nice_id", c.Up.ExitNodeNiceID) + } + if c.Up.ExitNodeOrgID != "" || c.v.IsSet("up.exit_node_org_id") { + c.v.Set("up.exit_node_org_id", c.Up.ExitNodeOrgID) } dir, err := GetPangolinConfigDir() From b1dbaf5d34a7fd1ee7a285065a34c9b62869891f Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 25 Sep 2026 11:51:25 -0400 Subject: [PATCH 04/14] Use the resource id for the gateway --- cmd/select/exitnode/exitnode.go | 43 ++++++----------------------- cmd/up/client/client.go | 49 +++++++++++++++++++-------------- cmd/up/client/exitnode.go | 24 ++++++++-------- cmd/up/client/exitnode_test.go | 34 ++++++++++++----------- internal/olm/client.go | 14 ++++++++-- 5 files changed, 79 insertions(+), 85 deletions(-) diff --git a/cmd/select/exitnode/exitnode.go b/cmd/select/exitnode/exitnode.go index ebc9ba0..065cee6 100644 --- a/cmd/select/exitnode/exitnode.go +++ b/cmd/select/exitnode/exitnode.go @@ -80,13 +80,6 @@ func exitNodeMain(cmd *cobra.Command, opts *ExitNodeCmdOpts) error { usable = append(usable, g) } } - // The saved exit node, if it was selected in this org. It identifies the - // active one by niceId, since resources can share sites. - savedNiceID := "" - if cfg.Up.ExitNodeNiceID != "" && (cfg.Up.ExitNodeOrgID == "" || cfg.Up.ExitNodeOrgID == orgID) { - savedNiceID = cfg.Up.ExitNodeNiceID - } - // A saved exit node can outlive the active one (e.g. its sites weren't // connected on startup), so offer to clear it either way. hasGateway := status.GatewayActive || cfg.Up.ExitNodeNiceID != "" @@ -111,7 +104,7 @@ func exitNodeMain(cmd *cobra.Command, opts *ExitNodeCmdOpts) error { return err } } else { - choice, err = selectExitNodeForm(usable, status, hasGateway, savedNiceID) + choice, err = selectExitNodeForm(usable, status, hasGateway) if err != nil { logger.Error("%v", err) return err @@ -132,7 +125,7 @@ func exitNodeMain(cmd *cobra.Command, opts *ExitNodeCmdOpts) error { } selected := usable[choice] - if _, err := olmClient.SelectGateway(selected.SiteIDs); err != nil { + if _, err := olmClient.SelectGateway(selected.SiteResourceID, selected.SiteIDs); err != nil { logger.Error("Failed to select exit node: %v", err) return err } @@ -152,7 +145,7 @@ func saveExitNode(cfg *config.Config) { } // selectExitNodeForm returns the index of the chosen gateway, or disableChoice. -func selectExitNodeForm(gateways []api.SiteResource, status *olm.StatusResponse, hasGateway bool, savedNiceID string) (int, error) { +func selectExitNodeForm(gateways []api.SiteResource, status *olm.StatusResponse, hasGateway bool) (int, error) { options := make([]huh.Option[int], 0, len(gateways)+1) if hasGateway { options = append(options, huh.NewOption("None (disable exit node)", disableChoice)) @@ -162,7 +155,7 @@ func selectExitNodeForm(gateways []api.SiteResource, status *olm.StatusResponse, if len(g.SiteNames) > 0 { label += " - " + strings.Join(g.SiteNames, ", ") } - if status.GatewayActive && isActive(g, status, savedNiceID) { + if status.GatewayActive && isActive(g, status) { label += " [active]" } options = append(options, huh.NewOption(label, i)) @@ -193,27 +186,9 @@ func selectExitNodeForm(gateways []api.SiteResource, status *olm.StatusResponse, return selected, nil } -// isActive matches by niceId when this CLI saved the selection, since two exit -// nodes can share sites; otherwise (selected by other means) by site IDs. -func isActive(g api.SiteResource, status *olm.StatusResponse, savedNiceID string) bool { - if savedNiceID != "" { - return g.NiceID == savedNiceID - } - return sameSites(g.SiteIDs, status.GatewaySiteIDs) -} - -func sameSites(a, b []int) bool { - if len(a) != len(b) { - return false - } - set := make(map[int]struct{}, len(a)) - for _, id := range a { - set[id] = struct{}{} - } - for _, id := range b { - if _, ok := set[id]; !ok { - return false - } - } - return true +// isActive matches by the resource ID olm reports it selected. Site IDs can't +// be used: two exit nodes can share sites, and the active set changes as the +// server adds/removes sites on the resource. +func isActive(g api.SiteResource, status *olm.StatusResponse) bool { + return status.GatewaySiteResourceID != 0 && g.SiteResourceID == status.GatewaySiteResourceID } diff --git a/cmd/up/client/client.go b/cmd/up/client/client.go index 7c7527a..6faea94 100644 --- a/cmd/up/client/client.go +++ b/cmd/up/client/client.go @@ -58,6 +58,8 @@ type ClientUpCmdOpts struct { DisableRelay bool SubnetRouter bool GatewaySiteIDs []int + + GatewaySiteResourceID int } // validateDNSIP ensures the given DNS server string is a valid IP address. @@ -193,7 +195,8 @@ logs, and removes the service again when you press Ctrl+C.`, cmd.Flags().BoolVar(&opts.PreferLocalRoutes, "prefer-local-routes", false, "Add tunnel routes with a high metric so overlapping local/connected routes take precedence (default false)") cmd.Flags().BoolVar(&opts.DisableRelay, "disable-relay", false, "Disable relay connections (default false)") cmd.Flags().BoolVar(&opts.SubnetRouter, "subnet-router", false, "Enable this client to act as a subnet router: traffic forwarded from the local network is NATed to this client's own tunnel IP before going out over the tunnel. Linux only, requires CAP_NET_ADMIN. (default false)") - cmd.Flags().IntSliceVar(&opts.GatewaySiteIDs, "exit-node-site-ids", nil, "Site IDs to route all traffic through as an exit node (default: the exit node saved by 'pangolin select exit-node', if any)") + cmd.Flags().IntSliceVar(&opts.GatewaySiteIDs, "exit-node-site-ids", nil, "Site IDs to route all traffic through as an exit node (default: the exit node saved by 'pangolin select exit-node', if any). Requires --exit-node-resource-id") + cmd.Flags().IntVar(&opts.GatewaySiteResourceID, "exit-node-resource-id", 0, "ID of the gateway site resource the --exit-node-site-ids belong to, so changes to that resource are applied while connected") cmd.Flags().BoolVar(&opts.Attached, "attach", false, "Run in attached (foreground) mode, (default: detached (background) mode)") cmd.Flags().BoolVar(&opts.Silent, "silent", false, "Disable TUI and run silently when detached") @@ -430,7 +433,11 @@ func clientUpMain(cmd *cobra.Command, opts *ClientUpCmdOpts, extraArgs []string) if credentialsFromKeyring { list = apiClient.ListGatewayResources } - opts.GatewaySiteIDs = resolveSavedExitNode(cfg.Up, orgID, list) + opts.GatewaySiteResourceID, opts.GatewaySiteIDs = resolveSavedExitNode(cfg.Up, orgID, list) + } else if len(opts.GatewaySiteIDs) > 0 && opts.GatewaySiteResourceID <= 0 { + err := fmt.Errorf("--exit-node-site-ids requires --exit-node-resource-id") + logger.Error("%v", err) + return err } // Handle log file setup - if detached mode, always use log file @@ -542,6 +549,7 @@ func clientUpMain(cmd *cobra.Command, opts *ClientUpCmdOpts, extraArgs []string) ids[i] = strconv.Itoa(id) } cmdArgs = append(cmdArgs, "--exit-node-site-ids", strings.Join(ids, ",")) + cmdArgs = append(cmdArgs, "--exit-node-resource-id", strconv.Itoa(opts.GatewaySiteResourceID)) } // Add positional args if any @@ -771,24 +779,25 @@ func clientUpMain(cmd *cobra.Command, opts *ClientUpCmdOpts, extraArgs []string) } tunnelConfig := olmpkg.TunnelConfig{ - Endpoint: endpoint, - ID: olmID, - Secret: olmSecret, - OrgID: orgID, - MTU: opts.MTU, - DNS: opts.DNS, - InterfaceName: opts.InterfaceName, - Holepunch: opts.Holepunch, - TlsClientCert: opts.TlsClientCert, - PingIntervalDuration: opts.PingInterval, - PingTimeoutDuration: opts.PingTimeout, - OverrideDNS: opts.OverrideDNS, - TunnelDNS: opts.TunnelDNS, - UpstreamDNS: upstreamDNS, - MatchDomains: opts.MatchDomains, - PreferLocalRoutes: opts.PreferLocalRoutes, - DisableRelay: opts.DisableRelay, - GatewaySiteIds: opts.GatewaySiteIDs, + Endpoint: endpoint, + ID: olmID, + Secret: olmSecret, + OrgID: orgID, + MTU: opts.MTU, + DNS: opts.DNS, + InterfaceName: opts.InterfaceName, + Holepunch: opts.Holepunch, + TlsClientCert: opts.TlsClientCert, + PingIntervalDuration: opts.PingInterval, + PingTimeoutDuration: opts.PingTimeout, + OverrideDNS: opts.OverrideDNS, + TunnelDNS: opts.TunnelDNS, + UpstreamDNS: upstreamDNS, + MatchDomains: opts.MatchDomains, + PreferLocalRoutes: opts.PreferLocalRoutes, + DisableRelay: opts.DisableRelay, + GatewaySiteIds: opts.GatewaySiteIDs, + GatewaySiteResourceId: opts.GatewaySiteResourceID, // SubnetRouter: opts.SubnetRouter, UserToken: userToken, InitialFingerprint: initialFingerprint, diff --git a/cmd/up/client/exitnode.go b/cmd/up/client/exitnode.go index 81c307a..2843f3d 100644 --- a/cmd/up/client/exitnode.go +++ b/cmd/up/client/exitnode.go @@ -6,33 +6,33 @@ import ( "github.com/fosrl/cli/internal/logger" ) -// resolveSavedExitNode returns the current site IDs of the exit node saved by -// `pangolin select exit-node`, or nil if none should be applied. Only the -// resource is saved, so its sites always come from the server and can't be -// stale. +// resolveSavedExitNode returns the resource ID and current site IDs of the exit +// node saved by `pangolin select exit-node`, or 0/nil if none should be +// applied. Only the resource's niceId is saved, so its ID and sites always come +// from the server and can't be stale (or break if the niceId was renamed away). // // list is nil when there's no user session to query the server with. If the // saved exit node can't be resolved for any reason, the client connects // without one and says why. -func resolveSavedExitNode(up config.UpConfig, orgID string, list func(orgID string) ([]api.SiteResource, error)) []int { +func resolveSavedExitNode(up config.UpConfig, orgID string, list func(orgID string) ([]api.SiteResource, error)) (int, []int) { if up.ExitNodeNiceID == "" { - return nil + return 0, nil } if orgID != "" && up.ExitNodeOrgID != "" && up.ExitNodeOrgID != orgID { logger.Info("Saved exit node '%s' belongs to a different organization; not using it", up.ExitNodeNiceID) - return nil + return 0, nil } if list == nil || orgID == "" { logger.Info("Saved exit node '%s' needs a logged-in session to look up; not using it (pass --exit-node-site-ids to set one explicitly)", up.ExitNodeNiceID) - return nil + return 0, nil } gateways, err := list(orgID) if err != nil { logger.Warning("Could not look up saved exit node '%s' (%v); connecting without it", up.ExitNodeNiceID, err) - return nil + return 0, nil } for _, g := range gateways { @@ -41,11 +41,11 @@ func resolveSavedExitNode(up config.UpConfig, orgID string, list func(orgID stri } if !g.Enabled || len(g.SiteIDs) == 0 { logger.Warning("Saved exit node '%s' is disabled or has no sites; not using it", g.NiceID) - return nil + return 0, nil } - return g.SiteIDs + return g.SiteResourceID, g.SiteIDs } logger.Warning("Saved exit node '%s' no longer exists; not using it. Run 'pangolin select exit-node' and choose None to forget it", up.ExitNodeNiceID) - return nil + return 0, nil } diff --git a/cmd/up/client/exitnode_test.go b/cmd/up/client/exitnode_test.go index e86e540..b4bf2c7 100644 --- a/cmd/up/client/exitnode_test.go +++ b/cmd/up/client/exitnode_test.go @@ -16,28 +16,30 @@ func TestResolveSavedExitNode(t *testing.T) { } tests := []struct { - name string - up config.UpConfig - org string - list func(string) ([]api.SiteResource, error) - want []int + name string + up config.UpConfig + org string + list func(string) ([]api.SiteResource, error) + want []int + wantID int }{ - {"nothing saved", config.UpConfig{}, "org1", lister(), nil}, + {"nothing saved", config.UpConfig{}, "org1", lister(), nil, 0}, {"uses the resource's current sites", saved, "org1", - lister(api.SiteResource{NiceID: "gw-b", Enabled: true, SiteIDs: []int{1, 2}}, api.SiteResource{NiceID: "gw-a", Enabled: true, SiteIDs: []int{2, 3}}), []int{2, 3}}, - {"resource deleted", saved, "org1", lister(api.SiteResource{NiceID: "gw-b", Enabled: true, SiteIDs: []int{1, 2}}), nil}, - {"same sites but different resource is not a match", saved, "org1", lister(api.SiteResource{NiceID: "gw-other", Enabled: true, SiteIDs: []int{1, 2}}), nil}, - {"resource disabled", saved, "org1", lister(api.SiteResource{NiceID: "gw-a", Enabled: false, SiteIDs: []int{1, 2}}), nil}, - {"resource has no sites", saved, "org1", lister(api.SiteResource{NiceID: "gw-a", Enabled: true}), nil}, - {"different org", saved, "org2", lister(api.SiteResource{NiceID: "gw-a", Enabled: true, SiteIDs: []int{5}}), nil}, - {"lookup fails: connect without it", saved, "org1", func(string) ([]api.SiteResource, error) { return nil, errors.New("boom") }, nil}, - {"no session to look it up with", saved, "org1", nil, nil}, + lister(api.SiteResource{SiteResourceID: 11, NiceID: "gw-b", Enabled: true, SiteIDs: []int{1, 2}}, api.SiteResource{SiteResourceID: 12, NiceID: "gw-a", Enabled: true, SiteIDs: []int{2, 3}}), []int{2, 3}, 12}, + {"resource deleted", saved, "org1", lister(api.SiteResource{NiceID: "gw-b", Enabled: true, SiteIDs: []int{1, 2}}), nil, 0}, + {"same sites but different resource is not a match", saved, "org1", lister(api.SiteResource{NiceID: "gw-other", Enabled: true, SiteIDs: []int{1, 2}}), nil, 0}, + {"resource disabled", saved, "org1", lister(api.SiteResource{NiceID: "gw-a", Enabled: false, SiteIDs: []int{1, 2}}), nil, 0}, + {"resource has no sites", saved, "org1", lister(api.SiteResource{NiceID: "gw-a", Enabled: true}), nil, 0}, + {"different org", saved, "org2", lister(api.SiteResource{NiceID: "gw-a", Enabled: true, SiteIDs: []int{5}}), nil, 0}, + {"lookup fails: connect without it", saved, "org1", func(string) ([]api.SiteResource, error) { return nil, errors.New("boom") }, nil, 0}, + {"no session to look it up with", saved, "org1", nil, nil, 0}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := resolveSavedExitNode(tt.up, tt.org, tt.list); !reflect.DeepEqual(got, tt.want) { - t.Fatalf("got %v, want %v", got, tt.want) + gotID, got := resolveSavedExitNode(tt.up, tt.org, tt.list) + if !reflect.DeepEqual(got, tt.want) || gotID != tt.wantID { + t.Fatalf("got %d %v, want %d %v", gotID, got, tt.wantID, tt.want) } }) } diff --git a/internal/olm/client.go b/internal/olm/client.go index d33a264..7f57dc1 100644 --- a/internal/olm/client.go +++ b/internal/olm/client.go @@ -39,6 +39,8 @@ type StatusResponse struct { ExitNode *OLMExitNodeStatus `json:"exitNode,omitempty"` GatewayActive bool `json:"gatewayActive,omitempty"` GatewaySiteIDs []int `json:"gatewaySiteIds,omitempty"` + + GatewaySiteResourceID int `json:"gatewaySiteResourceId,omitempty"` // the gateway resource the selection belongs to } // OLMExitNodeStatus represents the connectivity status of the client's own exit @@ -92,7 +94,8 @@ type JITConnectionResponse struct { // GatewayRequest represents a select-gateway request type GatewayRequest struct { - SiteIDs []int `json:"siteIds"` + SiteResourceID int `json:"siteResourceId"` + SiteIDs []int `json:"siteIds"` } // GatewayResponse represents the response from a gateway select/disable request @@ -245,13 +248,18 @@ func (c *Client) JITConnectByResourceID(resourceID string) (*JITConnectionRespon } // SelectGateway routes all tunnel traffic through the given sites (full tunnel). +// siteResourceID is the numeric ID of the gateway resource the sites belong to; +// olm uses it to apply later server-pushed changes to that resource only. // Every site must already be a connected peer of the running client. -func (c *Client) SelectGateway(siteIDs []int) (*GatewayResponse, error) { +func (c *Client) SelectGateway(siteResourceID int, siteIDs []int) (*GatewayResponse, error) { + if siteResourceID <= 0 { + return nil, fmt.Errorf("a gateway site resource ID is required") + } if len(siteIDs) == 0 { return nil, fmt.Errorf("at least one site ID is required") } - jsonData, err := json.Marshal(GatewayRequest{SiteIDs: siteIDs}) + jsonData, err := json.Marshal(GatewayRequest{SiteResourceID: siteResourceID, SiteIDs: siteIDs}) if err != nil { return nil, fmt.Errorf("failed to marshal request: %w", err) } From cf1615326fab828f7105ed9dc49442649e0e6ed1 Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 25 Sep 2026 12:10:50 -0400 Subject: [PATCH 05/14] Show gateway status --- cmd/status/client/client.go | 42 +++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/cmd/status/client/client.go b/cmd/status/client/client.go index 2d3feba..94f6f02 100644 --- a/cmd/status/client/client.go +++ b/cmd/status/client/client.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "os" + "sort" "time" "github.com/fosrl/cli/internal/logger" @@ -76,13 +77,14 @@ func printJSON(status *olm.StatusResponse) error { // printStatusTable prints the status information in a table format func printStatusTable(status *olm.StatusResponse) { // Print connection status - headers := []string{"AGENT", "VERSION", "STATUS", "ORG"} + headers := []string{"AGENT", "VERSION", "STATUS", "ORG", "GATEWAY"} rows := [][]string{ { status.Agent, status.Version, formatStatus(status.Connected, status.Registered), status.OrgID, + formatGateway(status), }, } utils.PrintTable(headers, rows) @@ -90,7 +92,7 @@ func printStatusTable(status *olm.StatusResponse) { // Print peers (and the exit node, if connected) if there are any if len(status.PeerStatuses) > 0 || status.ExitNode != nil { fmt.Println("") - peerHeaders := []string{"SITE", "ENDPOINT", "STATUS", "LAST SEEN", "CONNECTION"} + peerHeaders := []string{"SITE", "ENDPOINT", "STATUS", "LAST SEEN", "CONNECTION", "GATEWAY"} peerRows := [][]string{} if status.ExitNode != nil { @@ -101,10 +103,25 @@ func printStatusTable(status *olm.StatusResponse) { formatStatus(status.ExitNode.Connected, true), lastSeen, "Direct", + "-", }) } + gatewaySites := make(map[int]bool, len(status.GatewaySiteIDs)) + if status.GatewayActive { + for _, id := range status.GatewaySiteIDs { + gatewaySites[id] = true + } + } + + // Map iteration order is random; sort so the table is stable between runs. + peers := make([]*olm.OLMPeerStatus, 0, len(status.PeerStatuses)) for _, peer := range status.PeerStatuses { + peers = append(peers, peer) + } + sort.Slice(peers, func(i, j int) bool { return peers[i].SiteID < peers[j].SiteID }) + + for _, peer := range peers { lastSeen := formatLastSeen(peer.LastSeen.Format(time.RFC3339)) peerRows = append(peerRows, []string{ @@ -113,6 +130,7 @@ func printStatusTable(status *olm.StatusResponse) { formatStatus(peer.Connected, true), // Peers don't have registered field, use true lastSeen, formatConnectionMode(peer.IsLocal, peer.IsRelay), + formatGatewayMember(gatewaySites[peer.SiteID]), }) } @@ -122,6 +140,26 @@ func printStatusTable(status *olm.StatusResponse) { } } +// formatGateway summarizes whether the client is routing all traffic through a +// gateway (exit node), and which site resource it was selected from. +func formatGateway(status *olm.StatusResponse) string { + if !status.GatewayActive { + return "Off" + } + if status.GatewaySiteResourceID != 0 { + return fmt.Sprintf("Active (resource %d)", status.GatewaySiteResourceID) + } + return "Active" +} + +// formatGatewayMember marks the sites currently in use as the gateway. +func formatGatewayMember(isGateway bool) string { + if isGateway { + return "Yes" + } + return "-" +} + // formatConnectionMode summarizes how a peer is currently connected. Local and relay are // mutually exclusive; when neither applies the peer is connected directly to its public // endpoint. From f4bc3cd18dc95159da5a8454f26503e92edd06e7 Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 25 Sep 2026 15:11:40 -0400 Subject: [PATCH 06/14] Support selecting exit node prior to starting --- cmd/select/exitnode/exitnode.go | 89 +++++++++++++++++++++------------ 1 file changed, 58 insertions(+), 31 deletions(-) diff --git a/cmd/select/exitnode/exitnode.go b/cmd/select/exitnode/exitnode.go index 065cee6..8d21228 100644 --- a/cmd/select/exitnode/exitnode.go +++ b/cmd/select/exitnode/exitnode.go @@ -31,7 +31,9 @@ func ExitNodeCmd() *cobra.Command { tunnel traffic (full tunnel) through the sites backing it. While an exit node is active, a "None" option is shown to turn it off. -Requires a running client.`, + +With a running client the change takes effect immediately. Without one, the +choice is saved and applied the next time you run 'pangolin up'.`, Run: func(cmd *cobra.Command, args []string) { if err := exitNodeMain(cmd, &opts); err != nil { os.Exit(1) @@ -45,17 +47,19 @@ Requires a running client.`, } func exitNodeMain(cmd *cobra.Command, opts *ExitNodeCmdOpts) error { + // The client doesn't have to be running: the choice is saved to the config + // and applied by the next `pangolin up`. When it is running it is also + // applied live. olmClient := olm.NewClient("") - if !olmClient.IsRunning() { - err := fmt.Errorf("no client is currently running; start one with 'pangolin up'") - logger.Error("%v", err) - return err - } - - status, err := olmClient.GetStatus() - if err != nil { - logger.Error("Failed to get client status: %v", err) - return err + running := olmClient.IsRunning() + status := &olm.StatusResponse{} + if running { + var err error + status, err = olmClient.GetStatus() + if err != nil { + logger.Error("Failed to get client status: %v", err) + return err + } } cfg := config.ConfigFromContext(cmd.Context()) @@ -80,6 +84,19 @@ func exitNodeMain(cmd *cobra.Command, opts *ExitNodeCmdOpts) error { usable = append(usable, g) } } + // The saved exit node, if it was selected in this org. + savedNiceID := "" + if cfg.Up.ExitNodeNiceID != "" && (cfg.Up.ExitNodeOrgID == "" || cfg.Up.ExitNodeOrgID == orgID) { + savedNiceID = cfg.Up.ExitNodeNiceID + } + // With a running client, the active exit node is the one it reports; + // otherwise it is the saved one, which the next start will apply. + isActive := func(g api.SiteResource) bool { + if running { + return status.GatewayActive && g.SiteResourceID == status.GatewaySiteResourceID + } + return savedNiceID != "" && g.NiceID == savedNiceID + } // A saved exit node can outlive the active one (e.g. its sites weren't // connected on startup), so offer to clear it either way. hasGateway := status.GatewayActive || cfg.Up.ExitNodeNiceID != "" @@ -104,7 +121,7 @@ func exitNodeMain(cmd *cobra.Command, opts *ExitNodeCmdOpts) error { return err } } else { - choice, err = selectExitNodeForm(usable, status, hasGateway) + choice, err = selectExitNodeForm(usable, isActive, hasGateway) if err != nil { logger.Error("%v", err) return err @@ -112,40 +129,57 @@ func exitNodeMain(cmd *cobra.Command, opts *ExitNodeCmdOpts) error { } if choice == disableChoice { - if status.GatewayActive { + if running && status.GatewayActive { if _, err := olmClient.DisableGateway(); err != nil { logger.Error("Failed to disable exit node: %v", err) return err } } cfg.ClearExitNode() - saveExitNode(cfg) + if !saveExitNode(cfg, running) { + return fmt.Errorf("failed to save exit node") + } logger.Success("Exit node disabled") return nil } selected := usable[choice] - if _, err := olmClient.SelectGateway(selected.SiteResourceID, selected.SiteIDs); err != nil { - logger.Error("Failed to select exit node: %v", err) - return err + if running { + if _, err := olmClient.SelectGateway(selected.SiteResourceID, selected.SiteIDs); err != nil { + logger.Error("Failed to select exit node: %v", err) + return err + } } cfg.SetExitNode(orgID, selected.NiceID) - saveExitNode(cfg) + if !saveExitNode(cfg, running) { + return fmt.Errorf("failed to save exit node") + } - logger.Success("Routing all traffic through exit node: %s", selected.Name) + if running { + logger.Success("Routing all traffic through exit node: %s", selected.Name) + } else { + logger.Success("Exit node %s saved; it will be used the next time you run 'pangolin up'", selected.Name) + } return nil } // saveExitNode persists the exit node so the next `pangolin up` re-applies it. -// A failure is only a warning: the change is already live on the client. -func saveExitNode(cfg *config.Config) { +// With a running client the change is already live, so a save failure is only +// a warning; without one, saving is the whole point, so it is an error. +func saveExitNode(cfg *config.Config, alreadyApplied bool) bool { if err := cfg.Save(); err != nil { - logger.Warning("Exit node applied but could not be saved for the next start: %v", err) + if alreadyApplied { + logger.Warning("Exit node applied but could not be saved for the next start: %v", err) + return true + } + logger.Error("Failed to save exit node: %v", err) + return false } + return true } // selectExitNodeForm returns the index of the chosen gateway, or disableChoice. -func selectExitNodeForm(gateways []api.SiteResource, status *olm.StatusResponse, hasGateway bool) (int, error) { +func selectExitNodeForm(gateways []api.SiteResource, isActive func(api.SiteResource) bool, hasGateway bool) (int, error) { options := make([]huh.Option[int], 0, len(gateways)+1) if hasGateway { options = append(options, huh.NewOption("None (disable exit node)", disableChoice)) @@ -155,7 +189,7 @@ func selectExitNodeForm(gateways []api.SiteResource, status *olm.StatusResponse, if len(g.SiteNames) > 0 { label += " - " + strings.Join(g.SiteNames, ", ") } - if status.GatewayActive && isActive(g, status) { + if isActive(g) { label += " [active]" } options = append(options, huh.NewOption(label, i)) @@ -185,10 +219,3 @@ func selectExitNodeForm(gateways []api.SiteResource, status *olm.StatusResponse, return selected, nil } - -// isActive matches by the resource ID olm reports it selected. Site IDs can't -// be used: two exit nodes can share sites, and the active set changes as the -// server adds/removes sites on the resource. -func isActive(g api.SiteResource, status *olm.StatusResponse) bool { - return status.GatewaySiteResourceID != 0 && g.SiteResourceID == status.GatewaySiteResourceID -} From 281ac1db9b086102870844dabbf7bc4cf38c9d26 Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 25 Sep 2026 17:19:19 -0400 Subject: [PATCH 07/14] Gateway -> exit node --- cmd/status/client/client.go | 4 ++-- cmd/up/client/client.go | 2 +- internal/olm/client.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/status/client/client.go b/cmd/status/client/client.go index 94f6f02..6089175 100644 --- a/cmd/status/client/client.go +++ b/cmd/status/client/client.go @@ -77,7 +77,7 @@ func printJSON(status *olm.StatusResponse) error { // printStatusTable prints the status information in a table format func printStatusTable(status *olm.StatusResponse) { // Print connection status - headers := []string{"AGENT", "VERSION", "STATUS", "ORG", "GATEWAY"} + headers := []string{"AGENT", "VERSION", "STATUS", "ORG", "EXIT NODE"} rows := [][]string{ { status.Agent, @@ -92,7 +92,7 @@ func printStatusTable(status *olm.StatusResponse) { // Print peers (and the exit node, if connected) if there are any if len(status.PeerStatuses) > 0 || status.ExitNode != nil { fmt.Println("") - peerHeaders := []string{"SITE", "ENDPOINT", "STATUS", "LAST SEEN", "CONNECTION", "GATEWAY"} + peerHeaders := []string{"SITE", "ENDPOINT", "STATUS", "LAST SEEN", "CONNECTION", "EXIT NODE"} peerRows := [][]string{} if status.ExitNode != nil { diff --git a/cmd/up/client/client.go b/cmd/up/client/client.go index 6faea94..b785c6f 100644 --- a/cmd/up/client/client.go +++ b/cmd/up/client/client.go @@ -196,7 +196,7 @@ logs, and removes the service again when you press Ctrl+C.`, cmd.Flags().BoolVar(&opts.DisableRelay, "disable-relay", false, "Disable relay connections (default false)") cmd.Flags().BoolVar(&opts.SubnetRouter, "subnet-router", false, "Enable this client to act as a subnet router: traffic forwarded from the local network is NATed to this client's own tunnel IP before going out over the tunnel. Linux only, requires CAP_NET_ADMIN. (default false)") cmd.Flags().IntSliceVar(&opts.GatewaySiteIDs, "exit-node-site-ids", nil, "Site IDs to route all traffic through as an exit node (default: the exit node saved by 'pangolin select exit-node', if any). Requires --exit-node-resource-id") - cmd.Flags().IntVar(&opts.GatewaySiteResourceID, "exit-node-resource-id", 0, "ID of the gateway site resource the --exit-node-site-ids belong to, so changes to that resource are applied while connected") + cmd.Flags().IntVar(&opts.GatewaySiteResourceID, "exit-node-resource-id", 0, "ID of the exit node resource the --exit-node-site-ids belong to, so changes to that resource are applied while connected") cmd.Flags().BoolVar(&opts.Attached, "attach", false, "Run in attached (foreground) mode, (default: detached (background) mode)") cmd.Flags().BoolVar(&opts.Silent, "silent", false, "Disable TUI and run silently when detached") diff --git a/internal/olm/client.go b/internal/olm/client.go index 7f57dc1..c0330f7 100644 --- a/internal/olm/client.go +++ b/internal/olm/client.go @@ -253,7 +253,7 @@ func (c *Client) JITConnectByResourceID(resourceID string) (*JITConnectionRespon // Every site must already be a connected peer of the running client. func (c *Client) SelectGateway(siteResourceID int, siteIDs []int) (*GatewayResponse, error) { if siteResourceID <= 0 { - return nil, fmt.Errorf("a gateway site resource ID is required") + return nil, fmt.Errorf("an exit node resource ID is required") } if len(siteIDs) == 0 { return nil, fmt.Errorf("at least one site ID is required") From 570e9cc93b9f108268e2d1682123545e79570ac2 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 28 Sep 2026 14:41:24 -0400 Subject: [PATCH 08/14] Support the exit node takes precedence option --- cmd/config/config.go | 3 ++ cmd/up/client/client.go | 96 ++++++++++++++++++++++----------------- internal/config/config.go | 24 ++++++++++ 3 files changed, 81 insertions(+), 42 deletions(-) diff --git a/cmd/config/config.go b/cmd/config/config.go index 07e4145..b348a01 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -141,6 +141,9 @@ func dumpConfig(cfg *config.Config) error { if cfg.IsSet("up.prefer_local_routes") { up["prefer_local_routes"] = cfg.GetBool("up.prefer_local_routes") } + if cfg.IsSet("up.exit_node_takes_precedence") { + up["exit_node_takes_precedence"] = cfg.GetBool("up.exit_node_takes_precedence") + } if len(up) > 0 { out["up"] = up } diff --git a/cmd/up/client/client.go b/cmd/up/client/client.go index b785c6f..5bcad6c 100644 --- a/cmd/up/client/client.go +++ b/cmd/up/client/client.go @@ -35,29 +35,30 @@ const ( ) type ClientUpCmdOpts struct { - ID string - Secret string - Endpoint string - OrgID string - MTU int - DNS string - InterfaceName string - LogLevel string - HTTPAddr string - PingInterval time.Duration - PingTimeout time.Duration - Holepunch bool - TlsClientCert string - Attached bool - Silent bool - OverrideDNS bool - TunnelDNS bool - UpstreamDNS []string - MatchDomains []string - PreferLocalRoutes bool - DisableRelay bool - SubnetRouter bool - GatewaySiteIDs []int + ID string + Secret string + Endpoint string + OrgID string + MTU int + DNS string + InterfaceName string + LogLevel string + HTTPAddr string + PingInterval time.Duration + PingTimeout time.Duration + Holepunch bool + TlsClientCert string + Attached bool + Silent bool + OverrideDNS bool + TunnelDNS bool + UpstreamDNS []string + MatchDomains []string + PreferLocalRoutes bool + ExitNodeTakesPrecedence bool + DisableRelay bool + SubnetRouter bool + GatewaySiteIDs []int GatewaySiteResourceID int } @@ -193,6 +194,7 @@ logs, and removes the service again when you press Ctrl+C.`, cmd.Flags().StringSliceVar(&opts.UpstreamDNS, "upstream-dns", []string{}, "List of DNS servers to use for external DNS resolution if overriding system DNS") cmd.Flags().StringSliceVar(&opts.MatchDomains, "match-domains", nil, "FQDN wildcard patterns (e.g. '*.proxy.internal') to check against local records/upstream DNS; queries for non-matching domains go directly to the system's DNS servers (default: match all domains, or the value from config if set)") cmd.Flags().BoolVar(&opts.PreferLocalRoutes, "prefer-local-routes", false, "Add tunnel routes with a high metric so overlapping local/connected routes take precedence (default false)") + cmd.Flags().BoolVar(&opts.ExitNodeTakesPrecedence, "exit-node-takes-precedence", false, "Do not add routes or resolve aliases for individual resources, so all traffic is sent through the exit node instead of directly to resources (default false)") cmd.Flags().BoolVar(&opts.DisableRelay, "disable-relay", false, "Disable relay connections (default false)") cmd.Flags().BoolVar(&opts.SubnetRouter, "subnet-router", false, "Enable this client to act as a subnet router: traffic forwarded from the local network is NATed to this client's own tunnel IP before going out over the tunnel. Linux only, requires CAP_NET_ADMIN. (default false)") cmd.Flags().IntSliceVar(&opts.GatewaySiteIDs, "exit-node-site-ids", nil, "Site IDs to route all traffic through as an exit node (default: the exit node saved by 'pangolin select exit-node', if any). Requires --exit-node-resource-id") @@ -226,6 +228,7 @@ var olmEnvFlagOverrides = []struct { {"TUNNEL_DNS", "tunnel-dns"}, {"DISABLE_RELAY", "disable-relay"}, {"PREFER_LOCAL_ROUTES", "prefer-local-routes"}, + {"DISABLE_ROUTES_AND_ALIASES", "exit-node-takes-precedence"}, {"SUBNET_ROUTER", "subnet-router"}, } @@ -282,6 +285,9 @@ func applyUpDefaults(cmd *cobra.Command, opts *ClientUpCmdOpts, cfg *config.Conf if !cmd.Flags().Changed("prefer-local-routes") && cfg.IsSet("up.prefer_local_routes") { opts.PreferLocalRoutes = cfg.GetBool("up.prefer_local_routes") } + if !cmd.Flags().Changed("exit-node-takes-precedence") && cfg.IsSet("up.exit_node_takes_precedence") { + opts.ExitNodeTakesPrecedence = cfg.GetBool("up.exit_node_takes_precedence") + } } func clientUpMain(cmd *cobra.Command, opts *ClientUpCmdOpts, extraArgs []string) error { @@ -537,6 +543,11 @@ func clientUpMain(cmd *cobra.Command, opts *ClientUpCmdOpts, extraArgs []string) // same reason as MatchDomains above - it may have come from config. cmdArgs = append(cmdArgs, "--prefer-local-routes") } + if opts.ExitNodeTakesPrecedence { + // Always forwarded when true (rather than gated on Changed) for the + // same reason as MatchDomains above - it may have come from config. + cmdArgs = append(cmdArgs, "--exit-node-takes-precedence") + } if opts.DisableRelay { cmdArgs = append(cmdArgs, "--disable-relay") } @@ -779,25 +790,26 @@ func clientUpMain(cmd *cobra.Command, opts *ClientUpCmdOpts, extraArgs []string) } tunnelConfig := olmpkg.TunnelConfig{ - Endpoint: endpoint, - ID: olmID, - Secret: olmSecret, - OrgID: orgID, - MTU: opts.MTU, - DNS: opts.DNS, - InterfaceName: opts.InterfaceName, - Holepunch: opts.Holepunch, - TlsClientCert: opts.TlsClientCert, - PingIntervalDuration: opts.PingInterval, - PingTimeoutDuration: opts.PingTimeout, - OverrideDNS: opts.OverrideDNS, - TunnelDNS: opts.TunnelDNS, - UpstreamDNS: upstreamDNS, - MatchDomains: opts.MatchDomains, - PreferLocalRoutes: opts.PreferLocalRoutes, - DisableRelay: opts.DisableRelay, - GatewaySiteIds: opts.GatewaySiteIDs, - GatewaySiteResourceId: opts.GatewaySiteResourceID, + Endpoint: endpoint, + ID: olmID, + Secret: olmSecret, + OrgID: orgID, + MTU: opts.MTU, + DNS: opts.DNS, + InterfaceName: opts.InterfaceName, + Holepunch: opts.Holepunch, + TlsClientCert: opts.TlsClientCert, + PingIntervalDuration: opts.PingInterval, + PingTimeoutDuration: opts.PingTimeout, + OverrideDNS: opts.OverrideDNS, + TunnelDNS: opts.TunnelDNS, + UpstreamDNS: upstreamDNS, + MatchDomains: opts.MatchDomains, + PreferLocalRoutes: opts.PreferLocalRoutes, + DisableRoutesAndAliasesOnExitNode: opts.ExitNodeTakesPrecedence, + DisableRelay: opts.DisableRelay, + GatewaySiteIds: opts.GatewaySiteIDs, + GatewaySiteResourceId: opts.GatewaySiteResourceID, // SubnetRouter: opts.SubnetRouter, UserToken: userToken, InitialFingerprint: initialFingerprint, diff --git a/internal/config/config.go b/internal/config/config.go index adb7ecb..5746ca5 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -52,6 +52,14 @@ type UpConfig struct { // Defaults to false. PreferLocalRoutes *bool `mapstructure:"prefer_local_routes" json:"prefer_local_routes,omitempty"` + // ExitNodeTakesPrecedence, when enabled, stops routes from being added + // for individual resources and stops their aliases from being resolved, + // so all traffic is sent through the exit node instead of directly to + // resources. Used as the default for `pangolin up`'s + // --exit-node-takes-precedence flag when the flag isn't passed + // explicitly. Defaults to false. + ExitNodeTakesPrecedence *bool `mapstructure:"exit_node_takes_precedence" json:"exit_node_takes_precedence,omitempty"` + // The exit node selected with `pangolin select exit-node`, re-applied by // `pangolin up`. Only the resource is stored (niceId is unique per org); // its sites are looked up from the server on every start so they can't @@ -87,6 +95,7 @@ var ConfigOptions = []string{ "up.override_dns", "up.match_domains_dns", "up.prefer_local_routes", + "up.exit_node_takes_precedence", "session_cookie_name", } @@ -274,6 +283,13 @@ func (c *Config) SetKey(key, value string) error { } c.Up.PreferLocalRoutes = &b c.v.Set(key, b) + case "up.exit_node_takes_precedence": + b, err := parseBool(value) + if err != nil { + return err + } + c.Up.ExitNodeTakesPrecedence = &b + c.v.Set(key, b) case "session_cookie_name": c.SessionCookieName = value c.v.Set(key, value) @@ -319,6 +335,11 @@ func (c *Config) GetKey(key string) (string, error) { return "", errConfigKeyUnset(key) } return fmt.Sprintf("%t", c.GetBool(key)), nil + case "up.exit_node_takes_precedence": + if !c.IsSet(key) { + return "", errConfigKeyUnset(key) + } + return fmt.Sprintf("%t", c.GetBool(key)), nil case "session_cookie_name": return c.SessionCookieName, nil default: @@ -380,6 +401,9 @@ func (c *Config) Save() error { if c.Up.PreferLocalRoutes != nil { c.v.Set("up.prefer_local_routes", *c.Up.PreferLocalRoutes) } + if c.Up.ExitNodeTakesPrecedence != nil { + c.v.Set("up.exit_node_takes_precedence", *c.Up.ExitNodeTakesPrecedence) + } // Written even when empty once they're in the file, so clearing the exit // node overwrites the previous value. if c.Up.ExitNodeNiceID != "" || c.v.IsSet("up.exit_node_nice_id") { From c06a554cb42974a8a3129d65b1540fd266ba8e68 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 28 Sep 2026 16:46:48 -0400 Subject: [PATCH 09/14] Move exit node save to the account storage --- cmd/select/exitnode/exitnode.go | 41 +++++++++++++++++++-------------- cmd/up/client/client.go | 6 ++++- cmd/up/client/exitnode.go | 25 ++++++++------------ cmd/up/client/exitnode_test.go | 33 ++++++++++++-------------- internal/config/accounts.go | 18 +++++++++++++++ internal/config/config.go | 26 --------------------- 6 files changed, 72 insertions(+), 77 deletions(-) diff --git a/cmd/select/exitnode/exitnode.go b/cmd/select/exitnode/exitnode.go index 8d21228..a280d05 100644 --- a/cmd/select/exitnode/exitnode.go +++ b/cmd/select/exitnode/exitnode.go @@ -62,7 +62,6 @@ func exitNodeMain(cmd *cobra.Command, opts *ExitNodeCmdOpts) error { } } - cfg := config.ConfigFromContext(cmd.Context()) apiClient := api.FromContext(cmd.Context()) accountStore := config.AccountStoreFromContext(cmd.Context()) @@ -72,6 +71,12 @@ func exitNodeMain(cmd *cobra.Command, opts *ExitNodeCmdOpts) error { return err } + activeAccount, err := accountStore.ActiveAccount() + if err != nil { + logger.Error("%v", err) + return err + } + gateways, err := apiClient.ListGatewayResources(orgID) if err != nil { logger.Error("Failed to list exit nodes: %v", err) @@ -84,22 +89,19 @@ func exitNodeMain(cmd *cobra.Command, opts *ExitNodeCmdOpts) error { usable = append(usable, g) } } - // The saved exit node, if it was selected in this org. - savedNiceID := "" - if cfg.Up.ExitNodeNiceID != "" && (cfg.Up.ExitNodeOrgID == "" || cfg.Up.ExitNodeOrgID == orgID) { - savedNiceID = cfg.Up.ExitNodeNiceID - } + // The saved exit node, scoped to the account's current org. + savedResourceID := activeAccount.ExitNodeResourceID // With a running client, the active exit node is the one it reports; // otherwise it is the saved one, which the next start will apply. isActive := func(g api.SiteResource) bool { if running { return status.GatewayActive && g.SiteResourceID == status.GatewaySiteResourceID } - return savedNiceID != "" && g.NiceID == savedNiceID + return savedResourceID != 0 && g.SiteResourceID == savedResourceID } // A saved exit node can outlive the active one (e.g. its sites weren't // connected on startup), so offer to clear it either way. - hasGateway := status.GatewayActive || cfg.Up.ExitNodeNiceID != "" + hasGateway := status.GatewayActive || savedResourceID != 0 if len(usable) == 0 && !hasGateway { err := fmt.Errorf("no exit nodes available in this organization") logger.Error("%v", err) @@ -135,8 +137,8 @@ func exitNodeMain(cmd *cobra.Command, opts *ExitNodeCmdOpts) error { return err } } - cfg.ClearExitNode() - if !saveExitNode(cfg, running) { + activeAccount.ClearExitNode() + if !saveExitNode(accountStore, activeAccount, running) { return fmt.Errorf("failed to save exit node") } logger.Success("Exit node disabled") @@ -150,8 +152,8 @@ func exitNodeMain(cmd *cobra.Command, opts *ExitNodeCmdOpts) error { return err } } - cfg.SetExitNode(orgID, selected.NiceID) - if !saveExitNode(cfg, running) { + activeAccount.SetExitNode(selected.SiteResourceID) + if !saveExitNode(accountStore, activeAccount, running) { return fmt.Errorf("failed to save exit node") } @@ -163,11 +165,16 @@ func exitNodeMain(cmd *cobra.Command, opts *ExitNodeCmdOpts) error { return nil } -// saveExitNode persists the exit node so the next `pangolin up` re-applies it. -// With a running client the change is already live, so a save failure is only -// a warning; without one, saving is the whole point, so it is an error. -func saveExitNode(cfg *config.Config, alreadyApplied bool) bool { - if err := cfg.Save(); err != nil { +// saveExitNode persists the exit node on the account so the next `pangolin up` +// re-applies it. With a running client the change is already live, so a save +// failure is only a warning; without one, saving is the whole point, so it is +// an error. +func saveExitNode(accountStore *config.AccountStore, account *config.Account, alreadyApplied bool) bool { + err := accountStore.UpdateActiveAccount(account) + if err == nil { + err = accountStore.Save() + } + if err != nil { if alreadyApplied { logger.Warning("Exit node applied but could not be saved for the next start: %v", err) return true diff --git a/cmd/up/client/client.go b/cmd/up/client/client.go index 5bcad6c..eb01817 100644 --- a/cmd/up/client/client.go +++ b/cmd/up/client/client.go @@ -436,10 +436,14 @@ func clientUpMain(cmd *cobra.Command, opts *ClientUpCmdOpts, extraArgs []string) // isn't applied. if !cmd.Flags().Changed("exit-node-site-ids") { var list func(string) ([]api.SiteResource, error) + savedResourceID := 0 if credentialsFromKeyring { list = apiClient.ListGatewayResources + if activeAccount, err := accountStore.ActiveAccount(); err == nil { + savedResourceID = activeAccount.ExitNodeResourceID + } } - opts.GatewaySiteResourceID, opts.GatewaySiteIDs = resolveSavedExitNode(cfg.Up, orgID, list) + opts.GatewaySiteResourceID, opts.GatewaySiteIDs = resolveSavedExitNode(savedResourceID, orgID, list) } else if len(opts.GatewaySiteIDs) > 0 && opts.GatewaySiteResourceID <= 0 { err := fmt.Errorf("--exit-node-site-ids requires --exit-node-resource-id") logger.Error("%v", err) diff --git a/cmd/up/client/exitnode.go b/cmd/up/client/exitnode.go index 2843f3d..3d94f9c 100644 --- a/cmd/up/client/exitnode.go +++ b/cmd/up/client/exitnode.go @@ -2,41 +2,36 @@ package client import ( "github.com/fosrl/cli/internal/api" - "github.com/fosrl/cli/internal/config" "github.com/fosrl/cli/internal/logger" ) // resolveSavedExitNode returns the resource ID and current site IDs of the exit -// node saved by `pangolin select exit-node`, or 0/nil if none should be -// applied. Only the resource's niceId is saved, so its ID and sites always come -// from the server and can't be stale (or break if the niceId was renamed away). +// node saved by `pangolin select exit-node` on the active account, or 0/nil if +// none should be applied. Only the resource ID is saved (scoped to the +// account's org), so its sites always come from the server and can't be stale +// (or break if the resource's niceId was renamed away). // // list is nil when there's no user session to query the server with. If the // saved exit node can't be resolved for any reason, the client connects // without one and says why. -func resolveSavedExitNode(up config.UpConfig, orgID string, list func(orgID string) ([]api.SiteResource, error)) (int, []int) { - if up.ExitNodeNiceID == "" { - return 0, nil - } - - if orgID != "" && up.ExitNodeOrgID != "" && up.ExitNodeOrgID != orgID { - logger.Info("Saved exit node '%s' belongs to a different organization; not using it", up.ExitNodeNiceID) +func resolveSavedExitNode(savedResourceID int, orgID string, list func(orgID string) ([]api.SiteResource, error)) (int, []int) { + if savedResourceID == 0 { return 0, nil } if list == nil || orgID == "" { - logger.Info("Saved exit node '%s' needs a logged-in session to look up; not using it (pass --exit-node-site-ids to set one explicitly)", up.ExitNodeNiceID) + logger.Info("Saved exit node needs a logged-in session to look up; not using it (pass --exit-node-site-ids to set one explicitly)") return 0, nil } gateways, err := list(orgID) if err != nil { - logger.Warning("Could not look up saved exit node '%s' (%v); connecting without it", up.ExitNodeNiceID, err) + logger.Warning("Could not look up saved exit node (%v); connecting without it", err) return 0, nil } for _, g := range gateways { - if g.NiceID != up.ExitNodeNiceID { + if g.SiteResourceID != savedResourceID { continue } if !g.Enabled || len(g.SiteIDs) == 0 { @@ -46,6 +41,6 @@ func resolveSavedExitNode(up config.UpConfig, orgID string, list func(orgID stri return g.SiteResourceID, g.SiteIDs } - logger.Warning("Saved exit node '%s' no longer exists; not using it. Run 'pangolin select exit-node' and choose None to forget it", up.ExitNodeNiceID) + logger.Warning("Saved exit node no longer exists; not using it. Run 'pangolin select exit-node' and choose None to forget it") return 0, nil } diff --git a/cmd/up/client/exitnode_test.go b/cmd/up/client/exitnode_test.go index b4bf2c7..85200cb 100644 --- a/cmd/up/client/exitnode_test.go +++ b/cmd/up/client/exitnode_test.go @@ -6,38 +6,35 @@ import ( "testing" "github.com/fosrl/cli/internal/api" - "github.com/fosrl/cli/internal/config" ) func TestResolveSavedExitNode(t *testing.T) { - saved := config.UpConfig{ExitNodeNiceID: "gw-a", ExitNodeOrgID: "org1"} + const savedResourceID = 12 lister := func(gws ...api.SiteResource) func(string) ([]api.SiteResource, error) { return func(string) ([]api.SiteResource, error) { return gws, nil } } tests := []struct { - name string - up config.UpConfig - org string - list func(string) ([]api.SiteResource, error) - want []int - wantID int + name string + resource int + org string + list func(string) ([]api.SiteResource, error) + want []int + wantID int }{ - {"nothing saved", config.UpConfig{}, "org1", lister(), nil, 0}, - {"uses the resource's current sites", saved, "org1", + {"nothing saved", 0, "org1", lister(), nil, 0}, + {"uses the resource's current sites", savedResourceID, "org1", lister(api.SiteResource{SiteResourceID: 11, NiceID: "gw-b", Enabled: true, SiteIDs: []int{1, 2}}, api.SiteResource{SiteResourceID: 12, NiceID: "gw-a", Enabled: true, SiteIDs: []int{2, 3}}), []int{2, 3}, 12}, - {"resource deleted", saved, "org1", lister(api.SiteResource{NiceID: "gw-b", Enabled: true, SiteIDs: []int{1, 2}}), nil, 0}, - {"same sites but different resource is not a match", saved, "org1", lister(api.SiteResource{NiceID: "gw-other", Enabled: true, SiteIDs: []int{1, 2}}), nil, 0}, - {"resource disabled", saved, "org1", lister(api.SiteResource{NiceID: "gw-a", Enabled: false, SiteIDs: []int{1, 2}}), nil, 0}, - {"resource has no sites", saved, "org1", lister(api.SiteResource{NiceID: "gw-a", Enabled: true}), nil, 0}, - {"different org", saved, "org2", lister(api.SiteResource{NiceID: "gw-a", Enabled: true, SiteIDs: []int{5}}), nil, 0}, - {"lookup fails: connect without it", saved, "org1", func(string) ([]api.SiteResource, error) { return nil, errors.New("boom") }, nil, 0}, - {"no session to look it up with", saved, "org1", nil, nil, 0}, + {"resource deleted", savedResourceID, "org1", lister(api.SiteResource{SiteResourceID: 11, NiceID: "gw-b", Enabled: true, SiteIDs: []int{1, 2}}), nil, 0}, + {"resource disabled", savedResourceID, "org1", lister(api.SiteResource{SiteResourceID: savedResourceID, NiceID: "gw-a", Enabled: false, SiteIDs: []int{1, 2}}), nil, 0}, + {"resource has no sites", savedResourceID, "org1", lister(api.SiteResource{SiteResourceID: savedResourceID, NiceID: "gw-a", Enabled: true}), nil, 0}, + {"lookup fails: connect without it", savedResourceID, "org1", func(string) ([]api.SiteResource, error) { return nil, errors.New("boom") }, nil, 0}, + {"no session to look it up with", savedResourceID, "org1", nil, nil, 0}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotID, got := resolveSavedExitNode(tt.up, tt.org, tt.list) + gotID, got := resolveSavedExitNode(tt.resource, tt.org, tt.list) if !reflect.DeepEqual(got, tt.want) || gotID != tt.wantID { t.Fatalf("got %d %v, want %d %v", gotID, got, tt.wantID, tt.want) } diff --git a/internal/config/accounts.go b/internal/config/accounts.go index 2190b1b..ae4ae23 100644 --- a/internal/config/accounts.go +++ b/internal/config/accounts.go @@ -29,6 +29,24 @@ type Account struct { OrgID string `mapstructure:"orgId" json:"orgId,omitempty"` OlmCredentials *OlmCredentials `mapstructure:"olmCredentials" json:"olmCredentials,omitempty"` ServerInfo *ServerInfo `mapstructure:"serverInfo" json:"serverInfo,omitempty"` + + // The exit node (a gateway-mode site resource) selected with `pangolin + // select exit-node`, re-applied by `pangolin up`. It can differ per + // account, so it's stored here rather than on the root config, and it + // belongs to the account's currently selected org (OrgID above). Only the + // resource ID is stored (not the niceId, which can be renamed); its sites + // are looked up from the server on every start so they can't go stale. + ExitNodeResourceID int `mapstructure:"exitNodeResourceId" json:"exitNodeResourceId,omitempty"` +} + +// SetExitNode records the account's selected exit node (a gateway resource). +func (a *Account) SetExitNode(resourceID int) { + a.ExitNodeResourceID = resourceID +} + +// ClearExitNode forgets the account's selected exit node. +func (a *Account) ClearExitNode() { + a.SetExitNode(0) } type OlmCredentials struct { diff --git a/internal/config/config.go b/internal/config/config.go index 5746ca5..579b89a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -59,24 +59,6 @@ type UpConfig struct { // --exit-node-takes-precedence flag when the flag isn't passed // explicitly. Defaults to false. ExitNodeTakesPrecedence *bool `mapstructure:"exit_node_takes_precedence" json:"exit_node_takes_precedence,omitempty"` - - // The exit node selected with `pangolin select exit-node`, re-applied by - // `pangolin up`. Only the resource is stored (niceId is unique per org); - // its sites are looked up from the server on every start so they can't - // go stale. Use SetExitNode / ClearExitNode. - ExitNodeNiceID string `mapstructure:"exit_node_nice_id" json:"exit_node_nice_id,omitempty"` - ExitNodeOrgID string `mapstructure:"exit_node_org_id" json:"exit_node_org_id,omitempty"` -} - -// SetExitNode records the selected exit node (a gateway resource). -func (c *Config) SetExitNode(orgID, niceID string) { - c.Up.ExitNodeOrgID = orgID - c.Up.ExitNodeNiceID = niceID -} - -// ClearExitNode forgets the selected exit node. -func (c *Config) ClearExitNode() { - c.SetExitNode("", "") } // CompanionAppDataDirs holds per-platform overrides for the desktop app data directory. @@ -404,14 +386,6 @@ func (c *Config) Save() error { if c.Up.ExitNodeTakesPrecedence != nil { c.v.Set("up.exit_node_takes_precedence", *c.Up.ExitNodeTakesPrecedence) } - // Written even when empty once they're in the file, so clearing the exit - // node overwrites the previous value. - if c.Up.ExitNodeNiceID != "" || c.v.IsSet("up.exit_node_nice_id") { - c.v.Set("up.exit_node_nice_id", c.Up.ExitNodeNiceID) - } - if c.Up.ExitNodeOrgID != "" || c.v.IsSet("up.exit_node_org_id") { - c.v.Set("up.exit_node_org_id", c.Up.ExitNodeOrgID) - } dir, err := GetPangolinConfigDir() if err != nil { From 408b53e38ddc91d08edae234120af77918ab4d98 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 28 Sep 2026 17:33:37 -0400 Subject: [PATCH 10/14] Remove resource from status --- cmd/status/client/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/status/client/client.go b/cmd/status/client/client.go index 6089175..ef3c6d2 100644 --- a/cmd/status/client/client.go +++ b/cmd/status/client/client.go @@ -147,7 +147,7 @@ func formatGateway(status *olm.StatusResponse) string { return "Off" } if status.GatewaySiteResourceID != 0 { - return fmt.Sprintf("Active (resource %d)", status.GatewaySiteResourceID) + return fmt.Sprintf("Active") } return "Active" } From 37a6a2a6a0bedd065a6e68b751f2471bf6649b8f Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 29 Sep 2026 10:52:39 -0400 Subject: [PATCH 11/14] REVERT: 72e706d6ef4fdf113b8db875000b46a694ffed07 --- cmd/up/client/client.go | 10 +- ubnet | 3343 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 3348 insertions(+), 5 deletions(-) create mode 100644 ubnet diff --git a/cmd/up/client/client.go b/cmd/up/client/client.go index eb01817..6b011aa 100644 --- a/cmd/up/client/client.go +++ b/cmd/up/client/client.go @@ -814,11 +814,11 @@ func clientUpMain(cmd *cobra.Command, opts *ClientUpCmdOpts, extraArgs []string) DisableRelay: opts.DisableRelay, GatewaySiteIds: opts.GatewaySiteIDs, GatewaySiteResourceId: opts.GatewaySiteResourceID, - // SubnetRouter: opts.SubnetRouter, - UserToken: userToken, - InitialFingerprint: initialFingerprint, - InitialPostures: initialPostures, - EnableUAPI: false, // ONLY FOR DEBUG: TODO MAKE FALSE + SubnetRouter: opts.SubnetRouter, + UserToken: userToken, + InitialFingerprint: initialFingerprint, + InitialPostures: initialPostures, + EnableUAPI: false, // ONLY FOR DEBUG: TODO MAKE FALSE } // Check if running with elevated permissions (required for network interface creation) diff --git a/ubnet b/ubnet new file mode 100644 index 0000000..06d2129 --- /dev/null +++ b/ubnet @@ -0,0 +1,3343 @@ +commit 408b53e38ddc91d08edae234120af77918ab4d98 (HEAD -> dev, origin/dev) +Author: Owen +Date: Mon Sep 28 17:33:37 2026 -0400 + + Remove resource from status + +commit c06a554cb42974a8a3129d65b1540fd266ba8e68 +Author: Owen +Date: Mon Sep 28 16:46:48 2026 -0400 + + Move exit node save to the account storage + +commit 570e9cc93b9f108268e2d1682123545e79570ac2 +Author: Owen +Date: Mon Sep 28 14:41:24 2026 -0400 + + Support the exit node takes precedence option + +commit 281ac1db9b086102870844dabbf7bc4cf38c9d26 +Author: Owen +Date: Fri Sep 25 17:19:19 2026 -0400 + + Gateway -> exit node + +commit f4bc3cd18dc95159da5a8454f26503e92edd06e7 +Author: Owen +Date: Fri Sep 25 15:11:40 2026 -0400 + + Support selecting exit node prior to starting + +commit cf1615326fab828f7105ed9dc49442649e0e6ed1 +Author: Owen +Date: Fri Sep 25 12:10:50 2026 -0400 + + Show gateway status + +commit b1dbaf5d34a7fd1ee7a285065a34c9b62869891f +Author: Owen +Date: Fri Sep 25 11:51:25 2026 -0400 + + Use the resource id for the gateway + +commit 2a3c161d93319b729983053212a289f17d40a2e7 +Author: Owen +Date: Fri Sep 25 11:18:29 2026 -0400 + + Handle exit nodes with nice id + +commit 3a68064c3e19f884c856b778ccf150e62a9a487d +Author: Owen +Date: Thu Sep 24 17:28:30 2026 -0400 + + Make the none preselected + +commit 1f3239ea00e1eba17c594fa33b7d99fc94da972d +Author: Owen +Date: Thu Sep 24 17:25:27 2026 -0400 + + Implement exit node selection + +commit 072e360ae8ce675bef3871ca328f3cdda3ea06da +Author: Owen +Date: Tue Sep 15 17:03:16 2026 -0400 + + Update docs + +commit b1ae3cc2922039f7d2cb7502a71187d426572306 +Author: Owen +Date: Tue Sep 15 15:26:39 2026 -0400 + + Update version + +commit 57863c8c5e69cd332ee5c073aa59bc6cdbbf5806 +Author: Owen +Date: Tue Sep 15 11:42:17 2026 -0400 + + Update flake for go + +commit 07e3e03f009f6472fb5c3c20c5853efee4a584c5 +Author: Owen +Date: Tue Sep 15 11:35:34 2026 -0400 + + Update go in test + +commit 72e706d6ef4fdf113b8db875000b46a694ffed07 +Author: Owen +Date: Tue Sep 15 11:33:38 2026 -0400 + + Remove subnet router + +commit 545c8ca598cab9c29030461ea14e2c437b69f195 +Author: Owen +Date: Tue Sep 15 11:28:20 2026 -0400 + + Update to go 26 + +commit 3619cab2543a8e57240dbf6b5e64f922af24cdb0 +Merge: f79982e 68787e5 +Author: Owen +Date: Tue Sep 15 11:25:39 2026 -0400 + + Merge branch 'main' into dev + +commit f79982e220421f9dfcea1b01eaa4e9b2b8f5f74d +Author: Owen +Date: Tue Sep 15 11:23:03 2026 -0400 + + Bump flake version + + Closes #129 + +commit 68787e5b920f9d58a07b43aa21fad785b3d090d3 +Merge: d7e0e85 ca71535 +Author: Owen Schwartz +Date: Tue Sep 15 11:22:14 2026 -0400 + + Merge pull request #140 from fosrl/dependabot/go_modules/go-dependencies-71b28d097c + + chore(deps): bump the go-dependencies group across 1 directory with 10 updates + +commit d7e0e85ef50267dc1a07385ed44c288605b48fdf +Merge: 24126b4 8e58a76 +Author: Owen Schwartz +Date: Tue Sep 15 11:21:34 2026 -0400 + + Merge pull request #141 from fosrl/dependabot/github_actions/github-actions-dependencies-75d2e5028a + + chore(deps): bump the github-actions-dependencies group across 1 directory with 8 updates + +commit 330d2b6da3ff5dc621ec85fee8fbe648188e1ad4 +Author: Owen +Date: Tue Sep 15 11:20:41 2026 -0400 + + Update newt + +commit 6cd76fb91ce6bf582008685a13aab62810cf41a3 +Author: Owen +Date: Mon Sep 14 15:12:40 2026 -0400 + + Write site config to the same location + +commit 6f4c09cb90085967dc38ff70468b897d2f2ae93a +Author: Owen +Date: Mon Sep 14 12:21:38 2026 -0400 + + Include correct version information and auto update + +commit 965b820aafd1dbd17cf6539a9123c8e44aace2ce +Author: Owen +Date: Mon Sep 14 12:21:28 2026 -0400 + + Use existing account if already logged in + +commit ecd32d7091ae588b56c5015700f87738580c7e09 +Author: Owen +Date: Fri Sep 11 13:55:56 2026 -0400 + + Add AI disclosure + +commit 8e58a761a35fec50e7510bf4f2c4fb5a343d5cc6 +Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> +Date: Wed Sep 9 20:34:55 2026 +0000 + + chore(deps): bump the github-actions-dependencies group across 1 directory with 8 updates + + Bumps the github-actions-dependencies group with 8 updates in the / directory: + + | Package | From | To | + | --- | --- | --- | + | [aws-actions/configure-aws-credentials](https://github.com/aws-actions/configure-aws-credentials) | `6.2.3` | `6.2.4` | + | [docker/login-action](https://github.com/docker/login-action) | `4.4.0` | `4.6.0` | + | [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) | `4.2.0` | `4.3.0` | + | [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) | `4.2.0` | `4.3.0` | + | [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) | `4.1.1` | `4.2.2` | + | [aquasecurity/trivy-action](https://github.com/aquasecurity/trivy-action) | `c07df6fec6fa692e6fd1200d50aaa1fdd66f03c8` | `d2a0b60797ff03db6132bd4e2b293f9b37081297` | + | [softprops/action-gh-release](https://github.com/softprops/action-gh-release) | `3.0.2` | `3.0.3` | + | [actions/stale](https://github.com/actions/stale) | `10.4.0` | `11.0.0` | + + + + Updates `aws-actions/configure-aws-credentials` from 6.2.3 to 6.2.4 + - [Release notes](https://github.com/aws-actions/configure-aws-credentials/releases) + - [Changelog](https://github.com/aws-actions/configure-aws-credentials/blob/main/CHANGELOG.md) + - [Commits](https://github.com/aws-actions/configure-aws-credentials/compare/e6de054238d6b7531b4efff3b6587d9aade6a06c...cbe3b392738ccf3f987d68400dafcf4b0624a56c) + + Updates `docker/login-action` from 4.4.0 to 4.6.0 + - [Release notes](https://github.com/docker/login-action/releases) + - [Commits](https://github.com/docker/login-action/compare/af1e73f918a031802d376d3c8bbc3fe56130a9b0...dbcb813823bdd20940b903addbd779551569679f) + + Updates `docker/setup-buildx-action` from 4.2.0 to 4.3.0 + - [Release notes](https://github.com/docker/setup-buildx-action/releases) + - [Commits](https://github.com/docker/setup-buildx-action/compare/bb05f3f5519dd87d3ba754cc423b652a5edd6d2c...37fe631027851001ddb9b187196cc803df7f5f0e) + + Updates `docker/setup-qemu-action` from 4.2.0 to 4.3.0 + - [Release notes](https://github.com/docker/setup-qemu-action/releases) + - [Commits](https://github.com/docker/setup-qemu-action/compare/96fe6ef7f33517b61c61be40b68a1882f3264fb8...1f40c72289eff860ee54a304f1438e3cff362e0a) + + Updates `actions/attest-build-provenance` from 4.1.1 to 4.2.2 + - [Release notes](https://github.com/actions/attest-build-provenance/releases) + - [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md) + - [Commits](https://github.com/actions/attest-build-provenance/compare/0f67c3f4856b2e3261c31976d6725780e5e4c373...4d101475d8b20a2381f78447822ac1eab6504dd8) + + Updates `aquasecurity/trivy-action` from c07df6fec6fa692e6fd1200d50aaa1fdd66f03c8 to d2a0b60797ff03db6132bd4e2b293f9b37081297 + - [Release notes](https://github.com/aquasecurity/trivy-action/releases) + - [Commits](https://github.com/aquasecurity/trivy-action/compare/c07df6fec6fa692e6fd1200d50aaa1fdd66f03c8...d2a0b60797ff03db6132bd4e2b293f9b37081297) + + Updates `softprops/action-gh-release` from 3.0.2 to 3.0.3 + - [Release notes](https://github.com/softprops/action-gh-release/releases) + - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) + - [Commits](https://github.com/softprops/action-gh-release/compare/3d0d9888cb7fd7b750713d6e236d1fcb99157228...efb35369e0ad2afab669f228072c1b0d510eae64) + + Updates `actions/stale` from 10.4.0 to 11.0.0 + - [Release notes](https://github.com/actions/stale/releases) + - [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md) + - [Commits](https://github.com/actions/stale/compare/1e223db275d687790206a7acac4d1a11bd6fe629...4391f3da665fdf50b6810c1a66712fb9ba21aa93) + + --- + updated-dependencies: + - dependency-name: aws-actions/configure-aws-credentials + dependency-version: 6.2.4 + dependency-type: direct:production + update-type: version-update:semver-patch + dependency-group: github-actions-dependencies + - dependency-name: docker/login-action + dependency-version: 4.6.0 + dependency-type: direct:production + update-type: version-update:semver-minor + dependency-group: github-actions-dependencies + - dependency-name: docker/setup-buildx-action + dependency-version: 4.3.0 + dependency-type: direct:production + update-type: version-update:semver-minor + dependency-group: github-actions-dependencies + - dependency-name: docker/setup-qemu-action + dependency-version: 4.3.0 + dependency-type: direct:production + update-type: version-update:semver-minor + dependency-group: github-actions-dependencies + - dependency-name: actions/attest-build-provenance + dependency-version: 4.2.2 + dependency-type: direct:production + update-type: version-update:semver-minor + dependency-group: github-actions-dependencies + - dependency-name: aquasecurity/trivy-action + dependency-version: d2a0b60797ff03db6132bd4e2b293f9b37081297 + dependency-type: direct:production + dependency-group: github-actions-dependencies + - dependency-name: softprops/action-gh-release + dependency-version: 3.0.3 + dependency-type: direct:production + update-type: version-update:semver-patch + dependency-group: github-actions-dependencies + - dependency-name: actions/stale + dependency-version: 11.0.0 + dependency-type: direct:production + update-type: version-update:semver-major + dependency-group: github-actions-dependencies + ... + + Signed-off-by: dependabot[bot] + +commit fefb20d4996699faa0a9921ce425d9e86697d083 +Author: Owen +Date: Tue Sep 8 17:40:09 2026 -0400 + + Support env vars identical to olm + +commit 2b74f13ff2426e297ea13590e7a15d23cbf86978 +Author: Owen +Date: Tue Sep 8 14:43:40 2026 -0400 + + Process status output and handle proper log storage location + +commit a8be3ded1d301f1311bddf640d7b7b2e4a417ce2 +Author: Owen +Date: Tue Sep 8 14:17:36 2026 -0400 + + Allow windows machine clients in the cli + +commit 51ae2fcb5a58557771dc57da414abe41b5f1e10c +Author: Owen +Date: Tue Sep 8 14:00:15 2026 -0400 + + Remove olm branding + +commit 24d1e7c2f3e5036d41dc4c2e56574c9906e7b813 +Author: Owen +Date: Tue Sep 8 12:20:26 2026 -0400 + + Clear logs when uninstalling the service + +commit 770a504043f6d0a8d27fbc232ad7dd716d71bdd4 +Author: Owen +Date: Tue Sep 8 11:48:26 2026 -0400 + + Add service control for windows, macos + +commit ca7153531df429bda69528657b61f08b349dac5b +Author: dependabot[bot] +Date: Fri Sep 4 20:34:35 2026 +0000 + + chore(nix): fix hash for updated go dependencies + +commit 7d880fd190cd9eb5aeff461ad3ba4a472e419765 +Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> +Date: Fri Sep 4 20:33:39 2026 +0000 + + chore(deps): bump the go-dependencies group across 1 directory with 10 updates + + Bumps the go-dependencies group with 8 updates in the / directory: + + | Package | From | To | + | --- | --- | --- | + | [github.com/Masterminds/semver/v3](https://github.com/Masterminds/semver) | `3.4.0` | `3.5.0` | + | [github.com/charmbracelet/bubbles](https://github.com/charmbracelet/bubbles) | `0.21.1-0.20250623103423-23b8fd6302d7` | `1.0.0` | + | [github.com/charmbracelet/huh](https://github.com/charmbracelet/huh) | `0.8.0` | `1.0.0` | + | [github.com/fosrl/olm](https://github.com/fosrl/olm) | `1.9.0` | `1.9.1` | + | [github.com/mattn/go-isatty](https://github.com/mattn/go-isatty) | `0.0.20` | `0.0.24` | + | [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) | `2.2.4` | `2.4.3` | + | [go.yaml.in/yaml/v3](https://github.com/yaml/go-yaml) | `3.0.4` | `3.0.5` | + | [golang.org/x/crypto](https://github.com/golang/crypto) | `0.53.0` | `0.55.0` | + + + + Updates `github.com/Masterminds/semver/v3` from 3.4.0 to 3.5.0 + - [Release notes](https://github.com/Masterminds/semver/releases) + - [Changelog](https://github.com/Masterminds/semver/blob/master/CHANGELOG.md) + - [Commits](https://github.com/Masterminds/semver/compare/v3.4.0...v3.5.0) + + Updates `github.com/charmbracelet/bubbles` from 0.21.1-0.20250623103423-23b8fd6302d7 to 1.0.0 + - [Release notes](https://github.com/charmbracelet/bubbles/releases) + - [Commits](https://github.com/charmbracelet/bubbles/commits/v1.0.0) + + Updates `github.com/charmbracelet/huh` from 0.8.0 to 1.0.0 + - [Release notes](https://github.com/charmbracelet/huh/releases) + - [Commits](https://github.com/charmbracelet/huh/compare/v0.8.0...v1.0.0) + + Updates `github.com/fosrl/olm` from 1.9.0 to 1.9.1 + - [Release notes](https://github.com/fosrl/olm/releases) + - [Commits](https://github.com/fosrl/olm/compare/1.9.0...1.9.1) + + Updates `github.com/mattn/go-isatty` from 0.0.20 to 0.0.24 + - [Commits](https://github.com/mattn/go-isatty/compare/v0.0.20...v0.0.24) + + Updates `github.com/pelletier/go-toml/v2` from 2.2.4 to 2.4.3 + - [Release notes](https://github.com/pelletier/go-toml/releases) + - [Commits](https://github.com/pelletier/go-toml/compare/v2.2.4...v2.4.3) + + Updates `go.yaml.in/yaml/v3` from 3.0.4 to 3.0.5 + - [Commits](https://github.com/yaml/go-yaml/compare/v3.0.4...v3.0.5) + + Updates `golang.org/x/crypto` from 0.53.0 to 0.55.0 + - [Commits](https://github.com/golang/crypto/compare/v0.53.0...v0.55.0) + + Updates `golang.org/x/sys` from 0.46.0 to 0.47.0 + - [Commits](https://github.com/golang/sys/compare/v0.46.0...v0.47.0) + + Updates `golang.org/x/term` from 0.44.0 to 0.45.0 + - [Commits](https://github.com/golang/term/compare/v0.44.0...v0.45.0) + + --- + updated-dependencies: + - dependency-name: github.com/Masterminds/semver/v3 + dependency-version: 3.5.0 + dependency-type: direct:production + update-type: version-update:semver-minor + dependency-group: go-dependencies + - dependency-name: github.com/charmbracelet/bubbles + dependency-version: 1.0.0 + dependency-type: direct:production + update-type: version-update:semver-major + dependency-group: go-dependencies + - dependency-name: github.com/charmbracelet/huh + dependency-version: 1.0.0 + dependency-type: direct:production + update-type: version-update:semver-major + dependency-group: go-dependencies + - dependency-name: github.com/fosrl/olm + dependency-version: 1.9.1 + dependency-type: direct:production + update-type: version-update:semver-patch + dependency-group: go-dependencies + - dependency-name: github.com/mattn/go-isatty + dependency-version: 0.0.24 + dependency-type: direct:production + update-type: version-update:semver-patch + dependency-group: go-dependencies + - dependency-name: github.com/pelletier/go-toml/v2 + dependency-version: 2.4.3 + dependency-type: direct:production + update-type: version-update:semver-minor + dependency-group: go-dependencies + - dependency-name: go.yaml.in/yaml/v3 + dependency-version: 3.0.5 + dependency-type: direct:production + update-type: version-update:semver-patch + dependency-group: go-dependencies + - dependency-name: golang.org/x/crypto + dependency-version: 0.55.0 + dependency-type: direct:production + update-type: version-update:semver-minor + dependency-group: go-dependencies + - dependency-name: golang.org/x/sys + dependency-version: 0.47.0 + dependency-type: direct:production + update-type: version-update:semver-minor + dependency-group: go-dependencies + - dependency-name: golang.org/x/term + dependency-version: 0.45.0 + dependency-type: direct:production + update-type: version-update:semver-minor + dependency-group: go-dependencies + ... + + Signed-off-by: dependabot[bot] + +commit a479fd6bc77064813517087cdc4dd04417456424 +Author: Owen +Date: Fri Sep 4 13:04:32 2026 -0400 + + Move service controls to service subcommand instead of under up + +commit 79eaada67a4f55a1271575441d1f8f44c93bf36a +Author: Owen +Date: Fri Sep 4 12:48:59 2026 -0400 + + Support installing systemd service + +commit e8063204e40b991f546f5fb1301c1149bc796d01 +Author: Owen +Date: Fri Sep 4 10:35:28 2026 -0400 + + Use the actual newt version + +commit 34e6e19871ecd773f218616cf3b32bb05baf07ce +Author: Owen +Date: Thu Sep 3 17:22:10 2026 -0400 + + Support site entrypoint + +commit 31db0241a9915ca4baff5a765f9527bc449ae97c +Author: Owen +Date: Thu Sep 3 16:57:46 2026 -0400 + + Include site up command to run newt + +commit 24126b4cee3c3b4f87c8323f4f975d23401d0033 +Merge: dd61170 5e7e964 +Author: Milo Schwartz +Date: Thu Sep 3 16:51:24 2026 -0400 + + Merge pull request #139 from fosrl/dev + + update readme + +commit 5e7e964ba0d89900b71f5d5fb1cba0fe5560e93a +Author: miloschwartz +Date: Thu Sep 3 16:50:28 2026 -0400 + + update readme + +commit 676fe2db36fe1ae7be0081733819d1dc391ee248 +Author: Owen +Date: Tue Sep 1 13:37:52 2026 -0400 + + Require sudo + +commit a2e021c779c31d90c85d898fce9e32c6b6d45a7e +Author: Owen +Date: Fri Aug 21 10:15:25 2026 -0400 + + Update version + +commit dd61170ef18ee8b76286f18416ce6124a86e4098 (tag: 0.16.0) +Merge: fa90273 7282f9c +Author: Owen Schwartz +Date: Fri Aug 21 09:20:27 2026 -0400 + + Merge pull request #133 from fosrl/dev + + 0.16.0 + +commit 7282f9cf1597d452ff7b2e066d8414c1ef67441f +Author: Owen +Date: Fri Aug 21 09:18:38 2026 -0400 + + Update flake + +commit 425f036e5f827d51c96fe67d6aed7ad2daf7d16a +Author: Owen +Date: Thu Aug 20 11:38:30 2026 -0400 + + Properly handled private resources keys for each client type + +commit 7a3623881b7a3db2b41be1d2a62dcef1e6b92bd2 +Merge: 2f0ce67 2962fee +Author: Owen +Date: Thu Aug 20 10:30:13 2026 -0400 + + Merge branch 'aig' into dev + +commit 2962fee974d60db106e2b6fd4ae66b4eee3d2c8f +Author: Owen +Date: Thu Aug 20 10:22:25 2026 -0400 + + Update olm and newt + +commit da509ac641668bbece1ee23489ad96197b7e2744 +Author: Owen +Date: Thu Aug 20 10:21:35 2026 -0400 + + Add gemini + +commit 11948fd7f95d1be5583bd952f9a0ec37054433be +Author: Owen +Date: Wed Aug 19 15:37:48 2026 -0400 + + Prompt for opencode providers + +commit ebdfcbaebb328e0314659338856a76f0aa4afb53 +Author: Owen +Date: Wed Aug 19 15:07:03 2026 -0400 + + Add a reset command to restore the config without the changes + +commit f537e1c69baafef9717052f7c02d66b316c48fc6 +Author: Owen +Date: Tue Aug 18 17:11:41 2026 -0400 + + Make sure config is cross platform + +commit b8ada5547f8ebc42c58f4687654d6519039d2d47 +Author: Owen +Date: Tue Aug 18 16:58:44 2026 -0400 + + Implement commands to write config for ai clients + +commit 8d7f1b37538989bbcb39a3043ab42dc1f42e5606 +Author: Owen +Date: Fri Aug 14 14:05:04 2026 -0400 + + Support applying blueprints with glob pattern * + +commit d0421836f2271fcbdcac0a674aa16de652bd3d0f +Author: Owen +Date: Wed Aug 5 10:33:54 2026 -0400 + + show the exit node status + +commit 0cbd465f8da13f94873da7ae908c92427549d4ae +Author: Owen +Date: Tue Aug 4 15:46:06 2026 -0400 + + add uapi to the tunnel config + +commit 2f0ce67fac668a94677f36c7600398a18260dfc2 +Author: Owen +Date: Mon Aug 3 17:33:16 2026 -0400 + + Bump version + +commit fa90273f53fe77d1cbd950cb0bcba69dd4318faf (tag: 0.15.1) +Merge: 6234839 4728563 +Author: Owen Schwartz +Date: Mon Aug 3 16:31:35 2026 -0400 + + Merge pull request #105 from fosrl/dependabot/github_actions/github-actions-dependencies-afb104c379 + + chore(deps): bump the github-actions-dependencies group across 1 directory with 11 updates + +commit 6234839b22c4a7f90d272b8b0ae7c2008965b064 +Merge: 61a4bd7 b2400ba +Author: Owen Schwartz +Date: Mon Aug 3 16:30:20 2026 -0400 + + Merge pull request #123 from fosrl/dev + + 0.15.1 + +commit b2400ba3013407ef7b1523d9e05fa195f6d0b975 +Author: Owen +Date: Mon Aug 3 15:57:37 2026 -0400 + + Update vendor hash + +commit 0c8d8026c4dab99c11fa49fca8d2f7280e39455f +Author: Owen +Date: Mon Aug 3 15:54:27 2026 -0400 + + Update olm + # + +commit 3e923b90edb72900cfdd5a190b421979096dacd1 +Author: Owen +Date: Mon Aug 3 15:26:27 2026 -0400 + + Support -i? + +commit 71cd7543a1dadc80ff13c7bcd96083c7aea0b656 +Author: Owen +Date: Fri Jul 31 15:16:58 2026 -0400 + + Allow the cookie name to be configurable + +commit 4728563837f7e9e36c216f9df1524c3be8afa9d5 (origin/dependabot/github_actions/github-actions-dependencies-afb104c379) +Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> +Date: Wed Jul 22 20:34:09 2026 +0000 + + chore(deps): bump the github-actions-dependencies group across 1 directory with 11 updates + + Bumps the github-actions-dependencies group with 11 updates in the / directory: + + | Package | From | To | + | --- | --- | --- | + | [aws-actions/configure-aws-credentials](https://github.com/aws-actions/configure-aws-credentials) | `6.0.0` | `6.2.3` | + | [actions/checkout](https://github.com/actions/checkout) | `6` | `7` | + | [docker/login-action](https://github.com/docker/login-action) | `4.2.0` | `4.4.0` | + | [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) | `3.12.0` | `4.2.0` | + | [docker/build-push-action](https://github.com/docker/build-push-action) | `6.19.2` | `7.3.0` | + | [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) | `3.7.0` | `4.2.0` | + | [actions/setup-go](https://github.com/actions/setup-go) | `6.1.0` | `7.0.0` | + | [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) | `4.1.0` | `4.1.1` | + | [aquasecurity/trivy-action](https://github.com/aquasecurity/trivy-action) | `bfa4b33a029b9aa80ddb784b45574c30e072c59e` | `c07df6fec6fa692e6fd1200d50aaa1fdd66f03c8` | + | [softprops/action-gh-release](https://github.com/softprops/action-gh-release) | `3.0.0` | `3.0.2` | + | [actions/stale](https://github.com/actions/stale) | `10.1.1` | `10.4.0` | + + + + Updates `aws-actions/configure-aws-credentials` from 6.0.0 to 6.2.3 + - [Release notes](https://github.com/aws-actions/configure-aws-credentials/releases) + - [Changelog](https://github.com/aws-actions/configure-aws-credentials/blob/main/CHANGELOG.md) + - [Commits](https://github.com/aws-actions/configure-aws-credentials/compare/8df5847569e6427dd6c4fb1cf565c83acfa8afa7...e6de054238d6b7531b4efff3b6587d9aade6a06c) + + Updates `actions/checkout` from 6 to 7 + - [Release notes](https://github.com/actions/checkout/releases) + - [Commits](https://github.com/actions/checkout/compare/v6...v7) + + Updates `docker/login-action` from 4.2.0 to 4.4.0 + - [Release notes](https://github.com/docker/login-action/releases) + - [Commits](https://github.com/docker/login-action/compare/650006c6eb7dba73a995cc03b0b2d7f5ca915bee...af1e73f918a031802d376d3c8bbc3fe56130a9b0) + + Updates `docker/setup-buildx-action` from 3.12.0 to 4.2.0 + - [Release notes](https://github.com/docker/setup-buildx-action/releases) + - [Commits](https://github.com/docker/setup-buildx-action/compare/8d2750c68a42422c14e847fe6c8ac0403b4cbd6f...bb05f3f5519dd87d3ba754cc423b652a5edd6d2c) + + Updates `docker/build-push-action` from 6.19.2 to 7.3.0 + - [Release notes](https://github.com/docker/build-push-action/releases) + - [Commits](https://github.com/docker/build-push-action/compare/10e90e3645eae34f1e60eeb005ba3a3d33f178e8...53b7df96c91f9c12dcc8a07bcb9ccacbed38856a) + + Updates `docker/setup-qemu-action` from 3.7.0 to 4.2.0 + - [Release notes](https://github.com/docker/setup-qemu-action/releases) + - [Commits](https://github.com/docker/setup-qemu-action/compare/c7c53464625b32c7a7e944ae62b3e17d2b600130...96fe6ef7f33517b61c61be40b68a1882f3264fb8) + + Updates `actions/setup-go` from 6.1.0 to 7.0.0 + - [Release notes](https://github.com/actions/setup-go/releases) + - [Commits](https://github.com/actions/setup-go/compare/v6.1.0...b7ad1dad31e06c5925ef5d2fc7ad053ef454303e) + + Updates `actions/attest-build-provenance` from 4.1.0 to 4.1.1 + - [Release notes](https://github.com/actions/attest-build-provenance/releases) + - [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md) + - [Commits](https://github.com/actions/attest-build-provenance/compare/a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32...0f67c3f4856b2e3261c31976d6725780e5e4c373) + + Updates `aquasecurity/trivy-action` from bfa4b33a029b9aa80ddb784b45574c30e072c59e to c07df6fec6fa692e6fd1200d50aaa1fdd66f03c8 + - [Release notes](https://github.com/aquasecurity/trivy-action/releases) + - [Commits](https://github.com/aquasecurity/trivy-action/compare/bfa4b33a029b9aa80ddb784b45574c30e072c59e...c07df6fec6fa692e6fd1200d50aaa1fdd66f03c8) + + Updates `softprops/action-gh-release` from 3.0.0 to 3.0.2 + - [Release notes](https://github.com/softprops/action-gh-release/releases) + - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) + - [Commits](https://github.com/softprops/action-gh-release/compare/b4309332981a82ec1c5618f44dd2e27cc8bfbfda...3d0d9888cb7fd7b750713d6e236d1fcb99157228) + + Updates `actions/stale` from 10.1.1 to 10.4.0 + - [Release notes](https://github.com/actions/stale/releases) + - [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md) + - [Commits](https://github.com/actions/stale/compare/997185467fa4f803885201cee163a9f38240193d...1e223db275d687790206a7acac4d1a11bd6fe629) + + --- + updated-dependencies: + - dependency-name: actions/attest-build-provenance + dependency-version: 4.1.1 + dependency-type: direct:production + update-type: version-update:semver-patch + dependency-group: github-actions-dependencies + - dependency-name: actions/checkout + dependency-version: '7' + dependency-type: direct:production + update-type: version-update:semver-major + dependency-group: github-actions-dependencies + - dependency-name: actions/setup-go + dependency-version: 6.5.0 + dependency-type: direct:production + update-type: version-update:semver-minor + dependency-group: github-actions-dependencies + - dependency-name: actions/stale + dependency-version: 10.3.0 + dependency-type: direct:production + update-type: version-update:semver-minor + dependency-group: github-actions-dependencies + - dependency-name: aquasecurity/trivy-action + dependency-version: c07df6fec6fa692e6fd1200d50aaa1fdd66f03c8 + dependency-type: direct:production + dependency-group: github-actions-dependencies + - dependency-name: aws-actions/configure-aws-credentials + dependency-version: 6.2.2 + dependency-type: direct:production + update-type: version-update:semver-minor + dependency-group: github-actions-dependencies + - dependency-name: docker/build-push-action + dependency-version: 7.3.0 + dependency-type: direct:production + update-type: version-update:semver-major + dependency-group: github-actions-dependencies + - dependency-name: docker/login-action + dependency-version: 4.4.0 + dependency-type: direct:production + update-type: version-update:semver-minor + dependency-group: github-actions-dependencies + - dependency-name: docker/setup-buildx-action + dependency-version: 4.2.0 + dependency-type: direct:production + update-type: version-update:semver-major + dependency-group: github-actions-dependencies + - dependency-name: docker/setup-qemu-action + dependency-version: 4.2.0 + dependency-type: direct:production + update-type: version-update:semver-major + dependency-group: github-actions-dependencies + - dependency-name: softprops/action-gh-release + dependency-version: 3.0.1 + dependency-type: direct:production + update-type: version-update:semver-patch + dependency-group: github-actions-dependencies + ... + + Signed-off-by: dependabot[bot] + +commit 61a4bd7f38da315fb3614ffd983f94fe0c66bdc8 +Merge: 824ca1f 89b4a4b +Author: Owen Schwartz +Date: Mon Jul 20 11:53:12 2026 -0400 + + Merge pull request #115 from fosrl/dev + + Add new cli args + +commit 89b4a4bd7d6496b3b08096cf2a704d38855e843f +Author: Owen +Date: Mon Jul 20 11:52:49 2026 -0400 + + Add new cli args + +commit 824ca1f14d1ffd4fecefb599b85f1130e5d7b45e +Merge: 91aed8f 5a7237a +Author: Owen Schwartz +Date: Sun Jul 19 14:52:23 2026 -0400 + + Merge pull request #114 from fosrl/dev + + Update version + +commit 5a7237a53993ab6eda6fb39d1b53c2819b9283f2 +Author: Owen +Date: Sun Jul 19 14:51:51 2026 -0400 + + Update version + +commit 91aed8f6aef125aa7e20d259c94167cd92d2ebd9 (tag: 0.15.0) +Merge: 6d2e563 7c481c6 +Author: Owen Schwartz +Date: Sun Jul 19 14:39:36 2026 -0400 + + Merge pull request #113 from fosrl/dev + + 0.15.0 + +commit 7c481c65c913290f8fb6a980a4249955731b9e1a +Author: Owen +Date: Sun Jul 19 14:37:13 2026 -0400 + + Update go mod and flake and add script + +commit d082617ada25a35dd51043205b39243294140e5f +Author: Owen +Date: Sat Jul 18 17:38:33 2026 -0400 + + add prefer local routes config + +commit 906fca6d2ba01c8a6e3d9450760f841cda7eb57f +Author: Owen +Date: Fri Jul 17 17:17:04 2026 -0400 + + Rename to match_domains_dns + +commit 0f5968c5338fd7f4ed5a8db981172f646bd0b55c +Author: Owen +Date: Fri Jul 17 16:41:57 2026 -0400 + + Properly allow setting match domains + +commit b05d9bae63c58571fe69ee389a676fe73889506f +Author: Owen +Date: Fri Jul 17 13:58:24 2026 -0400 + + Reflect local status in the status command + +commit 1ac23ad48f76a7cfe0dc4824778c9fd52f9c0e1f +Author: Owen +Date: Fri Jul 17 13:54:01 2026 -0400 + + Fix matched domains not compiling + +commit 79b531a3d0bd53b838b37515c10a3bf2b098a47b +Author: Owen +Date: Wed Jul 15 16:41:34 2026 -0400 + + Add match domains to config + +commit 6d2e563ca6d4e72417b341ba5caaa090834a95a1 (tag: 0.14.0) +Merge: 5ca974c 2f97dd0 +Author: Milo Schwartz +Date: Wed Jul 15 16:29:55 2026 -0400 + + Merge pull request #110 from fosrl/dev + + 0.14.0 + +commit 2f97dd064f6c2c9ec7cc3214ca2414f31c48b77c +Author: miloschwartz +Date: Wed Jul 15 15:47:29 2026 -0400 + + add config show command and update docs + +commit 558ff0d3674ae941f45857273871195b4f8c723e +Author: miloschwartz +Date: Fri Jul 10 18:05:04 2026 -0400 + + only list approved alises + +commit 956bc08896cc845f561e8eacfaa2082f9b1f40c0 +Author: miloschwartz +Date: Fri Jul 10 16:01:45 2026 -0400 + + add config options to up + +commit 5ca974ca6dfb9fd529d6bf740bf31c6033542a84 (tag: 0.13.0) +Merge: 8c1021f 172911d +Author: Owen Schwartz +Date: Wed Jul 8 10:08:03 2026 -0400 + + Merge pull request #106 from fosrl/dev + + 1.13.0 + +commit 172911d7f845143f0eddde4271aea28a7e370aef +Author: Owen +Date: Wed Jul 8 10:05:24 2026 -0400 + + Update flake + +commit f38b8ff6817a7d55e1c38d26e5716697a8cf826a +Author: Owen +Date: Wed Jul 8 09:46:46 2026 -0400 + + Update olm and newt + +commit d47575108ec29466ba52e2219b52bb27c1fcb602 +Author: Owen +Date: Tue Jul 7 17:56:34 2026 -0400 + + Update docs + +commit fea1230e95850441cee9f3ad74a11dfad66c283b +Author: Owen +Date: Tue Jul 7 11:08:50 2026 -0400 + + Remove the default dns for upstream and netstack + +commit 8c1021f7eceab18b54f5ef5d1bb16bae753e2670 +Merge: 927532c 8df17a7 +Author: Owen Schwartz +Date: Tue Jul 7 15:00:35 2026 -0400 + + Merge pull request #85 from fosrl/chore/dependabot-single-pr-groups + + chore(dependabot): group dependency updates into single PRs per ecosystem + +commit 927532c79e3210427a9b4eb1b0934903c09768c0 (tag: 0.12.0) +Merge: 5c57163 fbdac30 +Author: Milo Schwartz +Date: Wed Jul 1 20:47:45 2026 -0400 + + Merge pull request #102 from fosrl/dev + + update docs + +commit fbdac30019726907dc81762f2c1f38460b3938b5 +Author: miloschwartz +Date: Wed Jul 1 20:47:19 2026 -0400 + + update docs + +commit 5c5716369d6f4a9f16156dfb48618180340cc6ab +Merge: 7b911a4 3a4b71a +Author: Milo Schwartz +Date: Wed Jul 1 20:47:01 2026 -0400 + + Merge pull request #101 from fosrl/dev + + support labels in list alises + +commit 3a4b71a3580688e7db68ca6fa7624fd60197a5e2 +Author: miloschwartz +Date: Wed Jul 1 16:17:40 2026 -0400 + + support labels in list alises + +commit 7b911a44496e036f80ccdcac982f411449e76a28 (tag: 0.11.0) +Merge: 55be9ff 9bb77ba +Author: Milo Schwartz +Date: Fri Jun 26 18:09:21 2026 -0400 + + Merge pull request #100 from fosrl/dev + + update docs + +commit 9bb77bad9e281f614616d88f756b1e827e3ad442 +Author: miloschwartz +Date: Fri Jun 26 18:08:49 2026 -0400 + + disable docs action + +commit cb61d118d05a84deadfa242b1c9d7f01476cb90c +Author: miloschwartz +Date: Fri Jun 26 18:08:17 2026 -0400 + + update docs + +commit 55be9ff48407e0a56d49368e78d31565547e36f9 +Merge: 8baf493 f8f042e +Author: Milo Schwartz +Date: Fri Jun 26 17:59:10 2026 -0400 + + Merge pull request #99 from fosrl/dev + + 0.11.0 + +commit f8f042e4aaff0b47c5e1832df78521a0fd8acbeb +Author: miloschwartz +Date: Fri Jun 26 17:26:41 2026 -0400 + + add window companion mode support + +commit 8baf493206f780d6708b3cb1cc9562bf6f9d82d1 (tag: 0.10.2) +Merge: b42ee2f 10530d7 +Author: Owen Schwartz +Date: Thu Jun 25 08:10:33 2026 -0700 + + Merge pull request #96 from fosrl/dev + + Fix windows update to use the api + +commit 10530d7e0ffcc1bf1d72a639962621bc004b579e +Author: Owen +Date: Thu Jun 25 11:10:06 2026 -0400 + + Fix windows update to use the api + +commit b42ee2ff732f837f0467eec11e54d75c7513735e (tag: 1.10.2) +Merge: 3548f84 ec6b021 +Author: Owen Schwartz +Date: Thu Jun 25 07:50:36 2026 -0700 + + Merge pull request #95 from fosrl/dev + + 0.10.2 + +commit ec6b021fce21799b787cf8090ec119039ade4dcd +Author: Owen +Date: Thu Jun 25 10:49:33 2026 -0400 + + Update flake + +commit 09f8c01072298a9d6ee841d21ddbe858de724128 +Author: Owen +Date: Thu Jun 25 10:29:25 2026 -0400 + + Use the api for version checking; no gh rate limit + + Make debug work and check api for version + +commit afcbaeaede460529af3637a94e68df0276485282 +Author: Owen +Date: Thu Jun 25 10:18:53 2026 -0400 + + Update olm and newt + +commit ecaba475dc24a80869422416f45e104980380517 +Author: miloschwartz +Date: Wed Jun 24 16:31:43 2026 -0400 + + improve error message responses + +commit 3548f843a2fbac0b1659267be6bfdfb8e1ad9b98 (tag: 0.10.1) +Merge: c1b5b11 8872706 +Author: Owen Schwartz +Date: Thu Jun 11 15:06:49 2026 -0700 + + Merge pull request #94 from fosrl/dev + + Remove cert validation check + +commit 887270696695752b4355435e15cecc9e7b146398 +Merge: 404f1bf c1b5b11 +Author: Owen +Date: Thu Jun 11 12:22:34 2026 -0700 + + Merge branch 'main' into dev + +commit 404f1bfb5f8deaa717406cb3c901e2104f6dc82b +Author: Owen +Date: Thu Jun 11 11:25:38 2026 -0700 + + Remove cert validation check + +commit c1b5b1164a4096843d2cdb90c22b7db5a7f4e975 +Merge: 485ffd7 8d9c986 +Author: Milo Schwartz +Date: Wed Jun 10 16:00:05 2026 -0700 + + Merge pull request #87 from fosrl/chore/bot-regenerate-cli-docs + + chore: regenerate CLI documentation + +commit 8d9c986c8de1c26515b3a5667f758a66f5b92a27 (origin/chore/bot-regenerate-cli-docs) +Author: oschwartz10612 <4999704+oschwartz10612@users.noreply.github.com> +Date: Wed Jun 10 18:38:55 2026 +0000 + + chore: regenerate CLI documentation + +commit 485ffd7eb3c8930849715a64df40cafecd64c8ce (tag: 0.10.0) +Merge: 26ed4be 9bccb68 +Author: Owen Schwartz +Date: Wed Jun 10 11:38:18 2026 -0700 + + Merge pull request #86 from fosrl/dev + + 0.10.0 + +commit 9bccb68febfe0372584ad965cc97b6ee7269975e +Author: Owen +Date: Wed Jun 10 11:34:41 2026 -0700 + + Update flake + +commit 756b73cbeb8f45a3634d65cac714d8ba9a4d61d3 +Author: Owen +Date: Wed Jun 10 11:33:10 2026 -0700 + + Update nix + +commit 62677e9da65921d2cccc67a67a1902cf0a329d9d +Author: Owen +Date: Wed Jun 10 11:21:44 2026 -0700 + + Bump olm + +commit d583e023b5ac2f73fc7018db049b0154eaa67d8c +Merge: bd88e4f bc40199 +Author: Owen +Date: Wed Jun 10 11:03:22 2026 -0700 + + Merge branch 'dns' into dev + +commit bd88e4f554f24cbf9ad3beec53729cbbb0c48735 +Author: Owen +Date: Wed Jun 10 10:41:20 2026 -0700 + + Fix scp -r flag + +commit bc401991edff1a3ebbb59055eb4e899172d0351e +Author: Owen +Date: Tue Jun 9 20:51:15 2026 -0700 + + Add watchdog command and reset command + +commit 6c4b62642bd27af101baa93807f2a6c2899a0dc9 +Author: Owen +Date: Mon Jun 8 21:26:15 2026 -0700 + + Adjust exec args + +commit cf6fb93921b07e7076c85e85f3e53e62852811fa +Merge: 3a20145 26ed4be +Author: Owen +Date: Sun Jun 7 12:11:05 2026 -0700 + + Merge branch 'main' into dev + +commit 8df17a76b87c4f8620aad83869fe5769449e7bbf (origin/chore/dependabot-single-pr-groups) +Author: Marc Schäfer +Date: Sun Jun 7 11:15:14 2026 +0200 + + chore(dependabot): group dependency updates into single PRs per ecosystem + +commit 3a2014585b551fb283d08b01b311fd3423089cc3 +Author: Owen +Date: Fri Jun 5 13:56:14 2026 -0700 + + Filter the options for the ssh command if native + +commit 26ed4be0e13de906fc8591dd86a02bd6187f8d00 +Merge: b88f104 0137b14 +Author: Owen Schwartz +Date: Fri Jun 5 11:18:09 2026 -0700 + + Merge pull request #81 from fosrl/chore/bot-regenerate-cli-docs + + chore: regenerate CLI documentation + +commit d7c0e14cd53dc27809fa80f52d80c793ccfffb02 +Author: Owen +Date: Thu Jun 4 14:55:09 2026 -0700 + + Allow ctrl-c to exit the connecting spinner + +commit 0137b14884bdace3cbe63da3d9a1eef0cd1aff36 +Author: oschwartz10612 <4999704+oschwartz10612@users.noreply.github.com> +Date: Thu Jun 4 19:11:20 2026 +0000 + + chore: regenerate CLI documentation + +commit b88f10451ad1a786138d8c1e8af736b392faa7c2 (tag: 0.9.0) +Merge: 817148d d199a78 +Author: Owen Schwartz +Date: Thu Jun 4 12:11:06 2026 -0700 + + Merge pull request #80 from fosrl/dev + + 0.9.0 + +commit d199a788d552bea2aa3c9508e83f319d56be10c4 +Merge: d7c1526 817148d +Author: Owen +Date: Thu Jun 4 12:07:06 2026 -0700 + + Merge branch 'main' into dev + +commit 817148dd651e6991d9034538b333f3abee0cd82f +Merge: 51273d7 a1c21c6 +Author: Owen Schwartz +Date: Thu Jun 4 12:06:55 2026 -0700 + + Merge pull request #48 from fosrl/dependabot/github_actions/sigstore/cosign-installer-4.1.0 + + chore(deps): bump sigstore/cosign-installer from 4.0.0 to 4.1.2 + +commit a1c21c697be533415548f87332108a260e23fe42 (origin/dependabot/github_actions/sigstore/cosign-installer-4.1.0) +Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> +Date: Thu Jun 4 19:05:29 2026 +0000 + + chore(deps): bump sigstore/cosign-installer from 4.0.0 to 4.1.2 + + Bumps [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) from 4.0.0 to 4.1.2. + - [Release notes](https://github.com/sigstore/cosign-installer/releases) + - [Commits](https://github.com/sigstore/cosign-installer/compare/faadad0cce49287aee09b3a48701e75088a2c6ad...6f9f17788090df1f26f669e9d70d6ae9567deba6) + + --- + updated-dependencies: + - dependency-name: sigstore/cosign-installer + dependency-version: 4.1.0 + dependency-type: direct:production + update-type: version-update:semver-minor + ... + + Signed-off-by: dependabot[bot] + +commit 51273d7ad4d6a798aa98f0da0472b6b6a2d873ff +Merge: 02f2eae 4986d2b +Author: Owen Schwartz +Date: Thu Jun 4 12:04:22 2026 -0700 + + Merge pull request #29 from fosrl/dependabot/github_actions/actions/attest-build-provenance-3.2.0 + + chore(deps): bump actions/attest-build-provenance from 3.2.0 to 4.1.0 + +commit 02f2eaee1cf07dff934f60648dc0f118029d44b8 +Merge: a2abbed bd26695 +Author: Owen Schwartz +Date: Thu Jun 4 12:04:12 2026 -0700 + + Merge pull request #31 from fosrl/dependabot/github_actions/aquasecurity/trivy-action-0.34.0 + + chore(deps): bump aquasecurity/trivy-action from 97e0b3872f55f89b95b2f65b3dbab56962816478 to bfa4b33a029b9aa80ddb784b45574c30e072c59e + +commit a2abbed59e80dfa1db2109fe0d97e0a3b5a80f58 +Merge: 2394a69 98b7dd1 +Author: Owen Schwartz +Date: Thu Jun 4 12:04:09 2026 -0700 + + Merge pull request #32 from fosrl/dependabot/github_actions/docker/login-action-3.7.0 + + chore(deps): bump docker/login-action from 3.7.0 to 4.2.0 + +commit 2394a695f25334bda852f7148affbfce537321d1 +Merge: 36ed73b f2245fe +Author: Owen Schwartz +Date: Thu Jun 4 12:03:47 2026 -0700 + + Merge pull request #33 from fosrl/dependabot/github_actions/softprops/action-gh-release-2.5.0 + + chore(deps): bump softprops/action-gh-release from 2.4.2 to 3.0.0 + +commit 4986d2b390bb6edca14961b1aafc502f44b0cca7 +Author: dependabot[bot] +Date: Thu Jun 4 19:03:33 2026 +0000 + + chore(nix): fix hash for updated go dependencies + +commit 98b7dd14d379208d88f0da8d0fc7eeedeec237ed +Author: dependabot[bot] +Date: Thu Jun 4 19:03:14 2026 +0000 + + chore(nix): fix hash for updated go dependencies + +commit bd26695d34a7739b0957bff385fd48660ac920a0 +Author: dependabot[bot] +Date: Thu Jun 4 19:03:10 2026 +0000 + + chore(nix): fix hash for updated go dependencies + +commit f2245fee161cbdac650f8c505ab88778b17073e6 +Author: dependabot[bot] +Date: Thu Jun 4 19:03:05 2026 +0000 + + chore(nix): fix hash for updated go dependencies + +commit 20ebe6940b9e7fca8c7eb56ddb72ed5057db6e91 +Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> +Date: Thu Jun 4 19:02:25 2026 +0000 + + chore(deps): bump actions/attest-build-provenance from 3.2.0 to 4.1.0 + + Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 3.2.0 to 4.1.0. + - [Release notes](https://github.com/actions/attest-build-provenance/releases) + - [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md) + - [Commits](https://github.com/actions/attest-build-provenance/compare/96278af6caaf10aea03fd8d33a09a777ca52d62f...a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32) + + --- + updated-dependencies: + - dependency-name: actions/attest-build-provenance + dependency-version: 3.2.0 + dependency-type: direct:production + update-type: version-update:semver-minor + ... + + Signed-off-by: dependabot[bot] + +commit 3dc28eab01a7a5b52b8bb065d17f024437126835 +Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> +Date: Thu Jun 4 19:02:14 2026 +0000 + + chore(deps): bump docker/login-action from 3.7.0 to 4.2.0 + + Bumps [docker/login-action](https://github.com/docker/login-action) from 3.7.0 to 4.2.0. + - [Release notes](https://github.com/docker/login-action/releases) + - [Commits](https://github.com/docker/login-action/compare/c94ce9fb468520275223c153574b00df6fe4bcc9...650006c6eb7dba73a995cc03b0b2d7f5ca915bee) + + --- + updated-dependencies: + - dependency-name: docker/login-action + dependency-version: 3.7.0 + dependency-type: direct:production + update-type: version-update:semver-minor + ... + + Signed-off-by: dependabot[bot] + +commit 7bc735e095600e25111b013db38118d457d10cea +Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> +Date: Thu Jun 4 19:01:58 2026 +0000 + + chore(deps): bump aquasecurity/trivy-action + + Bumps [aquasecurity/trivy-action](https://github.com/aquasecurity/trivy-action) from 97e0b3872f55f89b95b2f65b3dbab56962816478 to bfa4b33a029b9aa80ddb784b45574c30e072c59e. + - [Release notes](https://github.com/aquasecurity/trivy-action/releases) + - [Commits](https://github.com/aquasecurity/trivy-action/compare/97e0b3872f55f89b95b2f65b3dbab56962816478...bfa4b33a029b9aa80ddb784b45574c30e072c59e) + + --- + updated-dependencies: + - dependency-name: aquasecurity/trivy-action + dependency-version: 0.34.0 + dependency-type: direct:production + update-type: version-update:semver-minor + ... + + Signed-off-by: dependabot[bot] + +commit 26d77dbcfc67e977ec9ca39b89bbe3d92c9b3645 +Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> +Date: Thu Jun 4 19:01:58 2026 +0000 + + chore(deps): bump softprops/action-gh-release from 2.4.2 to 3.0.0 + + Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.4.2 to 3.0.0. + - [Release notes](https://github.com/softprops/action-gh-release/releases) + - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) + - [Commits](https://github.com/softprops/action-gh-release/compare/5be0e66d93ac7ed76da52eca8bb058f665c3a5fe...b4309332981a82ec1c5618f44dd2e27cc8bfbfda) + + --- + updated-dependencies: + - dependency-name: softprops/action-gh-release + dependency-version: 2.5.0 + dependency-type: direct:production + update-type: version-update:semver-minor + ... + + Signed-off-by: dependabot[bot] + +commit 36ed73bbc162e93400359e51e3282b66645d519f +Merge: 0d92418 eca7fa8 +Author: Owen Schwartz +Date: Thu Jun 4 12:00:26 2026 -0700 + + Merge pull request #68 from fosrl/dependabot/go_modules/dminor-updates-0e33d7484c + + Bump the dminor-updates group across 1 directory with 3 updates + +commit d7c15268e47c385f9fedd1698385530d4d2f4269 +Author: Owen +Date: Mon Jun 1 11:32:12 2026 -0700 + + Add scp + +commit ee7cea56ed2f82ba521620b1fc8798dfbc7cac63 +Author: Owen +Date: Sun May 31 20:59:34 2026 -0700 + + Cert auth working with push mode native + +commit 0d924187ec21ba4f73b5a9a10ccc47b70309982b +Merge: 229f295 23c61c8 +Author: Owen Schwartz +Date: Wed May 20 15:33:32 2026 -0700 + + Merge pull request #76 from fosrl/dev + + Fix unsupported env context usage in a job if + +commit 23c61c80a6dca34fb71b8370f7d4106c4fe59d90 +Author: Owen +Date: Wed May 20 15:33:03 2026 -0700 + + Fix unsupported env context usage in a job if + +commit 229f295805a86cebe626321d2ce1572cc76c8fc4 (tag: 0.8.3) +Merge: 768f5b0 e3d9d3b +Author: Owen Schwartz +Date: Wed May 20 15:28:58 2026 -0700 + + Merge pull request #75 from fosrl/dev + + Better feedback when connecting with jit + +commit e3d9d3b9e967ffa62c4296b45926018c9e12f688 +Author: Owen +Date: Wed May 20 15:06:45 2026 -0700 + + Add better feedback when doing jit + +commit 3a30c98a336c7b554bc89269e71d480d1e6f6842 +Author: Owen +Date: Wed May 20 15:06:34 2026 -0700 + + Support both the cli docker hub repos + +commit 768f5b0822a5ea866c68806bb61c6638a2381017 (tag: 0.8.2) +Merge: 6e635d1 37e6869 +Author: Owen Schwartz +Date: Wed May 13 15:21:18 2026 -0700 + + Merge pull request #73 from fosrl/dev + + Update olm + +commit 37e686980b8357eec237710402a2a5d505fc5f9e +Author: Owen +Date: Wed May 13 15:20:30 2026 -0700 + + Update olm + +commit 8f168b856a1e2eb11d4589e3b48d702cca4eb654 +Author: Owen +Date: Wed May 13 15:11:47 2026 -0700 + + Adjust log message + +commit 6e635d195056d8c5b570721877abafffab041016 (tag: 0.8.1) +Merge: 5afbd94 c4c6002 +Author: Milo Schwartz +Date: Fri May 8 16:58:39 2026 -0700 + + Merge pull request #72 from fosrl/dev + + Dev + +commit c4c60024b9339e9039c34c6626403fe72c021511 +Author: miloschwartz +Date: Fri May 8 16:54:37 2026 -0700 + + bump olm + +commit 017092f4e0c5598a10fa6400613eab89f257f295 +Merge: d6a999a 5afbd94 +Author: miloschwartz +Date: Tue May 5 14:15:05 2026 -0700 + + Merge branch 'main' into dev + +commit d6a999aee89ed3737ef33d77c32cb3775443e248 +Author: miloschwartz +Date: Tue May 5 14:14:49 2026 -0700 + + improve no up client message on ssh + +commit eca7fa88d4f5a881ee48406d846f160ab0d6b57e (origin/dependabot/go_modules/dminor-updates-0e33d7484c) +Author: dependabot[bot] +Date: Tue Apr 28 04:24:00 2026 +0000 + + chore(nix): fix hash for updated go dependencies + +commit 0944fc385bb60172540899f862cedc885f15502f +Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> +Date: Tue Apr 28 04:23:01 2026 +0000 + + Bump the dminor-updates group across 1 directory with 3 updates + + Bumps the dminor-updates group with 1 update in the / directory: [golang.org/x/crypto](https://github.com/golang/crypto). + + + Updates `golang.org/x/crypto` from 0.49.0 to 0.50.0 + - [Commits](https://github.com/golang/crypto/compare/v0.49.0...v0.50.0) + + Updates `golang.org/x/sys` from 0.42.0 to 0.43.0 + - [Commits](https://github.com/golang/sys/compare/v0.42.0...v0.43.0) + + Updates `golang.org/x/term` from 0.41.0 to 0.42.0 + - [Commits](https://github.com/golang/term/compare/v0.41.0...v0.42.0) + + --- + updated-dependencies: + - dependency-name: golang.org/x/crypto + dependency-version: 0.50.0 + dependency-type: direct:production + update-type: version-update:semver-minor + dependency-group: dminor-updates + - dependency-name: golang.org/x/sys + dependency-version: 0.43.0 + dependency-type: direct:production + update-type: version-update:semver-minor + dependency-group: dminor-updates + - dependency-name: golang.org/x/term + dependency-version: 0.42.0 + dependency-type: direct:production + update-type: version-update:semver-minor + dependency-group: dminor-updates + ... + + Signed-off-by: dependabot[bot] + +commit 5afbd94e2ddc1e707baa1a9c2425a8a53d197171 (tag: 0.8.0) +Merge: e3914b7 7392569 +Author: Owen Schwartz +Date: Mon Apr 27 21:21:42 2026 -0700 + + Merge pull request #67 from fosrl/dev + + 1.8.0 + +commit 7392569a58cb1a0c45f49602ee0e98f2f6410622 +Author: Owen +Date: Mon Apr 27 20:16:43 2026 -0700 + + Update olm, newt + +commit 135260ea5f8b3f06aebd8bcb6e9ff11a11f6ad4c +Author: miloschwartz +Date: Mon Apr 27 11:41:51 2026 -0700 + + dont run docs workflow on bot pr + +commit e3914b7ce615e4176a3e30f2ae447e63d631fdd6 +Merge: 5adc480 05368bc +Author: Milo Schwartz +Date: Mon Apr 27 11:39:51 2026 -0700 + + Merge pull request #66 from fosrl/chore/bot-regenerate-cli-docs + + chore: regenerate CLI documentation + +commit 05368bc45a7d4c30ff5f526a15dc1b5ed49537d6 +Author: miloschwartz <6850869+miloschwartz@users.noreply.github.com> +Date: Mon Apr 27 18:37:54 2026 +0000 + + chore: regenerate CLI documentation + +commit 5adc480762dbcb2f173d40b91679e10e325ae6ac +Merge: 4f3e44d 449a6c9 +Author: Milo Schwartz +Date: Mon Apr 27 11:37:38 2026 -0700 + + Merge pull request #65 from fosrl/dev + + remove cobra footer + +commit 449a6c90b4674b969320ded6bde2a66e1a6786e4 +Author: miloschwartz +Date: Mon Apr 27 11:37:05 2026 -0700 + + remove cobra footer + +commit 4f3e44d6851841b455645c83019135a32419f01b +Merge: b597565 9a1b012 +Author: Milo Schwartz +Date: Mon Apr 27 11:31:17 2026 -0700 + + Merge pull request #64 from fosrl/chore/bot-regenerate-cli-docs + + chore: regenerate CLI documentation + +commit 9a1b012e7a2e5c1eea8250a1ecd0a216bf14c74a +Author: miloschwartz <6850869+miloschwartz@users.noreply.github.com> +Date: Mon Apr 27 18:27:00 2026 +0000 + + chore: regenerate CLI documentation + +commit b59756577f29a80e6e4904e99fd611ce260db926 +Merge: 81c1df8 f675894 +Author: Milo Schwartz +Date: Mon Apr 27 11:26:19 2026 -0700 + + Merge pull request #63 from fosrl/dev + + open pr instead of commit in update docs workflow + +commit f67589406359f6245d63fd6fd4903d8e86be9be5 +Author: miloschwartz +Date: Mon Apr 27 11:25:38 2026 -0700 + + open pr instead of commit in update docs workflow + +commit 81c1df8ad2d273819420399e26b66f9e04fd3d2c +Merge: f58533a 972d897 +Author: Milo Schwartz +Date: Mon Apr 27 11:22:56 2026 -0700 + + Merge pull request #62 from fosrl/dev + + add auto gen docs action workflow + +commit 972d8975eab16fe408900f58436f2c15244424f9 +Author: miloschwartz +Date: Mon Apr 27 11:22:21 2026 -0700 + + add auto gen docs action workflow + +commit f58533a090e7ad931cde9d53a9b5672422ffba62 (tag: 0.7.0) +Merge: 8a2b53e 6dbf513 +Author: Milo Schwartz +Date: Sun Apr 26 21:06:35 2026 -0700 + + Merge pull request #61 from fosrl/dev + + Dev + +commit 6dbf513cd73c69c47c0788fe53d3340f81cc42cf +Author: miloschwartz +Date: Sun Apr 26 20:59:10 2026 -0700 + + add confirmation dialog pre update + +commit dc7bc4e7dd1f6c78a06bff28005a810784df9441 +Author: miloschwartz +Date: Sun Apr 26 18:11:51 2026 -0700 + + support update windows cli + +commit 158d1d2f95c233b5b8318c07376ca590fa459678 +Author: miloschwartz +Date: Sun Apr 26 17:44:14 2026 -0700 + + add wix installer and icon + +commit 8c5001efd503e120d3d017c1221529eb3c053cf8 +Author: miloschwartz +Date: Fri Apr 24 15:41:14 2026 -0700 + + support full openssh flag passthrough for ssh + +commit 8a2b53ed784601d2eb383ab7a51f6de22fc48f82 (tag: 0.6.2) +Merge: fca7be8 073bb73 +Author: Owen Schwartz +Date: Fri Apr 24 13:46:06 2026 -0700 + + Merge pull request #58 from fosrl/dev + + Update newt + +commit 073bb73e53fb254af85863b59470685e085ecae7 +Author: Owen +Date: Fri Apr 24 13:45:32 2026 -0700 + + Update newt + +commit fca7be8492cf0f3dd115b3d53b490e877b69353b (tag: 0.6.1) +Merge: d9dad3b 8d363bf +Author: Owen Schwartz +Date: Thu Apr 23 17:10:45 2026 -0700 + + Merge pull request #57 from fosrl/dev + + 0.6.1 + +commit 8d363bf32450cff10508b68462a3e0544b0e6e68 +Merge: 2155b57 d9dad3b +Author: Owen +Date: Thu Apr 23 17:09:52 2026 -0700 + + Merge branch 'main' into dev + +commit 2155b57ea386e0b7c62365b2bb161d2c31eba69e +Merge: b21fa57 9dc88f7 +Author: Owen Schwartz +Date: Thu Apr 23 17:05:46 2026 -0700 + + Merge pull request #56 from LaurenceJJones/feat/blueprint-stdin + + enhance(blueprint): Accept input from stdin + +commit 9dc88f7be48be85bf451904937a3e7d105a272f7 +Author: Laurence +Date: Thu Apr 23 12:29:09 2026 +0100 + + enhance(blueprint): Accept input from stdin + + Allow users to pipe output of commands to stdin for the blueprint file, this is useful when you want to send a blueprint file but dont want to leave any artifacts on disk + +commit b21fa573282149f6b0a694047df95a88e4c5e600 +Merge: 6f6206c 7de8d5b +Author: Owen Schwartz +Date: Tue Apr 21 13:48:20 2026 -0700 + + Merge pull request #54 from LaurenceJJones/feat/apply-blueprint-api-key + + enhance(blueprint): support integration api usage + +commit 7de8d5bf1fb801f50ac3794e7eae7dc423fea376 +Author: Laurence +Date: Tue Apr 21 20:20:45 2026 +0100 + + revert generated docs + +commit adfc5518f78d755b2ee3f8a65f979e6fca20355c +Author: Laurence +Date: Tue Apr 21 20:17:09 2026 +0100 + + Enforce that if intent is to use the integration api all flags must be provided + +commit b9ef2f8c5cc8c0a1ac1ddf4430e08f187a6319f9 +Merge: 054906b 6f6206c +Author: Laurence +Date: Tue Apr 21 12:25:25 2026 +0100 + + Merge upstream/dev into feat/apply-blueprint-api-key + + Resolve blueprint.go: keep API key/session client flow and upstream + interpolateBlueprint ({{ env.VAR }}) before apply. + + Made-with: Cursor + +commit 054906b882461739a26190ca7c7dc61c47c630eb +Author: Laurence +Date: Tue Apr 21 12:21:44 2026 +0100 + + enhance(blueprint): support integration api usage + +commit 6f6206c400bcac8433f76377775bade729426778 +Author: Owen +Date: Mon Apr 13 20:22:09 2026 -0700 + + Update script to look for old bins + +commit d9dad3b658a590b61fefdadbd95b8dabdce18b64 (tag: 0.6.0) +Merge: c5f727d 068443d +Author: Milo Schwartz +Date: Mon Apr 13 16:56:08 2026 -0700 + + Merge pull request #52 from fosrl/dev + + 0.6.0 + +commit 068443dade48a89c5fbcd925db9899915b2216de +Author: Owen +Date: Mon Apr 13 15:13:43 2026 -0700 + + Fix update by passing bin location + +commit f916dbf6d63f5a325867650fe5e28bd11f4df288 +Author: Owen +Date: Sat Apr 11 21:13:15 2026 -0700 + + Bring {{env.ENV_VAR}} support to cli blueprints + +commit 3bceb31a8d8de905362d5948d9deb9fface8d063 +Author: miloschwartz +Date: Sat Apr 11 20:57:41 2026 -0700 + + add list aliases command + +commit 56136151cb0bc3526c0e08bbb9231315696ad34f +Author: Owen +Date: Tue Apr 7 11:34:59 2026 -0400 + + Add CODEOWNERS + +commit 5b86d71f36edbe9d807879fddee2bd540c5e329b +Merge: 662577c f740576 +Author: Owen +Date: Thu Apr 2 17:54:49 2026 -0400 + + Merge branch 'jit' into dev + +commit 662577c184a7efd37ea430d07a2d7d67981e1cdc +Author: Owen +Date: Thu Apr 2 17:53:41 2026 -0400 + + Add preshared key env var + + AUTH_DAEMON_PRE_SHARED_KEY + +commit c5f727df7e8429969e36a7e6fcfe6bc45fed9715 +Merge: 2a55679 88cf8ee +Author: Owen Schwartz +Date: Wed Mar 25 09:12:12 2026 -0700 + + Merge pull request #51 from LaurenceJJones/feat/installer-posix-compatibility + + enhance(install): prefer /usr/local/bin and posix compliance + +commit 88cf8eefec3aa07036ae7689ac833a69ec1f07e4 +Author: Laurence +Date: Wed Mar 25 13:50:43 2026 +0000 + + enhance(install): prefer /usr/local/bin and posix compliance + + Always install to /usr/local/bin and improve posix compliance so other shells can be used + +commit f7405762c8c9ffaff6c4eaf6a12edc09da41e570 +Author: Owen +Date: Sun Mar 22 13:59:31 2026 -0700 + + Support an array of message ids + +commit c2fea74b8a30e304c8f87e2e3d56ddb0fd14cfe4 +Author: Owen +Date: Fri Mar 20 17:53:18 2026 -0700 + + Handle both types of siteds + +commit 2a556799d6d5fa31901c882e7b0ff439bdd60986 (tag: 0.5.3) +Merge: d854ab6 566dbb4 +Author: Owen Schwartz +Date: Thu Mar 19 16:19:19 2026 -0700 + + Merge pull request #49 from fosrl/dev + + Bump olm + +commit 566dbb4c6c928eca0a0d9c46bf19cdfeee1d684f +Author: Owen +Date: Thu Mar 19 16:18:32 2026 -0700 + + Bump olm + +commit d854ab693fec023fa6257ff5de51f8f62b99122b (tag: 0.5.2) +Merge: 0fb37c0 5a42952 +Author: Owen Schwartz +Date: Tue Mar 17 17:58:42 2026 -0700 + + Merge pull request #46 from fosrl/dev + + Update cicd flow + +commit 5a429527a861b31e39c04446c33799fa15fb4ade +Author: Owen +Date: Tue Mar 17 17:58:12 2026 -0700 + + Remove local + +commit 064a04a102d5554300538754b194fac9a78394fe +Author: Owen +Date: Tue Mar 17 17:56:40 2026 -0700 + + Update cicd flow + +commit 597d07008179720c0ec9c2be88984f991902a040 +Merge: a8e4b48 0fb37c0 +Author: Owen +Date: Tue Mar 17 17:13:22 2026 -0700 + + Merge branch 'main' into dev + +commit 0fb37c033de08009a36382be3a913263c9415522 +Author: Owen +Date: Tue Mar 17 17:10:40 2026 -0700 + + Update olm + +commit c7ddfc96a6dac7b1ba11d38f4982884159cab22c +Author: Owen +Date: Tue Mar 17 17:00:25 2026 -0700 + + Bump newt + +commit 57159819d0ab36e750410ca89d364e8fc25a939c +Author: Owen +Date: Tue Mar 17 16:58:20 2026 -0700 + + Clarify default + +commit a8e4b482d417804a86d8f8ed9160fa171d2e962a +Author: Owen +Date: Tue Mar 17 17:10:40 2026 -0700 + + Update olm + +commit 351c51a16fe12455a3fd915b7e6b0cb4a35b1b39 +Author: Owen +Date: Tue Mar 17 17:00:25 2026 -0700 + + Bump newt + +commit dc5d90f387415cca279027fbab2e73d353d0fc92 +Author: Owen +Date: Tue Mar 17 16:58:20 2026 -0700 + + Clarify default + +commit 8d166f745ea90b66582bbfb8404ec917e06edefa +Author: Laurence +Date: Mon Mar 9 15:58:45 2026 +0000 + + fix(windows): Use ACL instead of chmod + + Golang chmod function on windows does not alter ACL's to be 'user only read' it simply changes the read permissions, instead we must use specific windows callouts to set the permissions to be user only preventing ssh from complaining about weak permissions + +commit eb3f550f45def6b6b28988e163d9f7e7072f27f5 +Author: Owen +Date: Fri Mar 6 15:14:22 2026 -0800 + + Jit message and wait working + +commit cefc55f4697cdffa6a2eb6816482dc2128135549 +Author: Owen +Date: Fri Mar 6 15:01:10 2026 -0800 + + Call jit on ssh and watch for status + +commit ebf90465f587091e5509beb8abb5fdb4970ea43c (tag: 0.5.1) +Author: Owen +Date: Thu Feb 26 21:49:22 2026 -0800 + + Pull from ecr + +commit 6acedb73ce3bec8dba664335ef1d2c3c21e43c75 +Merge: b34f562 52bf022 +Author: Owen +Date: Thu Feb 26 20:57:07 2026 -0800 + + Merge branch 'main' of github.com:fosrl/cli + +commit 52bf022d72ad3a64cae028a226312cbbcdde710c +Merge: b718931 f5f8a23 +Author: Owen Schwartz +Date: Thu Feb 26 20:57:00 2026 -0800 + + Merge pull request #40 from fosrl/dev + + generate random password for users + +commit f5f8a23e62949670aa0ab99f8cedfee01577e190 +Author: Owen +Date: Thu Feb 26 20:56:26 2026 -0800 + + Bump newt + +commit 08c724219e7dacb9f4224d9f9b88359e5c51d8b9 +Author: miloschwartz +Date: Thu Feb 26 10:30:50 2026 -0800 + + generate random password for use + +commit b718931d858bc8b67bd9ca1efbf3fc49d4a483fd +Author: miloschwartz +Date: Wed Feb 25 17:00:57 2026 -0800 + + make docs + +commit cd491cc541811fb036c7893e963250dafe57e9e6 (tag: 0.5.0) +Merge: 80d39fb 34cc588 +Author: miloschwartz +Date: Wed Feb 25 16:37:56 2026 -0800 + + Merge branch 'dev' + +commit 34cc588a0cdfb1bbea3974cfee9b2f42d39054e2 +Merge: 825acc8 a1569fa +Author: miloschwartz +Date: Wed Feb 25 16:37:51 2026 -0800 + + Merge branch 'main' into dev + +commit 825acc82cb247a98a166cfb6d7543f5265e18a1e +Author: miloschwartz +Date: Wed Feb 25 16:37:47 2026 -0800 + + bump olm + +commit 80d39fb55d373b384513eadcabb14449252252a9 +Merge: a1569fa ee114d0 +Author: Milo Schwartz +Date: Wed Feb 25 16:35:53 2026 -0800 + + Merge pull request #39 from fosrl/dev + + 0.5.0 + +commit ee114d043592694798c227678cbc7a36c5aad0e6 +Author: miloschwartz +Date: Wed Feb 25 16:31:02 2026 -0800 + + bump version + +commit e518b673ba201eccfedd78e51625ecc6d8c48a5b +Author: miloschwartz +Date: Wed Feb 25 16:30:43 2026 -0800 + + update readme + +commit 5e519960f5112a07c90572a081a8db6c6f1e96a7 +Author: miloschwartz +Date: Wed Feb 25 16:23:04 2026 -0800 + + show example command + +commit d3bbf97dc7f9b0af65b3f5fc4ada6307cf8b43bd +Author: miloschwartz +Date: Wed Feb 25 16:20:11 2026 -0800 + + dont support auth daemon on darwin + +commit 2ec3be373013704f3ea1534170c52d7f1a179e2c +Author: miloschwartz +Date: Wed Feb 25 16:16:31 2026 -0800 + + clean up help + +commit f321982c1b4ca414ade27aa04b9e8054b967cff2 +Author: miloschwartz +Date: Wed Feb 25 16:11:01 2026 -0800 + + use system ssh on windows + +commit 30cd8047d374e51b904f29b90e42d1f0daf511ea +Author: miloschwartz +Date: Wed Feb 25 15:53:27 2026 -0800 + + use raw in windwos + +commit 9e01e1e1ca401ae7bb548a1641b5d92dd8785002 +Author: Owen +Date: Wed Feb 25 15:22:26 2026 -0800 + + Test if connected + +commit 5acd20355864d70491729b811b09a7205d3ffe75 +Author: Owen +Date: Wed Feb 25 14:59:31 2026 -0800 + + Add iss file + +commit e8cf28e05e3e5f3afcc42c2f3d3763aabf1d657b +Author: Owen +Date: Wed Feb 25 14:56:46 2026 -0800 + + Add windows to make + +commit f644fab06a3d3fa160a543c105a05040d38ae771 +Author: Owen +Date: Wed Feb 25 14:55:03 2026 -0800 + + Support windows + +commit a1569fa7b54a5569328ab0ee26b58eb2b12d12bd (tag: 0.4.0) +Merge: 91fbc32 4fd373e +Author: Milo Schwartz +Date: Sun Feb 22 16:40:26 2026 -0800 + + Merge pull request #36 from fosrl/dev + + 0.4.0 + +commit 4fd373ecb56f128ee419a92cb1825d5565949fd8 +Author: miloschwartz +Date: Sun Feb 22 16:37:46 2026 -0800 + + bump newt + +commit 490ea23cdaffb07b0c3e4b621eb1dec9d716307e +Author: miloschwartz +Date: Sun Feb 22 16:20:27 2026 -0800 + + remove replace in go.mod + +commit a4428a5295c3a95aa8120a482c11faf491b3fb52 +Author: miloschwartz +Date: Sun Feb 22 16:18:38 2026 -0800 + + bump version, bump olm version, update docs + +commit bb62fa8d664387b37f8648c7f0de31b424aca23d +Author: Owen +Date: Wed Feb 18 15:39:09 2026 -0800 + + Use a context to allow switch org to work + +commit 58aee8fe5711cc762da6feae7ffa03e36de8aacf +Author: Owen +Date: Wed Feb 18 15:15:50 2026 -0800 + + Use correct org_id + +commit 90fd90423f43db88df8a4111a1bc498f27355356 +Author: miloschwartz +Date: Tue Feb 17 22:31:50 2026 -0800 + + clean errors + +commit a2767c8372892d33eb522f2cd8e6a2da5f8b294d (origin/ssh-agent) +Author: miloschwartz +Date: Tue Feb 17 14:41:18 2026 -0800 + + ensure client is running before ssh + +commit 560fa640b8d11091b2d6981e310562d0261460b6 +Author: miloschwartz +Date: Tue Feb 17 11:41:36 2026 -0800 + + check agent on select org and select account + +commit 8888f5e7582981626609d2354a182e53f51a882b +Author: miloschwartz +Date: Mon Feb 16 22:06:27 2026 -0800 + + add message polling + +commit 15438f25a367912332405d7aaa25d48b92a0d416 +Author: miloschwartz +Date: Mon Feb 16 21:35:55 2026 -0800 + + add force=true and custom port option + +commit debe69387e999b1927eff61fc5211e62743c4770 +Author: miloschwartz +Date: Mon Feb 16 20:55:52 2026 -0800 + + set force to true + +commit 54f88c499e214d295d084826a23bb6e718bc0184 +Author: miloschwartz +Date: Mon Feb 16 20:51:06 2026 -0800 + + clean up + +commit 2f7b69bdb4349386099d90e0c51916a1b95fa759 +Author: miloschwartz +Date: Mon Feb 16 20:36:58 2026 -0800 + + add auth-daemon + +commit 94dbb65c3c7507c969a77df43872381cfd0272db +Author: miloschwartz +Date: Mon Feb 16 16:40:14 2026 -0800 + + basic ssh working with org ca + +commit 6ad3d86b02423604d1a70eee19db9c4430314770 +Author: miloschwartz +Date: Mon Feb 16 15:17:00 2026 -0800 + + get user and hostname from api + +commit b0e3151a49d7d4cdfbb7f0504341fc6d3658dc28 +Author: miloschwartz +Date: Mon Feb 16 15:12:26 2026 -0800 + + support sign and output keys jit + +commit c5d1bc2dd983b6b7fbb48d0f640f1eb098f5931c +Author: miloschwartz +Date: Mon Feb 16 14:55:01 2026 -0800 + + add just in time key generation and signing + +commit 305c03601ae8cfa792b04254033cf4eef088b7d5 +Author: miloschwartz +Date: Mon Feb 16 14:47:24 2026 -0800 + + add priv/pub cert generation and signing endpoint + +commit 5a496397bc778c43266b757f5e719e0faa43545b +Author: miloschwartz +Date: Mon Feb 16 14:41:34 2026 -0800 + + add private key and certificate support + +commit 72a7110a627e74344d1ad2200a14ed3a27d77365 +Author: miloschwartz +Date: Mon Feb 16 14:01:54 2026 -0800 + + use native go-based ssh client + +commit d70b3dfe572b182210da6cd534815ecab76ca74e +Merge: edcb025 91fbc32 +Author: miloschwartz +Date: Mon Feb 16 12:29:39 2026 -0800 + + Merge branch 'main' into ssh-agent + +commit edcb025b09cab3f68806370a0025155d62b3b81c +Author: miloschwartz +Date: Mon Feb 16 12:29:30 2026 -0800 + + basic ssh client implementation + +commit b34f562358b5da6c12634d04bca9c6ede66f4c90 +Author: Owen +Date: Thu Feb 12 16:08:37 2026 -0800 + + Add install script + +commit 91fbc3235e6f5e74b4c12d395c70a2bf9c5de105 (tag: 0.3.3) +Author: Owen +Date: Thu Feb 12 15:56:52 2026 -0800 + + Update the version + +commit f4e44f0be1615ee8bf8d1756e46366032983b255 +Author: Owen +Date: Thu Feb 12 15:50:47 2026 -0800 + + Update version + +commit 05c99d96c9b10c7c4825d7d3de778368fe8536f6 +Author: Owen +Date: Thu Feb 12 15:47:53 2026 -0800 + + Fix the repo name + +commit da6919ae840e962593f3af63bc6749128bd9e970 +Author: Owen +Date: Thu Feb 12 15:17:44 2026 -0800 + + Fix non-constant format string in call + +commit 97a5408802dbe5127969059528943979751ad2fb +Author: Owen +Date: Thu Feb 12 14:58:40 2026 -0800 + + Update flake + +commit 0d95568e3c1c5a5d4543b89455ac6378151fd010 +Merge: a97201a c0d664f +Author: Owen Schwartz +Date: Thu Feb 12 14:53:12 2026 -0800 + + Merge pull request #26 from fosrl/dev + + CICD and Docker Build + +commit c0d664f71901d83ebade820b002d17e32fdce630 +Merge: 94f5fbc a97201a +Author: Owen +Date: Thu Feb 12 14:52:58 2026 -0800 + + Merge branch 'main' into dev + +commit 94f5fbc92c658525aef09fc9fc2043f39551ab43 +Merge: a58863c 6d715a9 +Author: Owen +Date: Thu Feb 12 14:51:33 2026 -0800 + + Merge branch 'dev' of github.com:fosrl/cli into dev + +commit a58863c8b17149fb81ca81b7c376ceebcb391646 +Author: Owen +Date: Thu Feb 12 14:48:09 2026 -0800 + + Add cicd and entrypoint and docker + +commit a97201acfaacfe7dfffa48a9dde04da8d862704a (tag: 0.3.2) +Author: miloschwartz +Date: Mon Feb 9 14:52:00 2026 -0800 + + bump version + +commit 01698a9ba92da441c20e74b72e00c5d755ce2b22 +Author: miloschwartz +Date: Mon Feb 9 14:51:36 2026 -0800 + + bump newt and olm + +commit b10170c457e0163c6caec85e12ae4c31c1e1b210 +Merge: 49543f6 6d715a9 +Author: Milo Schwartz +Date: Mon Feb 9 14:50:58 2026 -0800 + + Merge pull request #22 from fosrl/dev + + 0.3.2 + +commit 6d715a97367268e29265a2ed4bc28f5e4ad0e73d +Author: miloschwartz +Date: Mon Feb 9 14:41:23 2026 -0800 + + dont fingerprint machine clients + +commit 49543f6286ec69f2e8c7eb613d0fe8d5e102ea86 (tag: 0.3.1) +Merge: 2cf33a7 c212d15 +Author: Milo Schwartz +Date: Thu Jan 29 16:24:12 2026 -0800 + + Merge pull request #20 from fosrl/dev + + Dev + +commit c212d155ad5ba1c8a4497f8c22dc5323f6886641 +Author: miloschwartz +Date: Thu Jan 29 16:22:58 2026 -0800 + + remove log + +commit 9bb3f9cb1336bf3814d78ce4227acad4cf86b063 +Author: miloschwartz +Date: Thu Jan 29 15:57:43 2026 -0800 + + uncomment go.mod replace + +commit 2c4a36b0f12c9170c7e417788a2d7c1033678bc5 +Author: miloschwartz +Date: Thu Jan 29 15:57:15 2026 -0800 + + add script and bump version + +commit f9406c646907a9cd5f4573b1e1d1f70c67798ee4 +Author: miloschwartz +Date: Tue Jan 27 17:17:50 2026 -0800 + + validate dns inputs + +commit e77670635ea42e3f32940f8813d3fff75e7de7d1 +Author: miloschwartz +Date: Tue Jan 27 17:12:10 2026 -0800 + + make docs + +commit 5c544e1bea3b8da38ad9d8a7d935440a56522711 +Author: miloschwartz +Date: Tue Jan 27 16:50:35 2026 -0800 + + add improved description for settings + +commit 2cf33a784b1ddebf2959252cd7e28439e4aacd68 (tag: 0.3.0) +Merge: 106b70c 66498c3 +Author: Milo Schwartz +Date: Fri Jan 23 10:51:15 2026 -0800 + + Merge pull request #18 from fosrl/dev + + 0.3.0 + +commit 66498c3d95ed68f544b72d31df59473a18e1e051 +Author: miloschwartz +Date: Fri Jan 23 10:50:34 2026 -0800 + + make docs + +commit da494773aa7b1ba2b07bf35b3bb42d2aaefe35f6 +Merge: 8913bae 106b70c +Author: miloschwartz +Date: Fri Jan 23 10:50:12 2026 -0800 + + Merge branch 'main' into dev + +commit 8913bae50f3b8b85f9ca366db91a7505f2661248 +Author: miloschwartz +Date: Fri Jan 23 10:48:54 2026 -0800 + + bump version + +commit ccc7e90c9f4ac344503211504a24230b44f9b50e +Author: miloschwartz +Date: Fri Jan 23 10:48:21 2026 -0800 + + bump olm and newt + +commit 6a4911a2d5dfb7200a245146e2eab3e8422bb193 +Author: Owen +Date: Thu Jan 22 15:16:15 2026 -0800 + + Handle blueprint response correctly + +commit e914f70c45c0d9e74710a90ce3fc2864806a77ac +Author: miloschwartz +Date: Thu Jan 22 12:56:18 2026 -0800 + + move apply to apply blueprint, add file flag, and update docs + +commit ac247d4577cf65fc55379b5ccc4c6d4c6d661067 +Author: Owen +Date: Wed Jan 21 17:39:15 2026 -0800 + + Keep the olm id when logging out again + +commit 95e4ce90c91d3889be48eaa9a4ad71c43ae99ba0 +Author: miloschwartz +Date: Wed Jan 21 15:37:21 2026 -0800 + + fix auto update check and biometrics check + +commit dacdc3cebcdb471835ba9beb8930ec993c34f685 +Author: Owen +Date: Wed Jan 21 15:00:40 2026 -0800 + + Allow running up command as sudo + +commit 57a50e8476a4277635261e76b9e867f10d0b63bb +Author: Owen +Date: Wed Jan 21 14:55:34 2026 -0800 + + Dont print the error + +commit c4cb7fa1eeb7a354a206e1b417a4ee6fadf77ae3 +Author: Owen +Date: Wed Jan 21 14:05:36 2026 -0800 + + Handle the fingerprint correctly + +commit 64182907db8cc1353363c34e5c472ed167cdc1e6 +Author: Owen +Date: Wed Jan 21 11:53:21 2026 -0800 + + Implement apply blueprint + +commit ca728f979cc0d6fe34dde02235ab8b99d3ab6cc6 +Author: miloschwartz +Date: Wed Jan 21 14:57:14 2026 -0800 + + fix darwin posture checks and add test script + +commit 106b70c5e0d4e0a8ceb16009ae65c818e5f5f1b7 +Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> +Date: Wed Jan 7 20:07:13 2026 +0000 + + Bump actions/checkout from 5.0.0 to 6.0.1 + + Bumps [actions/checkout](https://github.com/actions/checkout) from 5.0.0 to 6.0.1. + - [Release notes](https://github.com/actions/checkout/releases) + - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) + - [Commits](https://github.com/actions/checkout/compare/08c6903cd8c0fde910a37f88322edcfb5dd907a8...8e8c483db84b4bee98b60c0593521ed34d9990e8) + + --- + updated-dependencies: + - dependency-name: actions/checkout + dependency-version: 6.0.1 + dependency-type: direct:production + update-type: version-update:semver-major + ... + + Signed-off-by: dependabot[bot] + +commit 405c42b3b2a07b37c5037a1cd6ec6a122707ea39 +Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> +Date: Wed Jan 7 20:07:26 2026 +0000 + + Bump the patch-updates group with 2 updates + + Bumps the patch-updates group with 2 updates: [github.com/charmbracelet/bubbletea](https://github.com/charmbracelet/bubbletea) and [github.com/spf13/cobra](https://github.com/spf13/cobra). + + + Updates `github.com/charmbracelet/bubbletea` from 1.3.6 to 1.3.10 + - [Release notes](https://github.com/charmbracelet/bubbletea/releases) + - [Commits](https://github.com/charmbracelet/bubbletea/compare/v1.3.6...v1.3.10) + + Updates `github.com/spf13/cobra` from 1.10.1 to 1.10.2 + - [Release notes](https://github.com/spf13/cobra/releases) + - [Commits](https://github.com/spf13/cobra/compare/v1.10.1...v1.10.2) + + --- + updated-dependencies: + - dependency-name: github.com/charmbracelet/bubbletea + dependency-version: 1.3.10 + dependency-type: direct:production + update-type: version-update:semver-patch + dependency-group: patch-updates + - dependency-name: github.com/spf13/cobra + dependency-version: 1.10.2 + dependency-type: direct:production + update-type: version-update:semver-patch + dependency-group: patch-updates + ... + + Signed-off-by: dependabot[bot] + +commit 09d7e181a69fa6f339db2f3d8bc1f07fed819940 +Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> +Date: Wed Jan 14 20:37:26 2026 +0000 + + Bump actions/setup-go from 6.1.0 to 6.2.0 + + Bumps [actions/setup-go](https://github.com/actions/setup-go) from 6.1.0 to 6.2.0. + - [Release notes](https://github.com/actions/setup-go/releases) + - [Commits](https://github.com/actions/setup-go/compare/4dc6199c7b1a012772edbd06daecab0f50c9053c...7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5) + + --- + updated-dependencies: + - dependency-name: actions/setup-go + dependency-version: 6.2.0 + dependency-type: direct:production + update-type: version-update:semver-minor + ... + + Signed-off-by: dependabot[bot] + +commit f7132bb92538d09ca7e909801ec30454654e14cd +Author: Varun Narravula +Date: Wed Jan 21 09:37:04 2026 -0800 + + feat(blueprint): add apply blueprint cmd + +commit f4de2d171247d5dd05eec46a65a3c7f352b3cf35 +Author: miloschwartz +Date: Mon Jan 19 18:22:00 2026 -0800 + + add bubble up error, server health check, server info check, and better account handling + +commit a4f30269e9801d8db69c32f54082b493723c60fe +Merge: 0f1d4ab 69df31a +Author: miloschwartz +Date: Mon Jan 19 18:19:01 2026 -0800 + + Merge branch 'add-fingerprint-and-posture-check-info' into dev + +commit 0f1d4abf73e61b68e8b93bdbce2062e6ffc047a6 +Merge: 882ea7a 9c11413 +Author: Varun Narravula +Date: Mon Jan 19 15:33:16 2026 -0800 + + Merge pull request #15 from water-sucks/add-fingerprint-and-posture-check-info + + feat: add fingerprint and posture check info polling + +commit 9c11413102e3c5db3477fc5e9bd0948f56b29033 +Author: Varun Narravula +Date: Fri Jan 16 00:50:19 2026 -0800 + + fix(fingerprint): cache platform fingerprint hash when running as root + +commit 5aaae682223b52e050b4f1f9f324d91a3444ba06 +Author: Varun Narravula +Date: Fri Jan 16 00:41:56 2026 -0800 + + fix(fingerprint): resolve import cycles, standardize output + +commit e3f0d0565db7bd66de86f32c09c8af0b5e085b75 +Author: Varun Narravula +Date: Wed Jan 14 13:15:00 2026 -0800 + + feat(login): recover olm device when possible + +commit cddb7065a06b4edb75498e16f22d9361c355eeff +Author: Varun Narravula +Date: Wed Jan 14 12:28:59 2026 -0800 + + revert: use previous account save/delete mechanism with login/logout + + Reverts commits: + + - b6060a72bdcdc16e532d60013e12ee74c5c5a3a0 + - 1c35fdb54bf5dc7a8d475236e11210abad7b278c + +commit 8ea474d2d36e4cb24287e53be74bcccaba9dc3dd +Author: Varun Narravula +Date: Thu Jan 15 23:29:37 2026 -0800 + + feat: add prettier device names for linux and macos devices + +commit 53014efb8f34630735a2e9aa708ccd36ff94c8cf +Author: Varun Narravula +Date: Thu Jan 15 12:42:33 2026 -0800 + + chore(deps): pin olm version to remote + +commit 44eb27001b1eecf34cba776f5352da7b2669e01a +Author: Varun Narravula +Date: Wed Jan 14 16:34:33 2026 -0800 + + feat(fingerprint): add macos platform fingerprint hash + +commit a03e901ed55d0b536e7fb0e52f929b2809d537eb +Author: Varun Narravula +Date: Wed Jan 14 16:17:39 2026 -0800 + + feat(fingerprint): add windows platform fingerprint hash + +commit 394dc3eb591e47f4a0dc53ccfdd13af2eaeb4942 +Author: Varun Narravula +Date: Wed Jan 14 15:43:35 2026 -0800 + + feat(fingerprint): add linux platform fingerprint hash + +commit 753c2400c14e971da1ba1d1add4a868c44c8f1a7 +Author: Varun Narravula +Date: Wed Jan 14 15:24:13 2026 -0800 + + fix(fingerprint): use sudo user when possible on linux + +commit 858e1f6a07200a7cb211a415dc1c6f4720da3624 +Author: Varun Narravula +Date: Tue Jan 13 11:27:14 2026 -0800 + + feat(fingerprint): start fingerprinting routine when client comes up + +commit 3853fd2225f3e0772bcb483fc6b50a1200509344 +Author: Varun Narravula +Date: Tue Jan 13 09:40:43 2026 -0800 + + chore(deps): use new olm version, update package references + +commit b95dec5af0423aee644461f4c327cbe054b39e25 +Author: Varun Narravula +Date: Tue Jan 13 09:26:40 2026 -0800 + + feat(fingerprint): add windows-specific fingerprint/posture query impls + +commit 3a804da9d463de784c0eb5eb2634a1879fa58edf +Author: Varun Narravula +Date: Mon Jan 12 02:49:05 2026 -0800 + + feat(fingerprint): add linux-specific fingerprint/posture query impls + +commit ac14da7cf74c17e5d331fda39f56cc16f881cde7 +Author: Varun Narravula +Date: Mon Jan 12 02:07:56 2026 -0800 + + feat(fingerprint): add macos-specific fingerprint/posture query impls + +commit 7fa77375f67d1ee5b4325e0c791dedad82eeedd8 +Author: Varun Narravula +Date: Mon Jan 12 01:05:09 2026 -0800 + + feat(fingerprint): init internal/fingerprint package structure + +commit 69df31aee264888b84847c434f6a2df11a8916cf +Author: miloschwartz +Date: Fri Jan 16 16:20:00 2026 -0800 + + add bubble up error from olm + +commit 7ec8e1fc04c5950e901c5df7f220fd447f12fb39 +Author: Varun Narravula +Date: Fri Jan 16 00:50:19 2026 -0800 + + fix(fingerprint): cache platform fingerprint hash when running as root + +commit 4e5f7088bc292753564fc9f0f95b41e1e01317b3 +Author: Varun Narravula +Date: Fri Jan 16 00:41:56 2026 -0800 + + fix(fingerprint): resolve import cycles, standardize output + +commit 0d5ce5af2a8aa027644b198345ee99e6b0d87f5b +Author: Varun Narravula +Date: Wed Jan 14 13:15:00 2026 -0800 + + feat(login): recover olm device when possible + +commit a2f3ac0988de199634a0453d638c0c294a4a5650 +Author: Varun Narravula +Date: Wed Jan 14 12:28:59 2026 -0800 + + revert: use previous account save/delete mechanism with login/logout + + Reverts commits: + + - b6060a72bdcdc16e532d60013e12ee74c5c5a3a0 + - 1c35fdb54bf5dc7a8d475236e11210abad7b278c + +commit a50674e317addf60e7f476bae7846ef77c4fb146 +Author: Varun Narravula +Date: Thu Jan 15 23:29:37 2026 -0800 + + feat: add prettier device names for linux and macos devices + +commit 88f476e8706f2dd16f2ddfa19b0bc5f65af85acb +Author: Varun Narravula +Date: Thu Jan 15 12:42:33 2026 -0800 + + chore(deps): pin olm version to remote + +commit 25714cf3e3e22035d03c1af82441c80c441d9171 +Author: Varun Narravula +Date: Wed Jan 14 16:34:33 2026 -0800 + + feat(fingerprint): add macos platform fingerprint hash + +commit 3d806f579318459f1a56213e641307148a4faae5 +Author: Varun Narravula +Date: Wed Jan 14 16:17:39 2026 -0800 + + feat(fingerprint): add windows platform fingerprint hash + +commit fc58b8ef930102732ff37dfb5b40939c0b366c68 +Author: Varun Narravula +Date: Wed Jan 14 15:43:35 2026 -0800 + + feat(fingerprint): add linux platform fingerprint hash + +commit 8afeb9ab084b5f8c76b83d08794be6c7b3541d5d +Author: Varun Narravula +Date: Wed Jan 14 15:24:13 2026 -0800 + + fix(fingerprint): use sudo user when possible on linux + +commit 6001139113f652d9b46a472e9e06a259fb316cb2 +Author: Varun Narravula +Date: Tue Jan 13 11:27:14 2026 -0800 + + feat(fingerprint): start fingerprinting routine when client comes up + +commit f536e6d0e064cbbfbee57d0dea17cb5c5c89f4a6 +Author: Varun Narravula +Date: Tue Jan 13 09:40:43 2026 -0800 + + chore(deps): use new olm version, update package references + +commit 43082648643d103bbd41e653a71405c395f0f22c +Author: Varun Narravula +Date: Tue Jan 13 09:26:40 2026 -0800 + + feat(fingerprint): add windows-specific fingerprint/posture query impls + +commit d565650878f257cb90c31ea118b245069fe940ca +Author: Varun Narravula +Date: Mon Jan 12 02:49:05 2026 -0800 + + feat(fingerprint): add linux-specific fingerprint/posture query impls + +commit ffa354ee3634fa3b58474164459a870c0eb71eb5 +Author: Varun Narravula +Date: Mon Jan 12 02:07:56 2026 -0800 + + feat(fingerprint): add macos-specific fingerprint/posture query impls + +commit 9847022cab07b21da769e61f3f9e1c74a9b90465 +Author: Varun Narravula +Date: Mon Jan 12 01:05:09 2026 -0800 + + feat(fingerprint): init internal/fingerprint package structure + +commit 882ea7a8b5b91bfb454bd456d248e00bcb4cfbe9 +Author: miloschwartz +Date: Tue Jan 13 15:08:01 2026 -0800 + + add check if device is blocked + +commit 822b17ad08c239338a030f7af191c30010d6352c +Author: miloschwartz +Date: Mon Jan 12 21:27:20 2026 -0800 + + set user agent + +commit d6f8870faac0a74748f3cc143014b1f1acb1a549 +Author: miloschwartz +Date: Fri Jan 9 15:09:36 2026 -0800 + + fix perist org switch + +commit 1b8f2a93ed52372bd1346e7defe63823f6cc9f6f +Author: miloschwartz +Date: Fri Jan 9 14:48:17 2026 -0800 + + switch to other account on logout + +commit de7976254eef7a1f719ae5f73f94b8b84f48e198 +Author: Lokowitz +Date: Tue Jan 6 08:30:13 2026 +0000 + + cleanup dependa config + +commit 7e85c71e4f55bc914ba300c2d41d7012669784e5 +Author: Lokowitz +Date: Tue Jan 6 08:27:19 2026 +0000 + + add empty line at the end + +commit 11be2d66b98be6e10c8626a81c07e47103301d50 +Author: Lokowitz +Date: Tue Jan 6 08:09:32 2026 +0000 + + add dependabot config + +commit 0bae2078cfe12f1c656dece96ab730ce9a5f47d4 +Author: Lokowitz +Date: Tue Jan 6 08:21:57 2026 +0000 + + fix make files + +commit 176e6d8d38fcefc2b5444c93e4a8f297844dada6 +Author: Lokowitz +Date: Tue Jan 6 08:19:47 2026 +0000 + + add test + +commit 064f4940025cebcb149e9f0090f2b22526b15081 +Author: miloschwartz +Date: Mon Jan 5 21:33:59 2026 -0500 + + increase newt version + +commit 5d905333f7124bbbbceea35e30d73a8dfca5265b (tag: 0.2.1) +Author: miloschwartz +Date: Mon Jan 5 21:29:16 2026 -0500 + + bump version + +commit 6f017b3c8bc41e3105979981baed893eec1fd61f +Author: miloschwartz +Date: Mon Jan 5 21:27:46 2026 -0500 + + hide log statement if olm exists + +commit f543b1f18207b46064571198d11c14f03ed72459 +Merge: 60c60bc b6060a7 +Author: Milo Schwartz +Date: Mon Jan 5 18:27:07 2026 -0800 + + Merge pull request #9 from water-sucks/preserve-olm-creds-on-logout + + fix(olm): preserve olm credentials on logout + +commit b6060a72bdcdc16e532d60013e12ee74c5c5a3a0 +Author: Varun Narravula +Date: Mon Jan 5 15:24:38 2026 -0800 + + fix(select): only use available (logged-in) accounts for selection + +commit 1c35fdb54bf5dc7a8d475236e11210abad7b278c +Author: Varun Narravula +Date: Mon Jan 5 15:16:09 2026 -0800 + + feat(accounts): mark account as deactivated upon logout, reuse in login + +commit f251229d0eed13b7863bac74c1a88dd45d94d3c5 +Author: Varun Narravula +Date: Tue Dec 23 15:00:11 2025 -0800 + + chore(deps): pin olm and newt to upstream tags + +commit 60c60bc862f320ef8149130690bf7de97fa189d5 (tag: 0.2.0) +Author: miloschwartz +Date: Mon Dec 22 17:06:14 2025 -0500 + + bump version + +commit f37462aeae30d1ea8375ffea86accc4336b14319 +Merge: 0346b6b 91716d5 +Author: Milo Schwartz +Date: Mon Dec 22 11:13:34 2025 -0800 + + Merge pull request #6 from fosrl/dev + + 0.2.0 + +commit 91716d5356b9a65022511d69c1aa4e71083f1be2 +Merge: 7fe8687 e835b59 +Author: Milo Schwartz +Date: Mon Dec 22 11:12:12 2025 -0800 + + Merge pull request #4 from water-sucks/add-basic-completion + + feat(select): add basic completion flags + +commit 7fe8687c75d697a68f3cdeca668d5c15221441d8 +Author: miloschwartz +Date: Mon Dec 22 14:06:42 2025 -0500 + + update docs + +commit e835b594ac5fea622d7b3ddd6c2d7ac7539c2cc3 +Author: Varun Narravula +Date: Fri Dec 19 12:31:25 2025 -0800 + + feat(select): add completion funcs for account email/host flags + +commit 825a87c80b827bb99198b4c53da636fcbfecbaf0 +Author: Varun Narravula +Date: Fri Dec 19 12:19:55 2025 -0800 + + feat(select): add completion func for org flag + +commit aec8830e871c162563db217be03653a8d03619ee +Merge: 129b782 470cdc2 +Author: Milo Schwartz +Date: Fri Dec 19 19:25:09 2025 -0800 + + Merge pull request #3 from water-sucks/refactor-cobra-struture + + refactor: use functions to init cobra commands, flatten cmd structures + +commit 470cdc2dc8c122f5bc7b4d116683605f2af4b593 +Author: Varun Narravula +Date: Fri Dec 19 12:10:33 2025 -0800 + + docs: re-generate cobra markdown docs + +commit 431fff10ce3efaafde87020535291401954a966c +Author: Varun Narravula +Date: Fri Dec 19 01:45:34 2025 -0800 + + feat: normalize exit success/error codes, only call os.Exit() in Run() + + This normalizes error handling to always propagate up until the Run() + function, where this will exit with the 1 status code if an error + is detected. + + Before this commit, some unsuccessful attempts and error states resulted + in an exit code of 0, which can throw off shell scripts. + +commit e66cb374b64fafbace8857144f7f0f0230a32a2b +Author: Varun Narravula +Date: Fri Dec 19 01:14:17 2025 -0800 + + refactor(client-up): avoid globals, completely redo cobra flag logic + + Instead of using global variables for Cobra flag values, and separate + defaults and clunky getter functions everywhere, set the defaults inside + Cobra's flag set itself. + + This makes handling of values radically simpler. + + Additionally, this also adds more stringent flag validation before even + running the program, and errors are handled more gracefully at the end + with a single os.Exit() invocation. + +commit 58640ec01dde1bbd06868ad78218841507f9f12e +Author: Varun Narravula +Date: Thu Dec 18 22:35:54 2025 -0800 + + refactor: move client subcommands into separate packages for consistency + +commit 89c8d027b45e2c04e7f9149cd283bab75df7a930 +Author: Varun Narravula +Date: Thu Dec 18 22:30:37 2025 -0800 + + refactor: use functions to init cobra functions, flatten structures + +commit 129b7823ae58202979857ee9014256d241784af4 +Author: Owen +Date: Fri Dec 19 10:12:03 2025 -0500 + + Add tunnel dns flag option + +commit ddfcb6588436d20f27dc1e4191ce489db871eab8 +Merge: 0346b6b 9225bba +Author: Milo Schwartz +Date: Thu Dec 18 19:16:37 2025 -0800 + + Merge pull request #1 from water-sucks/multi-account-login + + feat(auth): support multiple accounts + +commit 9225bba39d7c29619ebe9c88c1d30722f223b33d +Author: miloschwartz +Date: Thu Dec 18 22:15:57 2025 -0500 + + remove olm from log message + +commit b4c2468a911f2d296be75a5b42f7e93484e991ce +Author: Varun Narravula +Date: Wed Dec 17 23:55:32 2025 -0800 + + feat(config): add config.json and validate settings once before startup + +commit 2e50c76d5e72a14f4e86d331f61da97a2c17ec32 +Author: Varun Narravula +Date: Wed Dec 17 21:47:04 2025 -0800 + + refactor: move logger into separate package + +commit fbcee826786c73041ad5c5aa2ad87a9ce710cc86 +Author: Varun Narravula +Date: Wed Dec 17 10:29:12 2025 -0800 + + fix(up/down): pass context explicitly to prevent panics + +commit 544db88db62a4bf7c1a3867a55381adf85ae152d +Author: Varun Narravula +Date: Wed Dec 17 10:15:11 2025 -0800 + + docs: re-generate CLI docs + +commit 5332b142c49a1d5d28141f33d4250546f9a53adf +Author: Varun Narravula +Date: Wed Dec 17 10:13:16 2025 -0800 + + fix(config): exit early if failed to parse config + +commit 99ec424e245de3a1f9a37d0c3c4c1350f78ca4ac +Author: Varun Narravula +Date: Wed Dec 17 10:09:57 2025 -0800 + + feat(select): select between multiple accounts + +commit 9ff622d021c18d375aa4374bbefc41320893d7d9 +Author: Varun Narravula +Date: Wed Dec 17 09:49:31 2025 -0800 + + refactor(select): move org subcommand into separate package + +commit 7f42cc0da011fdf1e543fdaa4b1410f0064f4459 +Author: Varun Narravula +Date: Tue Dec 16 20:24:42 2025 -0800 + + chore: remove unused internal/secrets package + +commit 35bf24b6c722d793519ed1d0613795bc379faacf +Author: Varun Narravula +Date: Tue Dec 16 20:13:10 2025 -0800 + + feat(client-up): use active account mechanism + +commit e3a8f95a50f9507a9a72a2664d174b11cc0e9541 +Author: Varun Narravula +Date: Tue Dec 16 20:11:37 2025 -0800 + + refactor(select): use active account for selecting org + +commit cdb0c34c44a0c58aa30219cd6fc00708ff1bf500 +Author: Varun Narravula +Date: Tue Dec 16 20:09:15 2025 -0800 + + refactor: rename internal/accounts -> internal/config + +commit b9d502a6b844adf07de5bad8d7407fc3fe29290b +Author: Varun Narravula +Date: Tue Dec 16 16:33:17 2025 -0800 + + refactor: use context to pass account store and API client + +commit 94cb1b3c0ed9e5c6f9a258409691e284c328dd0f +Author: Varun Narravula +Date: Tue Dec 16 15:28:02 2025 -0800 + + feat(logout): implement multiple account support + +commit 5d9d7705f7984dc5799607a299a4ba52502ed84f +Author: Varun Narravula +Date: Tue Dec 16 14:09:38 2025 -0800 + + feat(login): use new account store for storing credentials + +commit bce5dfa0d4048f89032e46f999fa2ac6f17011c0 +Author: Varun Narravula +Date: Tue Dec 16 11:41:57 2025 -0800 + + feat(accounts): init account store, change config location + +commit 2e4aed15d713dbe554b75e4e87bdf1f8882338e0 +Author: Varun Narravula +Date: Mon Dec 15 20:30:59 2025 -0800 + + feat(nix): add simple flake devshell + +commit 0346b6b8875e4b117b2864dc8c7b80a32c5cb154 +Author: miloschwartz +Date: Tue Dec 16 22:44:53 2025 -0500 + + update docs + +commit cd143c34499a5accc62665d10bcc83ceff349143 +Author: miloschwartz +Date: Tue Dec 16 22:38:52 2025 -0500 + + bump version + +commit d02bc1445c43718b00024bc620af78d2df9625d0 (tag: 0.1.2) +Merge: 520926d e347884 +Author: Milo Schwartz +Date: Tue Dec 16 19:33:25 2025 -0800 + + Merge pull request #2 from fosrl/dev + + 0.1.2 + +commit e34788412dbff11d5fa81ba084886aaf2cb754d1 +Author: miloschwartz +Date: Tue Dec 16 22:32:09 2025 -0500 + + rename netstack dns flag + +commit 226400379596492fcb9ce4c83c6af968fe7362c7 +Author: miloschwartz +Date: Tue Dec 16 21:29:01 2025 -0500 + + fix max session length data type and override dns parsing + +commit 520926d5de12bc5ca04153b81d958f6585428209 (tag: 0.1.1) +Author: miloschwartz +Date: Thu Dec 11 17:19:37 2025 -0500 + + bump version + +commit e6366c29c014b80c3280a79376fd1794021c119d +Author: Milo Schwartz +Date: Wed Dec 10 17:46:45 2025 -0500 + + Update README.md + +commit 568c7e26701b98f4f405b36a0e533e371a97599e (tag: 0.1.0) +Author: miloschwartz +Date: Mon Dec 8 12:19:07 2025 -0500 + + set version and add builds + +commit 144dffd191697fa69eab690acbd307a247e27cdc +Author: miloschwartz +Date: Mon Dec 8 10:24:29 2025 -0500 + + use static.pangolin.net + +commit e0c9262282a89865cce132a0ff656f8062006681 +Author: miloschwartz +Date: Sun Dec 7 23:24:15 2025 -0500 + + dont pull org from config if machine client + +commit b500f7a7a72a90e8996cbc36650343f2f3d8b551 +Author: miloschwartz +Date: Sun Dec 7 23:09:29 2025 -0500 + + update docs + +commit b44d34704ae722de697591bcda8e61535feee223 +Author: miloschwartz +Date: Sun Dec 7 21:38:41 2025 -0500 + + add site name to status + +commit 77b3260f17fe694f764d70afad681c62601a25d6 +Author: miloschwartz +Date: Sun Dec 7 16:29:40 2025 -0500 + + use official repo + +commit 36d8f00a43e4695e5aa99af8856f001895b8402c +Author: miloschwartz +Date: Sun Dec 7 14:25:49 2025 -0500 + + dont allow org flag if machine client + +commit 8122190fe015a296b7740b43473692634265739d +Author: Owen +Date: Sun Dec 7 12:06:29 2025 -0500 + + Update go + +commit 2c8d763c6d8542833e2fd0bea411158bca7c182b +Author: miloschwartz +Date: Fri Dec 5 15:16:05 2025 -0500 + + shorten org flag + +commit 426de470c6d0b3c5525bbf5b320e9d2fb38b7cbe +Author: miloschwartz +Date: Fri Dec 5 14:41:11 2025 -0500 + + update docs + +commit 7c11f5012e8c53cd73080ef8ddaba73ad7194771 +Author: miloschwartz +Date: Fri Dec 5 14:40:45 2025 -0500 + + remove global org id flag and add to commands + +commit b68c6a4633b4e7311823f5ba7df5c1904e7b243d +Author: Milo Schwartz +Date: Fri Dec 5 12:25:51 2025 -0500 + + Update README.md + +commit b7495a138c69b48ff91aa19f25a735a56e796e88 +Author: miloschwartz +Date: Fri Dec 5 12:12:23 2025 -0500 + + make interface lowercase + +commit dc93187b70eafd7113cae731b99759fe1c57a039 +Author: miloschwartz +Date: Fri Dec 5 11:26:45 2025 -0500 + + remove keyring + +commit c18a3b856d445e6ae8630510b92115db14bc2fc0 +Author: miloschwartz +Date: Fri Dec 5 10:36:44 2025 -0500 + + change command desc + +commit 1fff7bbc26e5cc71b3ee342f9a16b11afed71627 +Author: miloschwartz +Date: Thu Dec 4 22:57:27 2025 -0500 + + change to status output, agent + +commit 76bbbf6699c94ba64188a86bd97a277fd330e10a +Author: miloschwartz +Date: Wed Dec 3 13:54:41 2025 -0500 + + compare agent + +commit 6d1a55ce5c707d011ca8d9162a498584e42b5b4a +Author: miloschwartz +Date: Wed Dec 3 13:49:59 2025 -0500 + + remove some required flags + +commit 7b51822a92911348c342ab21eb45ea5787c0aee6 +Author: miloschwartz +Date: Wed Dec 3 12:13:15 2025 -0500 + + pass version and agent to olm + +commit 9efe68595b1869b619c0b25665d054ff60d8e9df +Author: miloschwartz +Date: Wed Dec 3 11:53:20 2025 -0500 + + support enable-api=false + +commit c59dc1289631a3ea564192b53bc57438b208d22d +Author: miloschwartz +Date: Wed Dec 3 11:44:34 2025 -0500 + + update docs + +commit 9f513eb34f27ba2a3f9e7cb16e7ed172ef35f0d9 +Author: miloschwartz +Date: Wed Dec 3 11:43:51 2025 -0500 + + ensure background check completes, and add disable option + +commit 3918cb60eadb09a6514fd40c87acc2eaf8007be7 +Author: miloschwartz +Date: Wed Dec 3 11:25:20 2025 -0500 + + add update check + +commit 755fc6652518d48c737d2b2ce3e67a4284710f28 +Author: miloschwartz +Date: Wed Dec 3 10:53:09 2025 -0500 + + remove site from desc + +commit be0a5c6655f517a32bbcb0a4c451a94c7271d1c5 +Author: miloschwartz +Date: Wed Dec 3 10:52:37 2025 -0500 + + add version + +commit 1c94045a92bb6c5f55d2aa7fe26c2f99abe78c47 +Author: miloschwartz +Date: Wed Dec 3 10:49:41 2025 -0500 + + add auth status + +commit bc6ff7fe90f7f6ab33642a5d036c3e473cc0423c +Author: Milo Schwartz +Date: Wed Dec 3 10:20:08 2025 -0500 + + Update README.md + +commit 0d2d31290ca785e8ba858559d317c6f2931c1fab +Author: miloschwartz +Date: Wed Dec 3 10:15:54 2025 -0500 + + add github files + +commit d24a9238f80aa42782bb262bf1485b50e3bf1a86 +Author: miloschwartz +Date: Wed Dec 3 10:12:22 2025 -0500 + + clean up + +commit 1ea0ee7bb9dbf29d10613233dd5344690341efe9 +Author: miloschwartz +Date: Tue Dec 2 23:09:44 2025 -0500 + + add auth checks, refactor, make exit work, and more + +commit e7a19bd7cea3ee78f96e9a2dcf308475820fd150 +Author: miloschwartz +Date: Tue Dec 2 20:00:17 2025 -0500 + + allow boolean flags to be false + +commit 6331bcc8dc9ce5a622ab2bd59bb3ea09fd9ba180 +Author: miloschwartz +Date: Tue Dec 2 17:46:28 2025 -0500 + + self terminate olm + +commit b2b431e17ce989b674fcfef8326017250c4a3be9 +Author: miloschwartz +Date: Tue Dec 2 17:33:41 2025 -0500 + + fetch secrets using user + +commit b2bbc52b506ad597aee782ea17874d5b24e8e612 +Author: miloschwartz +Date: Tue Dec 2 14:43:43 2025 -0500 + + fix login to use new method + +commit 0d679f2e7131e457f74e6ffa115c8b0505eda552 +Author: miloschwartz +Date: Mon Nov 10 20:39:10 2025 -0500 + + support --orgId in select org + +commit 7468c6394ceb9a253a5e3f4e3b5ad3f2f81048d1 +Author: miloschwartz +Date: Mon Nov 10 16:44:24 2025 -0500 + + support passing hostname and login method to login cmd + +commit 0b1d88b70786cc4df9d8abb0ab56ad20d80999ff +Author: miloschwartz +Date: Sat Nov 8 20:14:44 2025 -0800 + + fetch user token in parent process + +commit 00f570cd5e8660c3779586a21f8d05b31594b6cd +Author: miloschwartz +Date: Sat Nov 8 17:51:35 2025 -0800 + + support idp and create device on login + +commit a849052a60d7c95dd800fc96799f5af156445523 +Author: miloschwartz +Date: Fri Nov 7 16:51:28 2025 -0800 + + pass user session in olm config + +commit faac0b9f58f097ded595b03df2d9aa3c46846163 +Author: miloschwartz +Date: Thu Nov 6 20:23:25 2025 -0800 + + remove random number + +commit 4e57f089b399d42ae6173304577124cf450dce6e +Author: miloschwartz +Date: Thu Nov 6 20:12:19 2025 -0800 + + update create olm route + +commit bef7ad0c84fbf30d5849149d1ac3ac8df1599f0d +Author: miloschwartz +Date: Tue Nov 4 13:52:31 2025 -0800 + + clean up logs and fix sudo + +commit 5227523bb8402ca2d22743bff752f380a864df92 +Author: miloschwartz +Date: Tue Nov 4 12:35:34 2025 -0800 + + subprocess with sudo + +commit 1cefcef9f25303d745d712d63866261f6b952efd +Author: miloschwartz +Date: Mon Nov 3 21:22:31 2025 -0800 + + org switch working + +commit c9e8bc4d7283ca7899dec8d37079d777b9e60973 +Author: miloschwartz +Date: Mon Nov 3 16:34:45 2025 -0800 + + add docs + +commit db1e6187d644313102e7dfd53ae083975c83547a +Author: miloschwartz +Date: Mon Nov 3 16:22:51 2025 -0800 + + add up, down, logs, status + +commit 28051a0db6b175aef5b0331063d84287a6a3d9f0 +Author: miloschwartz +Date: Mon Nov 3 11:00:02 2025 -0800 + + add device web auth flow + +commit 77be745473dd2af25b17d7e8811f515dc2683867 +Author: miloschwartz +Date: Sat Nov 1 19:01:41 2025 -0700 + + add login flow with credentials, select org, logout + +commit b78558cc1f7a9e93346f6a7cbf3299e56f1593e5 +Author: miloschwartz +Date: Sat Nov 1 11:41:20 2025 -0700 + + add basic scafolding + +commit 5e19c030226739a7c9676bd32a1359790cf18e1f +Author: miloschwartz +Date: Fri Oct 31 17:37:09 2025 -0700 + + add gitignore + +commit 033166af3c8dee50bd9b6d45ff40260fae2968c2 +Author: miloschwartz +Date: Fri Oct 31 17:35:39 2025 -0700 + + first commit From 4c664bc5a0240f6db811e164db8970258d1f99b2 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 29 Sep 2026 11:58:35 -0400 Subject: [PATCH 12/14] Guard the exit node selection in companion mode --- cmd/select/exitnode/exitnode.go | 6 ++++++ internal/companion/state.go | 2 +- internal/notice/notices.go | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/cmd/select/exitnode/exitnode.go b/cmd/select/exitnode/exitnode.go index a280d05..fbe6027 100644 --- a/cmd/select/exitnode/exitnode.go +++ b/cmd/select/exitnode/exitnode.go @@ -7,6 +7,7 @@ import ( "github.com/charmbracelet/huh" "github.com/fosrl/cli/internal/api" + "github.com/fosrl/cli/internal/companion" "github.com/fosrl/cli/internal/config" "github.com/fosrl/cli/internal/logger" "github.com/fosrl/cli/internal/olm" @@ -47,6 +48,11 @@ choice is saved and applied the next time you run 'pangolin up'.`, } func exitNodeMain(cmd *cobra.Command, opts *ExitNodeCmdOpts) error { + if err := companion.GuardMutatingAuth(cmd.Context()); err != nil { + logger.Error("%v", err) + return err + } + // The client doesn't have to be running: the choice is saved to the config // and applied by the next `pangolin up`. When it is running it is also // applied live. diff --git a/internal/companion/state.go b/internal/companion/state.go index 026abdf..98859cd 100644 --- a/internal/companion/state.go +++ b/internal/companion/state.go @@ -90,7 +90,7 @@ func notReadySuggestion(provider Provider, dataDir string, session *Session) str func MutatingAuthError(providerName string) error { return fmt.Errorf( "Authentication is managed by %s.\n"+ - "Login, logout, and account or organization changes must be done in %s.\n"+ + "Login, logout, and account, organization, or exit node changes must be done in %s.\n"+ "To use standalone CLI auth, run 'pangolin companion disable'.", providerName, providerName, diff --git a/internal/notice/notices.go b/internal/notice/notices.go index 6d7e7f9..4356686 100644 --- a/internal/notice/notices.go +++ b/internal/notice/notices.go @@ -26,7 +26,7 @@ func companionModeIntroLines(cfg *config.Config) []string { lines := []string{ "Pangolin CLI now uses companion mode with " + clientName + ".", - "Login, logout, and account or organization changes are managed in " + clientName + ".", + "Login, logout, and account, organization, or exit node changes are managed in " + clientName + ".", } if companion.RequiredDesktopAppVersion() != "" { lines = append(lines, "Requires "+clientName+" version "+companion.RequiredDesktopAppVersion()+" or later.") From 235f3806f39a4ff2fcbdaddb7204e5addc75d8d7 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 29 Sep 2026 16:17:21 -0400 Subject: [PATCH 13/14] Update newt and olm --- go.mod | 8 ++++---- go.sum | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 24c6a0f..a739e6a 100644 --- a/go.mod +++ b/go.mod @@ -10,8 +10,8 @@ require ( github.com/charmbracelet/huh v1.0.0 github.com/charmbracelet/lipgloss v1.1.0 github.com/creack/pty v1.1.24 - github.com/fosrl/newt v1.17.0 - github.com/fosrl/olm v1.9.1 + github.com/fosrl/newt v1.18.0 + github.com/fosrl/olm v1.10.0 github.com/mattn/go-isatty v0.0.24 github.com/pelletier/go-toml/v2 v2.2.4 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c @@ -131,6 +131,6 @@ require ( // If changes to Olm or Newt are required, use these // replace directives during development. // -replace github.com/fosrl/olm => ../olm +// replace github.com/fosrl/olm => ../olm -replace github.com/fosrl/newt => ../newt +// replace github.com/fosrl/newt => ../newt diff --git a/go.sum b/go.sum index 657104a..ea64edf 100644 --- a/go.sum +++ b/go.sum @@ -74,6 +74,10 @@ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= +github.com/fosrl/newt v1.18.0 h1:fFBksIV6BoI+BGmv6fwo87tXBk1WlxbVoruvupeD6hk= +github.com/fosrl/newt v1.18.0/go.mod h1:CwcuQtifgDQeSWSEB3yfqOgheFv9yltVBYQNgDB/DoM= +github.com/fosrl/olm v1.10.0 h1:2YS6Kirab1yg+MZSM8y9/Ja5IpwQBFY6d7ZMsPuYGM0= +github.com/fosrl/olm v1.10.0/go.mod h1:c/ECpzDjwnkcLrXxNpFDpFViE7+NC8yRuuLCOhGj7OA= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= From 65558890f131c3b007ab059ab3917fdac922a934 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 29 Sep 2026 16:22:09 -0400 Subject: [PATCH 14/14] Update flake --- flake.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flake.nix b/flake.nix index 00b9ee0..b2eb87e 100644 --- a/flake.nix +++ b/flake.nix @@ -21,10 +21,10 @@ in rec { pangolin-cli = pkgs.buildGoModule { pname = "pangolin-cli"; - version = "0.17.0"; + version = "0.18.0"; src = ./.; - vendorHash = "sha256-BAI5T7W0Wixcn11sr41cIMYlfsnjNAcuzYnDLWI1pcs="; + vendorHash = "sha256-2kspX9UE7qHF6CfJxILkUfjWohQSmIkKRe7JuYKq3WQ="; ldflags = [ "-s"