mirror of
https://github.com/slackhq/nebula.git
synced 2026-11-15 16:26:52 +01:00
Report ssh command failures to the surface (#1959)
smoke-extra / freebsd-amd64 (push) Failing after 14s
smoke-extra / linux-amd64-ipv6disable (push) Failing after 13s
smoke-extra / netbsd-amd64 (push) Failing after 14s
smoke-extra / openbsd-amd64 (push) Failing after 15s
smoke-extra / linux-386 (push) Failing after 14s
smoke / Run multi node smoke test (push) Failing after 1m46s
Build and test / Static checks (push) Successful in 2m38s
Build and test / Test linux (push) Failing after 1m12s
Build and test / Test linux-pkcs11 (push) Failing after 2m6s
Build and test / Test linux-boringcrypto (push) Failing after 3m0s
Build and test / Test linux-fips140 (push) Failing after 2m56s
Build and test / Cross-build linux-arm (push) Successful in 3m10s
Build and test / Cross-build linux-mips (push) Successful in 3m55s
Build and test / Cross-build linux-other (push) Successful in 3m15s
Build and test / Cross-build windows (push) Successful in 1m5s
Build and test / Cross-build freebsd (push) Successful in 1m37s
Build and test / Cross-build netbsd (push) Successful in 1m36s
Build and test / Cross-build openbsd (push) Successful in 1m35s
Build and test / Cross-build mobile (push) Successful in 3m32s
smoke-extra / Run windows smoke test (push) Canceled after 0s
smoke / Run self traffic smoke test on macOS (push) Canceled after 0s
Build and test / Test macos (push) Canceled after 0s
Build and test / Test windows (push) Canceled after 0s
Build and test / CI status (push) Canceled after 0s
smoke-extra / freebsd-amd64 (push) Failing after 14s
smoke-extra / linux-amd64-ipv6disable (push) Failing after 13s
smoke-extra / netbsd-amd64 (push) Failing after 14s
smoke-extra / openbsd-amd64 (push) Failing after 15s
smoke-extra / linux-386 (push) Failing after 14s
smoke / Run multi node smoke test (push) Failing after 1m46s
Build and test / Static checks (push) Successful in 2m38s
Build and test / Test linux (push) Failing after 1m12s
Build and test / Test linux-pkcs11 (push) Failing after 2m6s
Build and test / Test linux-boringcrypto (push) Failing after 3m0s
Build and test / Test linux-fips140 (push) Failing after 2m56s
Build and test / Cross-build linux-arm (push) Successful in 3m10s
Build and test / Cross-build linux-mips (push) Successful in 3m55s
Build and test / Cross-build linux-other (push) Successful in 3m15s
Build and test / Cross-build windows (push) Successful in 1m5s
Build and test / Cross-build freebsd (push) Successful in 1m37s
Build and test / Cross-build netbsd (push) Successful in 1m36s
Build and test / Cross-build openbsd (push) Successful in 1m35s
Build and test / Cross-build mobile (push) Successful in 3m32s
smoke-extra / Run windows smoke test (push) Canceled after 0s
smoke / Run self traffic smoke test on macOS (push) Canceled after 0s
Build and test / Test macos (push) Canceled after 0s
Build and test / Test windows (push) Canceled after 0s
Build and test / CI status (push) Canceled after 0s
Report ssh command failures to the surface (#1959) The ssh debug commands told the user when something went wrong but never told the surface that ran them, so `ssh host cmd` always exited 0 and a few failures were not reported at all. A failed command now returns its error, a failure the user has already heard about is a commands.Failure, and the ssh exec path exits 1 for either. The command bodies also lose their ssh prefixes and the duplication they carried over from ssh.go; both goldens pin that nothing else changed. - Dispatch returns the callback's error, or shlex's; the ssh session logs it at debug, and not at all for a Failure the user already saw - commands.Fail writes a message and returns it as a Failure in one step; an unknown command, `help` for a command that is not there, a failed reload and an unbalanced quote are failures - ssh exec exit-status is 1 for a command, flag or failure error, 0 otherwise; the e2e golden records the exit status for every case - Clone rebinds help to the copy, so an ssh session's help lists logout and `help logout` and `logout -h` answer; sshd.LogoutCommand is the session's logout, exported for tests - "did not understand" names the first word only, the one looked up; DispatchArgs takes just the tokens - log-format writes its refusal to the user, as log-level does - print-relays emits the relay error as a string and omits the key when there is none, where it printed {} or "Error":null - list-hostmap and list-lighthouse-addrmap -json return the encode error; print-cert reports a certificate that will not marshal - a flags type that is not the one registered is an internal error, not a silent success - command functions and flag types are <name>Command and <name>Flags; parseVpnAddr and listHostMapFlagSet replace the repeated parse block and flag set; close-tunnel sends through sendCloseTunnel Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
b426843f88
commit
8a4bd8b10f
11 files changed
+641
-246
No files matched your search
+141
-187
@@ -3,7 +3,6 @@ package nebula
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
@@ -14,42 +13,51 @@ import (
|
|||||||
|
|
||||||
"github.com/slackhq/nebula/commands"
|
"github.com/slackhq/nebula/commands"
|
||||||
"github.com/slackhq/nebula/config"
|
"github.com/slackhq/nebula/config"
|
||||||
"github.com/slackhq/nebula/header"
|
|
||||||
"github.com/slackhq/nebula/logging"
|
"github.com/slackhq/nebula/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The commands nebula serves. attachCommands builds the one registry every surface
|
// The commands nebula serves. attachCommands builds the one registry every surface
|
||||||
// reads. See the commands package for what a Command is.
|
// reads. See the commands package for what a Command is.
|
||||||
|
|
||||||
type sshListHostMapFlags struct {
|
type listHostMapFlags struct {
|
||||||
Json bool
|
Json bool
|
||||||
Pretty bool
|
Pretty bool
|
||||||
ByIndex bool
|
ByIndex bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type sshPrintCertFlags struct {
|
// listHostMapFlagSet is the flag set list-hostmap and list-pending-hostmap share.
|
||||||
|
func listHostMapFlagSet() (*flag.FlagSet, any) {
|
||||||
|
fl := flag.NewFlagSet("", flag.ContinueOnError)
|
||||||
|
s := listHostMapFlags{}
|
||||||
|
fl.BoolVar(&s.Json, "json", false, "outputs as json with more information")
|
||||||
|
fl.BoolVar(&s.Pretty, "pretty", false, "pretty prints json, assumes -json")
|
||||||
|
fl.BoolVar(&s.ByIndex, "by-index", false, "gets all hosts in the hostmap from the index table")
|
||||||
|
return fl, &s
|
||||||
|
}
|
||||||
|
|
||||||
|
type printCertFlags struct {
|
||||||
Json bool
|
Json bool
|
||||||
Pretty bool
|
Pretty bool
|
||||||
Raw bool
|
Raw bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type sshPrintTunnelFlags struct {
|
type printTunnelFlags struct {
|
||||||
Pretty bool
|
Pretty bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type sshChangeRemoteFlags struct {
|
type changeRemoteFlags struct {
|
||||||
Address string
|
Address string
|
||||||
}
|
}
|
||||||
|
|
||||||
type sshCloseTunnelFlags struct {
|
type closeTunnelFlags struct {
|
||||||
LocalOnly bool
|
LocalOnly bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type sshCreateTunnelFlags struct {
|
type createTunnelFlags struct {
|
||||||
Address string
|
Address string
|
||||||
}
|
}
|
||||||
|
|
||||||
type sshDeviceInfoFlags struct {
|
type deviceInfoFlags struct {
|
||||||
Json bool
|
Json bool
|
||||||
Pretty bool
|
Pretty bool
|
||||||
}
|
}
|
||||||
@@ -58,32 +66,18 @@ func attachCommands(l *slog.Logger, c *config.C, reg *commands.Registry, f *Inte
|
|||||||
reg.Register(&commands.Command{
|
reg.Register(&commands.Command{
|
||||||
Name: commands.ListHostmap,
|
Name: commands.ListHostmap,
|
||||||
ShortDescription: "List all known previously connected hosts",
|
ShortDescription: "List all known previously connected hosts",
|
||||||
Flags: func() (*flag.FlagSet, any) {
|
Flags: listHostMapFlagSet,
|
||||||
fl := flag.NewFlagSet("", flag.ContinueOnError)
|
|
||||||
s := sshListHostMapFlags{}
|
|
||||||
fl.BoolVar(&s.Json, "json", false, "outputs as json with more information")
|
|
||||||
fl.BoolVar(&s.Pretty, "pretty", false, "pretty prints json, assumes -json")
|
|
||||||
fl.BoolVar(&s.ByIndex, "by-index", false, "gets all hosts in the hostmap from the index table")
|
|
||||||
return fl, &s
|
|
||||||
},
|
|
||||||
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
||||||
return sshListHostMap(f.hostMap, fs, w)
|
return listHostMapCommand(f.hostMap, fs, w)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
reg.Register(&commands.Command{
|
reg.Register(&commands.Command{
|
||||||
Name: commands.ListPendingHostmap,
|
Name: commands.ListPendingHostmap,
|
||||||
ShortDescription: "List all handshaking hosts",
|
ShortDescription: "List all handshaking hosts",
|
||||||
Flags: func() (*flag.FlagSet, any) {
|
Flags: listHostMapFlagSet,
|
||||||
fl := flag.NewFlagSet("", flag.ContinueOnError)
|
|
||||||
s := sshListHostMapFlags{}
|
|
||||||
fl.BoolVar(&s.Json, "json", false, "outputs as json with more information")
|
|
||||||
fl.BoolVar(&s.Pretty, "pretty", false, "pretty prints json, assumes -json")
|
|
||||||
fl.BoolVar(&s.ByIndex, "by-index", false, "gets all hosts in the hostmap from the index table")
|
|
||||||
return fl, &s
|
|
||||||
},
|
|
||||||
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
||||||
return sshListHostMap(f.handshakeManager, fs, w)
|
return listHostMapCommand(f.handshakeManager, fs, w)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -92,13 +86,13 @@ func attachCommands(l *slog.Logger, c *config.C, reg *commands.Registry, f *Inte
|
|||||||
ShortDescription: "List all lighthouse map entries",
|
ShortDescription: "List all lighthouse map entries",
|
||||||
Flags: func() (*flag.FlagSet, any) {
|
Flags: func() (*flag.FlagSet, any) {
|
||||||
fl := flag.NewFlagSet("", flag.ContinueOnError)
|
fl := flag.NewFlagSet("", flag.ContinueOnError)
|
||||||
s := sshListHostMapFlags{}
|
s := listHostMapFlags{}
|
||||||
fl.BoolVar(&s.Json, "json", false, "outputs as json with more information")
|
fl.BoolVar(&s.Json, "json", false, "outputs as json with more information")
|
||||||
fl.BoolVar(&s.Pretty, "pretty", false, "pretty prints json, assumes -json")
|
fl.BoolVar(&s.Pretty, "pretty", false, "pretty prints json, assumes -json")
|
||||||
return fl, &s
|
return fl, &s
|
||||||
},
|
},
|
||||||
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
||||||
return sshListLighthouseMap(f.lightHouse, fs, w)
|
return listLighthouseAddrmapCommand(f.lightHouse, fs, w)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -106,7 +100,7 @@ func attachCommands(l *slog.Logger, c *config.C, reg *commands.Registry, f *Inte
|
|||||||
Name: commands.Reload,
|
Name: commands.Reload,
|
||||||
ShortDescription: "Reloads configuration from disk, same as sending HUP to the process",
|
ShortDescription: "Reloads configuration from disk, same as sending HUP to the process",
|
||||||
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
||||||
return sshReload(l, c, w)
|
return reloadCommand(l, c, w)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -114,7 +108,7 @@ func attachCommands(l *slog.Logger, c *config.C, reg *commands.Registry, f *Inte
|
|||||||
Name: commands.LogLevel,
|
Name: commands.LogLevel,
|
||||||
ShortDescription: "Gets or sets the current log level",
|
ShortDescription: "Gets or sets the current log level",
|
||||||
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
||||||
return sshLogLevel(l, fs, a, w)
|
return logLevelCommand(l, fs, a, w)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -122,7 +116,7 @@ func attachCommands(l *slog.Logger, c *config.C, reg *commands.Registry, f *Inte
|
|||||||
Name: commands.LogFormat,
|
Name: commands.LogFormat,
|
||||||
ShortDescription: "Gets or sets the current log format",
|
ShortDescription: "Gets or sets the current log format",
|
||||||
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
||||||
return sshLogFormat(l, fs, a, w)
|
return logFormatCommand(l, fs, a, w)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -130,7 +124,7 @@ func attachCommands(l *slog.Logger, c *config.C, reg *commands.Registry, f *Inte
|
|||||||
Name: commands.Version,
|
Name: commands.Version,
|
||||||
ShortDescription: "Prints the currently running version of nebula",
|
ShortDescription: "Prints the currently running version of nebula",
|
||||||
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
||||||
return sshVersion(f, fs, a, w)
|
return versionCommand(f, fs, a, w)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -139,13 +133,13 @@ func attachCommands(l *slog.Logger, c *config.C, reg *commands.Registry, f *Inte
|
|||||||
ShortDescription: "Prints information about the network device.",
|
ShortDescription: "Prints information about the network device.",
|
||||||
Flags: func() (*flag.FlagSet, any) {
|
Flags: func() (*flag.FlagSet, any) {
|
||||||
fl := flag.NewFlagSet("", flag.ContinueOnError)
|
fl := flag.NewFlagSet("", flag.ContinueOnError)
|
||||||
s := sshDeviceInfoFlags{}
|
s := deviceInfoFlags{}
|
||||||
fl.BoolVar(&s.Json, "json", false, "outputs as json with more information")
|
fl.BoolVar(&s.Json, "json", false, "outputs as json with more information")
|
||||||
fl.BoolVar(&s.Pretty, "pretty", false, "pretty prints json, assumes -json")
|
fl.BoolVar(&s.Pretty, "pretty", false, "pretty prints json, assumes -json")
|
||||||
return fl, &s
|
return fl, &s
|
||||||
},
|
},
|
||||||
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
||||||
return sshDeviceInfo(f, fs, w)
|
return deviceInfoCommand(f, fs, w)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -154,14 +148,14 @@ func attachCommands(l *slog.Logger, c *config.C, reg *commands.Registry, f *Inte
|
|||||||
ShortDescription: "Prints the current certificate being used or the certificate for the provided vpn addr",
|
ShortDescription: "Prints the current certificate being used or the certificate for the provided vpn addr",
|
||||||
Flags: func() (*flag.FlagSet, any) {
|
Flags: func() (*flag.FlagSet, any) {
|
||||||
fl := flag.NewFlagSet("", flag.ContinueOnError)
|
fl := flag.NewFlagSet("", flag.ContinueOnError)
|
||||||
s := sshPrintCertFlags{}
|
s := printCertFlags{}
|
||||||
fl.BoolVar(&s.Json, "json", false, "outputs as json")
|
fl.BoolVar(&s.Json, "json", false, "outputs as json")
|
||||||
fl.BoolVar(&s.Pretty, "pretty", false, "pretty prints json, assumes -json")
|
fl.BoolVar(&s.Pretty, "pretty", false, "pretty prints json, assumes -json")
|
||||||
fl.BoolVar(&s.Raw, "raw", false, "raw prints the PEM encoded certificate, not compatible with -json or -pretty")
|
fl.BoolVar(&s.Raw, "raw", false, "raw prints the PEM encoded certificate, not compatible with -json or -pretty")
|
||||||
return fl, &s
|
return fl, &s
|
||||||
},
|
},
|
||||||
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
||||||
return sshPrintCert(f, fs, a, w)
|
return printCertCommand(f, fs, a, w)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -170,12 +164,12 @@ func attachCommands(l *slog.Logger, c *config.C, reg *commands.Registry, f *Inte
|
|||||||
ShortDescription: "Prints json details about a tunnel for the provided vpn addr",
|
ShortDescription: "Prints json details about a tunnel for the provided vpn addr",
|
||||||
Flags: func() (*flag.FlagSet, any) {
|
Flags: func() (*flag.FlagSet, any) {
|
||||||
fl := flag.NewFlagSet("", flag.ContinueOnError)
|
fl := flag.NewFlagSet("", flag.ContinueOnError)
|
||||||
s := sshPrintTunnelFlags{}
|
s := printTunnelFlags{}
|
||||||
fl.BoolVar(&s.Pretty, "pretty", false, "pretty prints json")
|
fl.BoolVar(&s.Pretty, "pretty", false, "pretty prints json")
|
||||||
return fl, &s
|
return fl, &s
|
||||||
},
|
},
|
||||||
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
||||||
return sshPrintTunnel(f, fs, a, w)
|
return printTunnelCommand(f, fs, a, w)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -184,12 +178,12 @@ func attachCommands(l *slog.Logger, c *config.C, reg *commands.Registry, f *Inte
|
|||||||
ShortDescription: "Prints json details about all relay info",
|
ShortDescription: "Prints json details about all relay info",
|
||||||
Flags: func() (*flag.FlagSet, any) {
|
Flags: func() (*flag.FlagSet, any) {
|
||||||
fl := flag.NewFlagSet("", flag.ContinueOnError)
|
fl := flag.NewFlagSet("", flag.ContinueOnError)
|
||||||
s := sshPrintTunnelFlags{}
|
s := printTunnelFlags{}
|
||||||
fl.BoolVar(&s.Pretty, "pretty", false, "pretty prints json")
|
fl.BoolVar(&s.Pretty, "pretty", false, "pretty prints json")
|
||||||
return fl, &s
|
return fl, &s
|
||||||
},
|
},
|
||||||
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
||||||
return sshPrintRelays(f, fs, a, w)
|
return printRelaysCommand(f, fs, a, w)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -198,12 +192,12 @@ func attachCommands(l *slog.Logger, c *config.C, reg *commands.Registry, f *Inte
|
|||||||
ShortDescription: "Changes the remote address used in the tunnel for the provided vpn addr",
|
ShortDescription: "Changes the remote address used in the tunnel for the provided vpn addr",
|
||||||
Flags: func() (*flag.FlagSet, any) {
|
Flags: func() (*flag.FlagSet, any) {
|
||||||
fl := flag.NewFlagSet("", flag.ContinueOnError)
|
fl := flag.NewFlagSet("", flag.ContinueOnError)
|
||||||
s := sshChangeRemoteFlags{}
|
s := changeRemoteFlags{}
|
||||||
fl.StringVar(&s.Address, "address", "", "The new remote address, ip:port")
|
fl.StringVar(&s.Address, "address", "", "The new remote address, ip:port")
|
||||||
return fl, &s
|
return fl, &s
|
||||||
},
|
},
|
||||||
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
||||||
return sshChangeRemote(f, fs, a, w)
|
return changeRemoteCommand(f, fs, a, w)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -212,12 +206,12 @@ func attachCommands(l *slog.Logger, c *config.C, reg *commands.Registry, f *Inte
|
|||||||
ShortDescription: "Closes a tunnel for the provided vpn addr",
|
ShortDescription: "Closes a tunnel for the provided vpn addr",
|
||||||
Flags: func() (*flag.FlagSet, any) {
|
Flags: func() (*flag.FlagSet, any) {
|
||||||
fl := flag.NewFlagSet("", flag.ContinueOnError)
|
fl := flag.NewFlagSet("", flag.ContinueOnError)
|
||||||
s := sshCloseTunnelFlags{}
|
s := closeTunnelFlags{}
|
||||||
fl.BoolVar(&s.LocalOnly, "local-only", false, "Disables notifying the remote that the tunnel is shutting down")
|
fl.BoolVar(&s.LocalOnly, "local-only", false, "Disables notifying the remote that the tunnel is shutting down")
|
||||||
return fl, &s
|
return fl, &s
|
||||||
},
|
},
|
||||||
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
||||||
return sshCloseTunnel(f, fs, a, w)
|
return closeTunnelCommand(f, fs, a, w)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -227,12 +221,12 @@ func attachCommands(l *slog.Logger, c *config.C, reg *commands.Registry, f *Inte
|
|||||||
Help: "The lighthouses will be queried for real addresses but you can provide one as well.",
|
Help: "The lighthouses will be queried for real addresses but you can provide one as well.",
|
||||||
Flags: func() (*flag.FlagSet, any) {
|
Flags: func() (*flag.FlagSet, any) {
|
||||||
fl := flag.NewFlagSet("", flag.ContinueOnError)
|
fl := flag.NewFlagSet("", flag.ContinueOnError)
|
||||||
s := sshCreateTunnelFlags{}
|
s := createTunnelFlags{}
|
||||||
fl.StringVar(&s.Address, "address", "", "Optionally provide a real remote address, ip:port ")
|
fl.StringVar(&s.Address, "address", "", "Optionally provide a real remote address, ip:port ")
|
||||||
return fl, &s
|
return fl, &s
|
||||||
},
|
},
|
||||||
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
||||||
return sshCreateTunnel(f, fs, a, w)
|
return createTunnelCommand(f, fs, a, w)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -241,15 +235,15 @@ func attachCommands(l *slog.Logger, c *config.C, reg *commands.Registry, f *Inte
|
|||||||
ShortDescription: "Query the lighthouses for the provided vpn address",
|
ShortDescription: "Query the lighthouses for the provided vpn address",
|
||||||
Help: "This command is asynchronous. Only currently known udp addresses will be printed.",
|
Help: "This command is asynchronous. Only currently known udp addresses will be printed.",
|
||||||
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
||||||
return sshQueryLighthouse(f, fs, a, w)
|
return queryLighthouseCommand(f, fs, a, w)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func sshListHostMap(hl controlHostLister, a any, w commands.StringWriter) error {
|
func listHostMapCommand(hl controlHostLister, a any, w commands.StringWriter) error {
|
||||||
fs, ok := a.(*sshListHostMapFlags)
|
fs, ok := a.(*listHostMapFlags)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return fmt.Errorf("internal error: expected *listHostMapFlags but was %T", a)
|
||||||
}
|
}
|
||||||
|
|
||||||
var hm []ControlHostInfo
|
var hm []ControlHostInfo
|
||||||
@@ -269,9 +263,8 @@ func sshListHostMap(hl controlHostLister, a any, w commands.StringWriter) error
|
|||||||
js.SetIndent("", " ")
|
js.SetIndent("", " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
err := js.Encode(hm)
|
if err := js.Encode(hm); err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@@ -286,10 +279,10 @@ func sshListHostMap(hl controlHostLister, a any, w commands.StringWriter) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func sshListLighthouseMap(lightHouse *LightHouse, a any, w commands.StringWriter) error {
|
func listLighthouseAddrmapCommand(lightHouse *LightHouse, a any, w commands.StringWriter) error {
|
||||||
fs, ok := a.(*sshListHostMapFlags)
|
fs, ok := a.(*listHostMapFlags)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return fmt.Errorf("internal error: expected *listHostMapFlags but was %T", a)
|
||||||
}
|
}
|
||||||
|
|
||||||
type lighthouseInfo struct {
|
type lighthouseInfo struct {
|
||||||
@@ -319,9 +312,8 @@ func sshListLighthouseMap(lightHouse *LightHouse, a any, w commands.StringWriter
|
|||||||
js.SetIndent("", " ")
|
js.SetIndent("", " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
err := js.Encode(addrMap)
|
if err := js.Encode(addrMap); err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@@ -340,22 +332,30 @@ func sshListLighthouseMap(lightHouse *LightHouse, a any, w commands.StringWriter
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func sshVersion(ifce *Interface, fs any, a []string, w commands.StringWriter) error {
|
// parseVpnAddr parses the vpn address a command takes as its first operand, failing to w
|
||||||
return w.WriteLine(fmt.Sprintf("%s", ifce.version))
|
// when there is none or it does not parse. noun is how the command's messages name it,
|
||||||
}
|
// "vpn address" or "vpn addr", which the output golden pins per command.
|
||||||
|
func parseVpnAddr(a []string, noun string, w commands.StringWriter) (netip.Addr, error) {
|
||||||
func sshQueryLighthouse(ifce *Interface, fs any, a []string, w commands.StringWriter) error {
|
|
||||||
if len(a) == 0 {
|
if len(a) == 0 {
|
||||||
return w.WriteLine("No vpn address was provided")
|
return netip.Addr{}, commands.Fail(w, "No vpn address was provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
vpnAddr, err := netip.ParseAddr(a[0])
|
vpnAddr, err := netip.ParseAddr(a[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return w.WriteLine(fmt.Sprintf("The provided vpn address could not be parsed: %s", a[0]))
|
return netip.Addr{}, commands.Fail(w, fmt.Sprintf("The provided %s could not be parsed: %s", noun, a[0]))
|
||||||
}
|
}
|
||||||
|
|
||||||
if !vpnAddr.IsValid() {
|
return vpnAddr, nil
|
||||||
return w.WriteLine(fmt.Sprintf("The provided vpn address could not be parsed: %s", a[0]))
|
}
|
||||||
|
|
||||||
|
func versionCommand(ifce *Interface, fs any, a []string, w commands.StringWriter) error {
|
||||||
|
return w.WriteLine(ifce.version)
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryLighthouseCommand(ifce *Interface, fs any, a []string, w commands.StringWriter) error {
|
||||||
|
vpnAddr, err := parseVpnAddr(a, "vpn address", w)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var cm *CacheMap
|
var cm *CacheMap
|
||||||
@@ -366,80 +366,56 @@ func sshQueryLighthouse(ifce *Interface, fs any, a []string, w commands.StringWr
|
|||||||
return json.NewEncoder(w.GetWriter()).Encode(cm)
|
return json.NewEncoder(w.GetWriter()).Encode(cm)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sshCloseTunnel(ifce *Interface, fs any, a []string, w commands.StringWriter) error {
|
func closeTunnelCommand(ifce *Interface, fs any, a []string, w commands.StringWriter) error {
|
||||||
flags, ok := fs.(*sshCloseTunnelFlags)
|
flags, ok := fs.(*closeTunnelFlags)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return fmt.Errorf("internal error: expected *closeTunnelFlags but was %T", fs)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(a) == 0 {
|
vpnAddr, err := parseVpnAddr(a, "vpn address", w)
|
||||||
return w.WriteLine("No vpn address was provided")
|
|
||||||
}
|
|
||||||
|
|
||||||
vpnAddr, err := netip.ParseAddr(a[0])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return w.WriteLine(fmt.Sprintf("The provided vpn address could not be parsed: %s", a[0]))
|
return err
|
||||||
}
|
|
||||||
|
|
||||||
if !vpnAddr.IsValid() {
|
|
||||||
return w.WriteLine(fmt.Sprintf("The provided vpn address could not be parsed: %s", a[0]))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hostInfo := ifce.hostMap.QueryVpnAddr(vpnAddr)
|
hostInfo := ifce.hostMap.QueryVpnAddr(vpnAddr)
|
||||||
if hostInfo == nil {
|
if hostInfo == nil {
|
||||||
return w.WriteLine(fmt.Sprintf("Could not find tunnel for vpn address: %v", a[0]))
|
return commands.Fail(w, fmt.Sprintf("Could not find tunnel for vpn address: %v", a[0]))
|
||||||
}
|
}
|
||||||
|
|
||||||
if !flags.LocalOnly {
|
if !flags.LocalOnly {
|
||||||
ifce.send(
|
ifce.sendCloseTunnel(hostInfo)
|
||||||
header.CloseTunnel,
|
|
||||||
0,
|
|
||||||
hostInfo.ConnectionState,
|
|
||||||
hostInfo,
|
|
||||||
[]byte{},
|
|
||||||
make([]byte, 12, 12),
|
|
||||||
make([]byte, mtu),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ifce.closeTunnel(hostInfo)
|
ifce.closeTunnel(hostInfo)
|
||||||
return w.WriteLine("Closed")
|
return w.WriteLine("Closed")
|
||||||
}
|
}
|
||||||
|
|
||||||
func sshCreateTunnel(ifce *Interface, fs any, a []string, w commands.StringWriter) error {
|
func createTunnelCommand(ifce *Interface, fs any, a []string, w commands.StringWriter) error {
|
||||||
flags, ok := fs.(*sshCreateTunnelFlags)
|
flags, ok := fs.(*createTunnelFlags)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return fmt.Errorf("internal error: expected *createTunnelFlags but was %T", fs)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(a) == 0 {
|
vpnAddr, err := parseVpnAddr(a, "vpn address", w)
|
||||||
return w.WriteLine("No vpn address was provided")
|
|
||||||
}
|
|
||||||
|
|
||||||
vpnAddr, err := netip.ParseAddr(a[0])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return w.WriteLine(fmt.Sprintf("The provided vpn address could not be parsed: %s", a[0]))
|
return err
|
||||||
}
|
|
||||||
|
|
||||||
if !vpnAddr.IsValid() {
|
|
||||||
return w.WriteLine(fmt.Sprintf("The provided vpn address could not be parsed: %s", a[0]))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hostInfo := ifce.hostMap.QueryVpnAddr(vpnAddr)
|
hostInfo := ifce.hostMap.QueryVpnAddr(vpnAddr)
|
||||||
if hostInfo != nil {
|
if hostInfo != nil {
|
||||||
return w.WriteLine(fmt.Sprintf("Tunnel already exists"))
|
return commands.Fail(w, "Tunnel already exists")
|
||||||
}
|
}
|
||||||
|
|
||||||
hostInfo = ifce.handshakeManager.QueryVpnAddr(vpnAddr)
|
hostInfo = ifce.handshakeManager.QueryVpnAddr(vpnAddr)
|
||||||
if hostInfo != nil {
|
if hostInfo != nil {
|
||||||
return w.WriteLine(fmt.Sprintf("Tunnel already handshaking"))
|
return commands.Fail(w, "Tunnel already handshaking")
|
||||||
}
|
}
|
||||||
|
|
||||||
var addr netip.AddrPort
|
var addr netip.AddrPort
|
||||||
if flags.Address != "" {
|
if flags.Address != "" {
|
||||||
addr, err = netip.ParseAddrPort(flags.Address)
|
addr, err = netip.ParseAddrPort(flags.Address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return w.WriteLine("Address could not be parsed")
|
return commands.Fail(w, "Address could not be parsed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,50 +431,42 @@ func sshCreateTunnel(ifce *Interface, fs any, a []string, w commands.StringWrite
|
|||||||
return w.WriteLine("Created")
|
return w.WriteLine("Created")
|
||||||
}
|
}
|
||||||
|
|
||||||
func sshChangeRemote(ifce *Interface, fs any, a []string, w commands.StringWriter) error {
|
func changeRemoteCommand(ifce *Interface, fs any, a []string, w commands.StringWriter) error {
|
||||||
flags, ok := fs.(*sshChangeRemoteFlags)
|
flags, ok := fs.(*changeRemoteFlags)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return fmt.Errorf("internal error: expected *changeRemoteFlags but was %T", fs)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(a) == 0 {
|
vpnAddr, err := parseVpnAddr(a, "vpn address", w)
|
||||||
return w.WriteLine("No vpn address was provided")
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if flags.Address == "" {
|
if flags.Address == "" {
|
||||||
return w.WriteLine("No address was provided")
|
return commands.Fail(w, "No address was provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
addr, err := netip.ParseAddrPort(flags.Address)
|
addr, err := netip.ParseAddrPort(flags.Address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return w.WriteLine("Address could not be parsed")
|
return commands.Fail(w, "Address could not be parsed")
|
||||||
}
|
|
||||||
|
|
||||||
vpnAddr, err := netip.ParseAddr(a[0])
|
|
||||||
if err != nil {
|
|
||||||
return w.WriteLine(fmt.Sprintf("The provided vpn address could not be parsed: %s", a[0]))
|
|
||||||
}
|
|
||||||
|
|
||||||
if !vpnAddr.IsValid() {
|
|
||||||
return w.WriteLine(fmt.Sprintf("The provided vpn address could not be parsed: %s", a[0]))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hostInfo := ifce.hostMap.QueryVpnAddr(vpnAddr)
|
hostInfo := ifce.hostMap.QueryVpnAddr(vpnAddr)
|
||||||
if hostInfo == nil {
|
if hostInfo == nil {
|
||||||
return w.WriteLine(fmt.Sprintf("Could not find tunnel for vpn address: %v", a[0]))
|
return commands.Fail(w, fmt.Sprintf("Could not find tunnel for vpn address: %v", a[0]))
|
||||||
}
|
}
|
||||||
|
|
||||||
hostInfo.SetRemote(addr)
|
hostInfo.SetRemote(addr)
|
||||||
return w.WriteLine("Changed")
|
return w.WriteLine("Changed")
|
||||||
}
|
}
|
||||||
|
|
||||||
func sshLogLevel(l *slog.Logger, fs any, a []string, w commands.StringWriter) error {
|
func logLevelCommand(l *slog.Logger, fs any, a []string, w commands.StringWriter) error {
|
||||||
ctrl, ok := l.Handler().(interface {
|
ctrl, ok := l.Handler().(interface {
|
||||||
GetLevel() slog.Level
|
GetLevel() slog.Level
|
||||||
SetLevel(slog.Level)
|
SetLevel(slog.Level)
|
||||||
})
|
})
|
||||||
if !ok {
|
if !ok {
|
||||||
return w.WriteLine("Log level is not reconfigurable on this logger")
|
return commands.Fail(w, "Log level is not reconfigurable on this logger")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(a) == 0 {
|
if len(a) == 0 {
|
||||||
@@ -507,20 +475,20 @@ func sshLogLevel(l *slog.Logger, fs any, a []string, w commands.StringWriter) er
|
|||||||
|
|
||||||
level, err := logging.ParseLevel(strings.ToLower(a[0]))
|
level, err := logging.ParseLevel(strings.ToLower(a[0]))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return w.WriteLine(fmt.Sprintf("Unknown log level %s. Possible log levels: trace, debug, info, warn, error", a))
|
return commands.Fail(w, fmt.Sprintf("Unknown log level %s. Possible log levels: trace, debug, info, warn, error", a))
|
||||||
}
|
}
|
||||||
|
|
||||||
ctrl.SetLevel(level)
|
ctrl.SetLevel(level)
|
||||||
return w.WriteLine(fmt.Sprintf("Log level is: %s", logging.LevelName(ctrl.GetLevel())))
|
return w.WriteLine(fmt.Sprintf("Log level is: %s", logging.LevelName(ctrl.GetLevel())))
|
||||||
}
|
}
|
||||||
|
|
||||||
func sshLogFormat(l *slog.Logger, fs any, a []string, w commands.StringWriter) error {
|
func logFormatCommand(l *slog.Logger, fs any, a []string, w commands.StringWriter) error {
|
||||||
ctrl, ok := l.Handler().(interface {
|
ctrl, ok := l.Handler().(interface {
|
||||||
GetFormat() string
|
GetFormat() string
|
||||||
SetFormat(string) error
|
SetFormat(string) error
|
||||||
})
|
})
|
||||||
if !ok {
|
if !ok {
|
||||||
return w.WriteLine("Log format is not reconfigurable on this logger")
|
return commands.Fail(w, "Log format is not reconfigurable on this logger")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(a) == 0 {
|
if len(a) == 0 {
|
||||||
@@ -528,31 +496,27 @@ func sshLogFormat(l *slog.Logger, fs any, a []string, w commands.StringWriter) e
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := ctrl.SetFormat(strings.ToLower(a[0])); err != nil {
|
if err := ctrl.SetFormat(strings.ToLower(a[0])); err != nil {
|
||||||
return err
|
return commands.Fail(w, fmt.Sprintf("Unable to set log format: %s", err))
|
||||||
}
|
}
|
||||||
return w.WriteLine(fmt.Sprintf("Log format is: %s", ctrl.GetFormat()))
|
return w.WriteLine(fmt.Sprintf("Log format is: %s", ctrl.GetFormat()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func sshPrintCert(ifce *Interface, fs any, a []string, w commands.StringWriter) error {
|
func printCertCommand(ifce *Interface, fs any, a []string, w commands.StringWriter) error {
|
||||||
args, ok := fs.(*sshPrintCertFlags)
|
args, ok := fs.(*printCertFlags)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return fmt.Errorf("internal error: expected *printCertFlags but was %T", fs)
|
||||||
}
|
}
|
||||||
|
|
||||||
cert := ifce.pki.getCertState().GetDefaultCertificate()
|
cert := ifce.pki.getCertState().GetDefaultCertificate()
|
||||||
if len(a) > 0 {
|
if len(a) > 0 {
|
||||||
vpnAddr, err := netip.ParseAddr(a[0])
|
vpnAddr, err := parseVpnAddr(a, "vpn addr", w)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return w.WriteLine(fmt.Sprintf("The provided vpn addr could not be parsed: %s", a[0]))
|
return err
|
||||||
}
|
|
||||||
|
|
||||||
if !vpnAddr.IsValid() {
|
|
||||||
return w.WriteLine(fmt.Sprintf("The provided vpn addr could not be parsed: %s", a[0]))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hostInfo := ifce.hostMap.QueryVpnAddr(vpnAddr)
|
hostInfo := ifce.hostMap.QueryVpnAddr(vpnAddr)
|
||||||
if hostInfo == nil {
|
if hostInfo == nil {
|
||||||
return w.WriteLine(fmt.Sprintf("Could not find tunnel for vpn addr: %v", a[0]))
|
return commands.Fail(w, fmt.Sprintf("Could not find tunnel for vpn addr: %v", a[0]))
|
||||||
}
|
}
|
||||||
|
|
||||||
cert = hostInfo.GetCert().Certificate
|
cert = hostInfo.GetCert().Certificate
|
||||||
@@ -561,16 +525,15 @@ func sshPrintCert(ifce *Interface, fs any, a []string, w commands.StringWriter)
|
|||||||
if args.Json || args.Pretty {
|
if args.Json || args.Pretty {
|
||||||
b, err := cert.MarshalJSON()
|
b, err := cert.MarshalJSON()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return commands.Fail(w, fmt.Sprintf("Unable to marshal the certificate: %s", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.Pretty {
|
if args.Pretty {
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
err := json.Indent(buf, b, "", " ")
|
if err := json.Indent(buf, b, "", " "); err != nil {
|
||||||
b = buf.Bytes()
|
return commands.Fail(w, fmt.Sprintf("Unable to marshal the certificate: %s", err))
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
b = buf.Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.WriteBytes(b)
|
return w.WriteBytes(b)
|
||||||
@@ -579,7 +542,7 @@ func sshPrintCert(ifce *Interface, fs any, a []string, w commands.StringWriter)
|
|||||||
if args.Raw {
|
if args.Raw {
|
||||||
b, err := cert.MarshalPEM()
|
b, err := cert.MarshalPEM()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return commands.Fail(w, fmt.Sprintf("Unable to marshal the certificate: %s", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.WriteBytes(b)
|
return w.WriteBytes(b)
|
||||||
@@ -588,20 +551,18 @@ func sshPrintCert(ifce *Interface, fs any, a []string, w commands.StringWriter)
|
|||||||
return w.WriteLine(cert.String())
|
return w.WriteLine(cert.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func sshPrintRelays(ifce *Interface, fs any, a []string, w commands.StringWriter) error {
|
func printRelaysCommand(ifce *Interface, fs any, a []string, w commands.StringWriter) error {
|
||||||
args, ok := fs.(*sshPrintTunnelFlags)
|
args, ok := fs.(*printTunnelFlags)
|
||||||
if !ok {
|
if !ok {
|
||||||
w.WriteLine(fmt.Sprintf("sshPrintRelays failed to convert args type"))
|
return fmt.Errorf("internal error: expected *printTunnelFlags but was %T", fs)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
relays := map[uint32]*HostInfo{}
|
ifce.hostMap.RLock()
|
||||||
ifce.hostMap.Lock()
|
relays := maps.Clone(ifce.hostMap.Relays)
|
||||||
maps.Copy(relays, ifce.hostMap.Relays)
|
ifce.hostMap.RUnlock()
|
||||||
ifce.hostMap.Unlock()
|
|
||||||
|
|
||||||
type RelayFor struct {
|
type RelayFor struct {
|
||||||
Error error
|
Error string `json:",omitempty"`
|
||||||
Type string
|
Type string
|
||||||
State string
|
State string
|
||||||
PeerAddr netip.Addr
|
PeerAddr netip.Addr
|
||||||
@@ -632,11 +593,11 @@ func sshPrintRelays(ifce *Interface, fs any, a []string, w commands.StringWriter
|
|||||||
co.Relays = append(co.Relays, &ro)
|
co.Relays = append(co.Relays, &ro)
|
||||||
relayHI := ifce.hostMap.QueryVpnAddr(v.vpnAddrs[0])
|
relayHI := ifce.hostMap.QueryVpnAddr(v.vpnAddrs[0])
|
||||||
if relayHI == nil {
|
if relayHI == nil {
|
||||||
ro.RelayForAddrs = append(ro.RelayForAddrs, RelayFor{Error: errors.New("could not find hostinfo")})
|
ro.RelayForAddrs = append(ro.RelayForAddrs, RelayFor{Error: "could not find hostinfo"})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, vpnAddr := range relayHI.relayState.CopyRelayForIps() {
|
for _, vpnAddr := range relayHI.relayState.CopyRelayForIps() {
|
||||||
rf := RelayFor{Error: nil}
|
rf := RelayFor{}
|
||||||
r, ok := relayHI.relayState.GetRelayForByAddr(vpnAddr)
|
r, ok := relayHI.relayState.GetRelayForByAddr(vpnAddr)
|
||||||
if ok {
|
if ok {
|
||||||
t := ""
|
t := ""
|
||||||
@@ -665,7 +626,7 @@ func sshPrintRelays(ifce *Interface, fs any, a []string, w commands.StringWriter
|
|||||||
rf.Type = t
|
rf.Type = t
|
||||||
rf.State = s
|
rf.State = s
|
||||||
if rf.LocalIndex != k {
|
if rf.LocalIndex != k {
|
||||||
rf.Error = fmt.Errorf("hostmap LocalIndex '%v' does not match RelayState LocalIndex", k)
|
rf.Error = fmt.Sprintf("hostmap LocalIndex '%v' does not match RelayState LocalIndex", k)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
relayedHI := ifce.hostMap.QueryVpnAddr(vpnAddr)
|
relayedHI := ifce.hostMap.QueryVpnAddr(vpnAddr)
|
||||||
@@ -676,35 +637,23 @@ func sshPrintRelays(ifce *Interface, fs any, a []string, w commands.StringWriter
|
|||||||
ro.RelayForAddrs = append(ro.RelayForAddrs, rf)
|
ro.RelayForAddrs = append(ro.RelayForAddrs, rf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err := enc.Encode(co)
|
return enc.Encode(co)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func sshPrintTunnel(ifce *Interface, fs any, a []string, w commands.StringWriter) error {
|
func printTunnelCommand(ifce *Interface, fs any, a []string, w commands.StringWriter) error {
|
||||||
args, ok := fs.(*sshPrintTunnelFlags)
|
args, ok := fs.(*printTunnelFlags)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return fmt.Errorf("internal error: expected *printTunnelFlags but was %T", fs)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(a) == 0 {
|
vpnAddr, err := parseVpnAddr(a, "vpn addr", w)
|
||||||
return w.WriteLine("No vpn address was provided")
|
|
||||||
}
|
|
||||||
|
|
||||||
vpnAddr, err := netip.ParseAddr(a[0])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return w.WriteLine(fmt.Sprintf("The provided vpn addr could not be parsed: %s", a[0]))
|
return err
|
||||||
}
|
|
||||||
|
|
||||||
if !vpnAddr.IsValid() {
|
|
||||||
return w.WriteLine(fmt.Sprintf("The provided vpn addr could not be parsed: %s", a[0]))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hostInfo := ifce.hostMap.QueryVpnAddr(vpnAddr)
|
hostInfo := ifce.hostMap.QueryVpnAddr(vpnAddr)
|
||||||
if hostInfo == nil {
|
if hostInfo == nil {
|
||||||
return w.WriteLine(fmt.Sprintf("Could not find tunnel for vpn addr: %v", a[0]))
|
return commands.Fail(w, fmt.Sprintf("Could not find tunnel for vpn addr: %v", a[0]))
|
||||||
}
|
}
|
||||||
|
|
||||||
enc := json.NewEncoder(w.GetWriter())
|
enc := json.NewEncoder(w.GetWriter())
|
||||||
@@ -715,7 +664,11 @@ func sshPrintTunnel(ifce *Interface, fs any, a []string, w commands.StringWriter
|
|||||||
return enc.Encode(copyHostInfo(hostInfo, ifce.hostMap.GetPreferredRanges()))
|
return enc.Encode(copyHostInfo(hostInfo, ifce.hostMap.GetPreferredRanges()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func sshDeviceInfo(ifce *Interface, fs any, w commands.StringWriter) error {
|
func deviceInfoCommand(ifce *Interface, fs any, w commands.StringWriter) error {
|
||||||
|
flags, ok := fs.(*deviceInfoFlags)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("internal error: expected flags to be deviceInfoFlags but was %+v", fs)
|
||||||
|
}
|
||||||
|
|
||||||
data := struct {
|
data := struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -727,11 +680,6 @@ func sshDeviceInfo(ifce *Interface, fs any, w commands.StringWriter) error {
|
|||||||
|
|
||||||
copy(data.Cidr, ifce.inside.Networks())
|
copy(data.Cidr, ifce.inside.Networks())
|
||||||
|
|
||||||
flags, ok := fs.(*sshDeviceInfoFlags)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("internal error: expected flags to be sshDeviceInfoFlags but was %+v", fs)
|
|
||||||
}
|
|
||||||
|
|
||||||
if flags.Json || flags.Pretty {
|
if flags.Json || flags.Pretty {
|
||||||
js := json.NewEncoder(w.GetWriter())
|
js := json.NewEncoder(w.GetWriter())
|
||||||
if flags.Pretty {
|
if flags.Pretty {
|
||||||
@@ -744,13 +692,19 @@ func sshDeviceInfo(ifce *Interface, fs any, w commands.StringWriter) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// sshReload reloads the config and writes the result to the client. It reloads even if the client has gone away
|
// reloadCommand reloads the config and writes the result to the client. It reloads even if the client has gone away
|
||||||
func sshReload(l *slog.Logger, c *config.C, w commands.StringWriter) error {
|
func reloadCommand(l *slog.Logger, c *config.C, w commands.StringWriter) error {
|
||||||
writeErr := w.WriteLine("Reloading config")
|
writeErr := w.WriteLine("Reloading config")
|
||||||
res := c.ReloadConfig()
|
res := c.ReloadConfig()
|
||||||
res.Log(l)
|
res.Log(l)
|
||||||
if err := w.WriteLine(res.String()); writeErr == nil {
|
if err := w.WriteLine(res.String()); writeErr == nil {
|
||||||
writeErr = err
|
writeErr = err
|
||||||
}
|
}
|
||||||
return writeErr
|
if writeErr != nil {
|
||||||
|
return writeErr
|
||||||
|
}
|
||||||
|
if res.Err != nil || res.Failed {
|
||||||
|
return &commands.Failure{Msg: res.String()}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
+26
-3
@@ -9,12 +9,14 @@ import (
|
|||||||
type CommandFlags func() (*flag.FlagSet, any)
|
type CommandFlags func() (*flag.FlagSet, any)
|
||||||
|
|
||||||
// CommandCallback is called when your command should execute.
|
// CommandCallback is called when your command should execute.
|
||||||
// fs will be a a pointer to the struct provided by Command.Flags callback, if there was one. -h and -help are reserved
|
// fs will be a pointer to the struct provided by Command.Flags callback, if there was one. -h and -help are reserved
|
||||||
// and handled automatically for you.
|
// and handled automatically for you.
|
||||||
// a will be any unconsumed arguments, if no Command.Flags was available this will be all the flags passed in.
|
// a will be any unconsumed arguments, if no Command.Flags was available this will be all the flags passed in.
|
||||||
// w is the writer to use when sending messages back to the client.
|
// w is the writer to use when sending messages back to the client.
|
||||||
// If an error is returned by the callback it is logged locally, the callback should handle messaging errors to the user
|
// An error returned by the callback is handed back to whoever dispatched the command, which reports a failure: exit
|
||||||
// where appropriate
|
// status 1 over ssh. The error is not shown to the user. For a failure the user caused or can act on, write the
|
||||||
|
// message and return it as a *Failure, which Fail does in one step; the surface then reports the failure without
|
||||||
|
// logging what the user was already told. Any other error is logged by the surface as well.
|
||||||
type CommandCallback func(fs any, a []string, w StringWriter) error
|
type CommandCallback func(fs any, a []string, w StringWriter) error
|
||||||
|
|
||||||
type Command struct {
|
type Command struct {
|
||||||
@@ -26,6 +28,27 @@ type Command struct {
|
|||||||
Callback CommandCallback
|
Callback CommandCallback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Failure is the error a command returns for a failure it has already explained to the user:
|
||||||
|
// the message it wrote is the whole story. A surface reports it as a failure and does not
|
||||||
|
// log it.
|
||||||
|
type Failure struct {
|
||||||
|
Msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Failure) Error() string {
|
||||||
|
return f.Msg
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fail writes msg to w and returns it as a *Failure, so a command reports a failure to the
|
||||||
|
// user and to its surface in one line. If the write itself fails, that error is returned
|
||||||
|
// instead: the user was not told anything.
|
||||||
|
func Fail(w StringWriter, msg string) error {
|
||||||
|
if err := w.WriteLine(msg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return &Failure{Msg: msg}
|
||||||
|
}
|
||||||
|
|
||||||
func execCommand(c *Command, args []string, w StringWriter) error {
|
func execCommand(c *Command, args []string, w StringWriter) error {
|
||||||
var (
|
var (
|
||||||
fl *flag.FlagSet
|
fl *flag.FlagSet
|
||||||
|
|||||||
+23
-21
@@ -38,7 +38,8 @@ func (r *Registry) registerHelp() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register adds a command that can be run by a user.
|
// Register adds a command that can be run by a user. A command registered under a name
|
||||||
|
// already held replaces it; see Clone for what that means for Help.
|
||||||
func (r *Registry) Register(c *Command) {
|
func (r *Registry) Register(c *Command) {
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
defer r.mu.Unlock()
|
defer r.mu.Unlock()
|
||||||
@@ -77,29 +78,32 @@ func (r *Registry) All() []*Command {
|
|||||||
return cmds
|
return cmds
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clone returns a copy holding the same commands.
|
// Clone returns a copy holding the same commands, with help bound to the copy: a
|
||||||
|
// command registered on the clone alone is listed and described there, and one the
|
||||||
|
// clone withholds is not. That rebinding replaces whatever was registered under Help on
|
||||||
|
// the source, built-in or not.
|
||||||
func (r *Registry) Clone() *Registry {
|
func (r *Registry) Clone() *Registry {
|
||||||
r.mu.RLock()
|
r.mu.RLock()
|
||||||
defer r.mu.RUnlock()
|
c := &Registry{commands: maps.Clone(r.commands)}
|
||||||
return &Registry{commands: maps.Clone(r.commands)}
|
r.mu.RUnlock()
|
||||||
|
c.registerHelp()
|
||||||
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dispatch parses a command line and runs it, reporting problems to w.
|
// Dispatch parses a command line and runs it, reporting problems to w. The error is for
|
||||||
func (r *Registry) Dispatch(line string, w StringWriter) {
|
// the caller to log, not to show; see CommandCallback.
|
||||||
// A line shlex cannot split, an unbalanced quote, gets no answer. sshd was silent
|
func (r *Registry) Dispatch(line string, w StringWriter) error {
|
||||||
// here too; this is a relocation, so it stays silent.
|
// A line shlex cannot split, an unbalanced quote.
|
||||||
args, err := shlex.Split(line, true)
|
args, err := shlex.Split(line, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return Fail(w, "could not parse the command line: "+err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = r.DispatchArgs(args, line, w)
|
return r.DispatchArgs(args, w)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DispatchArgs runs an already tokenized command line. orig is what the user typed,
|
// DispatchArgs runs an already tokenized command line. The error is as for Dispatch.
|
||||||
// used only for the "did not understand" message; callers that have no raw line may
|
func (r *Registry) DispatchArgs(args []string, w StringWriter) error {
|
||||||
// pass "" and the joined args are used instead.
|
|
||||||
func (r *Registry) DispatchArgs(args []string, orig string, w StringWriter) error {
|
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
r.dumpCommands(w)
|
r.dumpCommands(w)
|
||||||
return nil
|
return nil
|
||||||
@@ -107,16 +111,14 @@ func (r *Registry) DispatchArgs(args []string, orig string, w StringWriter) erro
|
|||||||
|
|
||||||
c := r.Get(Name(args[0]))
|
c := r.Get(Name(args[0]))
|
||||||
if c == nil {
|
if c == nil {
|
||||||
if orig == "" {
|
// Only the first word was looked up, so only it is named.
|
||||||
orig = strings.Join(args, " ")
|
msg := fmt.Sprintf("did not understand: %s", args[0])
|
||||||
}
|
if err := w.WriteLine(msg); err != nil {
|
||||||
|
|
||||||
if err := w.WriteLine(fmt.Sprintf("did not understand: %s", orig)); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
r.dumpCommands(w)
|
r.dumpCommands(w)
|
||||||
return nil
|
return &Failure{Msg: msg}
|
||||||
}
|
}
|
||||||
|
|
||||||
if checkHelpArgs(args) {
|
if checkHelpArgs(args) {
|
||||||
@@ -154,7 +156,7 @@ func (r *Registry) help(a []string, w StringWriter) error {
|
|||||||
// We are printing a specific commands help text
|
// We are printing a specific commands help text
|
||||||
cmd := r.Get(Name(a[0]))
|
cmd := r.Get(Name(a[0]))
|
||||||
if cmd == nil {
|
if cmd == nil {
|
||||||
return w.WriteLine("Command not available " + a[0])
|
return Fail(w, "Command not available "+a[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := w.WriteLine(fmt.Sprintf("%s - %s", cmd.Name, cmd.ShortDescription)); err != nil {
|
if err := w.WriteLine(fmt.Sprintf("%s - %s", cmd.Name, cmd.ShortDescription)); err != nil {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package commands
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -45,18 +46,41 @@ func TestFlagUsageGoesToTheWriter(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
merged := &bytes.Buffer{}
|
merged := &bytes.Buffer{}
|
||||||
require.Error(t, r.DispatchArgs([]string{"emit", "-nope"}, "", NewStringWriter(merged)))
|
require.Error(t, r.DispatchArgs([]string{"emit", "-nope"}, NewStringWriter(merged)))
|
||||||
assert.Contains(t, merged.String(), "flag provided but not defined")
|
assert.Contains(t, merged.String(), "flag provided but not defined")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestDispatchDropsUnparseableLine pins sshd's behaviour for a line shlex rejects: no
|
// TestDispatchReportsUnparseableLine: a line shlex rejects, an unbalanced quote, is a
|
||||||
// answer at all. An operator with an unbalanced quote gets silence, as they always have.
|
// failure the user is told about.
|
||||||
func TestDispatchDropsUnparseableLine(t *testing.T) {
|
func TestDispatchReportsUnparseableLine(t *testing.T) {
|
||||||
out := &bytes.Buffer{}
|
out := &bytes.Buffer{}
|
||||||
testRegistry().Dispatch(`served "unterminated`, NewStringWriter(out))
|
err := testRegistry().Dispatch(`served "unterminated`, NewStringWriter(out))
|
||||||
assert.Empty(t, out.String())
|
|
||||||
|
var f *Failure
|
||||||
|
require.ErrorAs(t, err, &f)
|
||||||
|
assert.Equal(t, f.Msg+"\n", out.String())
|
||||||
|
assert.Contains(t, out.String(), "could not parse the command line: ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestFailTellsTheUserAndTheSurface: Fail writes the message and returns it as a Failure;
|
||||||
|
// when the write fails, the user heard nothing, so the write error is what comes back.
|
||||||
|
func TestFailTellsTheUserAndTheSurface(t *testing.T) {
|
||||||
|
out := &bytes.Buffer{}
|
||||||
|
err := Fail(NewStringWriter(out), "no such tunnel")
|
||||||
|
var f *Failure
|
||||||
|
require.ErrorAs(t, err, &f)
|
||||||
|
assert.Equal(t, "no such tunnel", f.Msg)
|
||||||
|
assert.Equal(t, "no such tunnel\n", out.String())
|
||||||
|
|
||||||
|
err = Fail(NewStringWriter(failingWriter{}), "no such tunnel")
|
||||||
|
require.EqualError(t, err, "gone")
|
||||||
|
assert.NotErrorAs(t, err, &f)
|
||||||
|
}
|
||||||
|
|
||||||
|
type failingWriter struct{}
|
||||||
|
|
||||||
|
func (failingWriter) Write([]byte) (int, error) { return 0, errors.New("gone") }
|
||||||
|
|
||||||
// TestAllSortsByName: registration order does not matter.
|
// TestAllSortsByName: registration order does not matter.
|
||||||
func TestAllSortsByName(t *testing.T) {
|
func TestAllSortsByName(t *testing.T) {
|
||||||
r := New()
|
r := New()
|
||||||
@@ -72,8 +96,8 @@ func TestAllSortsByName(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TestHelpFlagAgreesWithHelpCommand pins that -h answers through the registered help
|
// TestHelpFlagAgreesWithHelpCommand pins that -h answers through the registered help
|
||||||
// command. On a clone, help is bound to the source, so a command registered only on the
|
// command, and that help on a clone describes the clone: a command registered only there
|
||||||
// clone is known to neither -h nor `help <name>`, and the two never disagree.
|
// is known to both -h and `help <name>`, and one withheld from it is known to neither.
|
||||||
func TestHelpFlagAgreesWithHelpCommand(t *testing.T) {
|
func TestHelpFlagAgreesWithHelpCommand(t *testing.T) {
|
||||||
r := testRegistry()
|
r := testRegistry()
|
||||||
c := r.Clone()
|
c := r.Clone()
|
||||||
@@ -88,10 +112,65 @@ func TestHelpFlagAgreesWithHelpCommand(t *testing.T) {
|
|||||||
cmd := &bytes.Buffer{}
|
cmd := &bytes.Buffer{}
|
||||||
c.Dispatch("help logout", NewStringWriter(cmd))
|
c.Dispatch("help logout", NewStringWriter(cmd))
|
||||||
assert.Equal(t, cmd.String(), flag.String())
|
assert.Equal(t, cmd.String(), flag.String())
|
||||||
assert.Equal(t, "Command not available logout\n", flag.String())
|
assert.Equal(t, "logout - ends the session\n", flag.String())
|
||||||
|
|
||||||
|
// The source never learns of it.
|
||||||
|
src := &bytes.Buffer{}
|
||||||
|
r.Dispatch("help logout", NewStringWriter(src))
|
||||||
|
assert.Equal(t, "Command not available logout\n", src.String())
|
||||||
|
|
||||||
// And a command both hold is described either way.
|
// And a command both hold is described either way.
|
||||||
flag.Reset()
|
flag.Reset()
|
||||||
c.Dispatch("served -h", NewStringWriter(flag))
|
c.Dispatch("served -h", NewStringWriter(flag))
|
||||||
assert.Equal(t, "served - a command this surface serves\n", flag.String())
|
assert.Equal(t, "served - a command this surface serves\n", flag.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestCloneHelpListsTheClone: the command list help prints is the clone's, so a clone
|
||||||
|
// that withholds a command does not name it, and the source still does.
|
||||||
|
func TestCloneHelpListsTheClone(t *testing.T) {
|
||||||
|
r := testRegistry()
|
||||||
|
c := New()
|
||||||
|
c.Register(r.Get("served"))
|
||||||
|
c = c.Clone()
|
||||||
|
|
||||||
|
out := &bytes.Buffer{}
|
||||||
|
c.Dispatch("help", NewStringWriter(out))
|
||||||
|
assert.Equal(t, "Available commands:\n"+
|
||||||
|
"help - prints available commands or help <command> for specific usage info\n"+
|
||||||
|
"served - a command this surface serves\n\n", out.String())
|
||||||
|
|
||||||
|
out.Reset()
|
||||||
|
c.Dispatch("help withheld", NewStringWriter(out))
|
||||||
|
assert.Equal(t, "Command not available withheld\n", out.String())
|
||||||
|
|
||||||
|
out.Reset()
|
||||||
|
r.Dispatch("help withheld", NewStringWriter(out))
|
||||||
|
assert.Equal(t, "withheld - a command this surface withholds\n", out.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDispatchReturnsTheCommandsError: what the callback returns reaches the caller, so
|
||||||
|
// a surface can log it.
|
||||||
|
func TestDispatchReturnsTheCommandsError(t *testing.T) {
|
||||||
|
r := New()
|
||||||
|
r.Register(&Command{
|
||||||
|
Name: "fails",
|
||||||
|
Callback: func(fs any, a []string, w StringWriter) error {
|
||||||
|
return errors.New("boom")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
out := &bytes.Buffer{}
|
||||||
|
require.EqualError(t, r.Dispatch("fails", NewStringWriter(out)), "boom")
|
||||||
|
assert.Empty(t, out.String(), "a command's own error is not shown; the command speaks for itself")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestUnknownCommandNamesTheWord: only the first word is looked up, so only it is
|
||||||
|
// reported, followed by the command list, and the line is a failure to the surface.
|
||||||
|
func TestUnknownCommandNamesTheWord(t *testing.T) {
|
||||||
|
out := &bytes.Buffer{}
|
||||||
|
err := testRegistry().Dispatch(`nope "with args"`, NewStringWriter(out))
|
||||||
|
var f *Failure
|
||||||
|
require.ErrorAs(t, err, &f)
|
||||||
|
assert.Equal(t, "did not understand: nope", f.Msg)
|
||||||
|
assert.True(t, strings.HasPrefix(out.String(), "did not understand: nope\nAvailable commands:\n"), out.String())
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package nebula
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/slackhq/nebula/commands"
|
||||||
|
"github.com/slackhq/nebula/logging"
|
||||||
|
"github.com/slackhq/nebula/test"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestLogFormatRefusalIsShown: a format the logger does not know is reported to the user
|
||||||
|
// as a failure, rather than returned bare, which over ssh was silence.
|
||||||
|
func TestLogFormatRefusalIsShown(t *testing.T) {
|
||||||
|
l := logging.NewLogger(io.Discard)
|
||||||
|
out := &bytes.Buffer{}
|
||||||
|
w := commands.NewStringWriter(out)
|
||||||
|
|
||||||
|
var f *commands.Failure
|
||||||
|
require.ErrorAs(t, logFormatCommand(l, nil, []string{"bogus"}, w), &f)
|
||||||
|
assert.Equal(t, "Unable to set log format: unknown log format `bogus`. possible formats: [text json]\n", out.String())
|
||||||
|
|
||||||
|
out.Reset()
|
||||||
|
require.NoError(t, logFormatCommand(l, nil, []string{"JSON"}, w))
|
||||||
|
assert.Equal(t, "Log format is: json\n", out.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPrintRelaysReportsErrorsAsText: the Error field was an error value, which encodes
|
||||||
|
// as {} and lost the message. It is the message now, and absent when there is none.
|
||||||
|
func TestPrintRelaysReportsErrorsAsText(t *testing.T) {
|
||||||
|
l := test.NewLogger()
|
||||||
|
hm := newHostMap(l)
|
||||||
|
relay := netip.MustParseAddr("10.0.0.1")
|
||||||
|
peer := netip.MustParseAddr("10.0.0.2")
|
||||||
|
|
||||||
|
// A relay host that is in Relays but not in Hosts, so its hostinfo cannot be found.
|
||||||
|
hm.Relays[1] = &HostInfo{vpnAddrs: []netip.Addr{relay}}
|
||||||
|
ifce := &Interface{hostMap: hm}
|
||||||
|
|
||||||
|
out := &bytes.Buffer{}
|
||||||
|
require.NoError(t, printRelaysCommand(ifce, &printTunnelFlags{}, nil, commands.NewStringWriter(out)))
|
||||||
|
assert.JSONEq(t, `{"Relays":[{"NebulaAddr":"10.0.0.1","RelayForAddrs":[{"Error":"could not find hostinfo","Type":"","State":"","PeerAddr":"","LocalIndex":0,"RemoteIndex":0,"RelayedThrough":null}]}]}`, out.String())
|
||||||
|
|
||||||
|
// The same relay host, found, relaying for a peer whose index matches: no Error key.
|
||||||
|
hi := hm.Relays[1]
|
||||||
|
r := &Relay{Type: ForwardingType, State: Established, PeerAddr: peer, LocalIndex: 1, RemoteIndex: 2}
|
||||||
|
hi.relayState = RelayState{
|
||||||
|
relayForByAddr: map[netip.Addr]*Relay{peer: r},
|
||||||
|
relayForByIdx: map[uint32]*Relay{r.LocalIndex: r},
|
||||||
|
}
|
||||||
|
hm.Hosts[relay] = hi
|
||||||
|
|
||||||
|
out.Reset()
|
||||||
|
require.NoError(t, printRelaysCommand(ifce, &printTunnelFlags{}, nil, commands.NewStringWriter(out)))
|
||||||
|
assert.JSONEq(t, `{"Relays":[{"NebulaAddr":"10.0.0.1","RelayForAddrs":[{"Type":"forwarding","State":"established","PeerAddr":"10.0.0.2","LocalIndex":1,"RemoteIndex":2,"RelayedThrough":null}]}]}`, out.String())
|
||||||
|
|
||||||
|
// And an index mismatch names it.
|
||||||
|
hm.Relays[7] = hi
|
||||||
|
delete(hm.Relays, 1)
|
||||||
|
out.Reset()
|
||||||
|
require.NoError(t, printRelaysCommand(ifce, &printTunnelFlags{}, nil, commands.NewStringWriter(out)))
|
||||||
|
assert.Contains(t, out.String(), `"Error":"hostmap LocalIndex '7' does not match RelayState LocalIndex"`)
|
||||||
|
}
|
||||||
@@ -374,6 +374,9 @@ func (c *Control) CloseAllTunnels(excludeLighthouses bool) (closed int) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Commands returns the registry the daemon's surfaces serve from. It is the live
|
||||||
|
// registry, not a copy: a command registered on it is served to every ssh session that
|
||||||
|
// starts afterwards. Clone it to run commands without that reach.
|
||||||
func (c *Control) Commands() *commands.Registry {
|
func (c *Control) Commands() *commands.Registry {
|
||||||
return c.commands
|
return c.commands
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/slackhq/nebula/cert"
|
"github.com/slackhq/nebula/cert"
|
||||||
"github.com/slackhq/nebula/cert_test"
|
"github.com/slackhq/nebula/cert_test"
|
||||||
"github.com/slackhq/nebula/commands"
|
"github.com/slackhq/nebula/commands"
|
||||||
|
"github.com/slackhq/nebula/sshd"
|
||||||
"github.com/slackhq/nebula/test"
|
"github.com/slackhq/nebula/test"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -64,16 +65,22 @@ func TestCommandOutput(t *testing.T) {
|
|||||||
control.Start()
|
control.Start()
|
||||||
t.Cleanup(func() { control.Stop() })
|
t.Cleanup(func() { control.Stop() })
|
||||||
|
|
||||||
|
// As an ssh session does: a clone of the daemon's registry with logout added, which
|
||||||
|
// help on the clone lists and describes. There is no session to end here.
|
||||||
reg := control.Commands().Clone()
|
reg := control.Commands().Clone()
|
||||||
|
reg.Register(sshd.LogoutCommand(func() {}))
|
||||||
|
|
||||||
run := func(args ...string) string {
|
// run dispatches as the ssh exec path does and returns the output and the error, which
|
||||||
|
// over ssh is exit status 1.
|
||||||
|
run := func(args ...string) (string, error) {
|
||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
_ = reg.DispatchArgs(args, strings.Join(args, " "), commands.NewStringWriter(buf))
|
err := reg.DispatchArgs(args, commands.NewStringWriter(buf))
|
||||||
return buf.String()
|
return buf.String(), err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seed a pending tunnel so the listing and per-address commands have something to show.
|
// Seed a pending tunnel so the listing and per-address commands have something to show.
|
||||||
seed := run("create-tunnel", "10.222.0.9")
|
seed, err := run("create-tunnel", "10.222.0.9")
|
||||||
|
require.NoError(t, err)
|
||||||
require.Equal(t, "Created\n", seed, "seeding a tunnel should succeed before anything else is recorded")
|
require.Equal(t, "Created\n", seed, "seeding a tunnel should succeed before anything else is recorded")
|
||||||
|
|
||||||
cases := []outputCase{
|
cases := []outputCase{
|
||||||
@@ -120,6 +127,9 @@ func TestCommandOutput(t *testing.T) {
|
|||||||
{"close-tunnel", []string{"not-an-address"}},
|
{"close-tunnel", []string{"not-an-address"}},
|
||||||
{"help", nil},
|
{"help", nil},
|
||||||
{"help", []string{"print-cert"}},
|
{"help", []string{"print-cert"}},
|
||||||
|
{"help", []string{"logout"}},
|
||||||
|
{"logout", []string{"-h"}},
|
||||||
|
{"not-a-command", []string{"10.222.0.9"}},
|
||||||
// reload is last so that whatever a reload does to the daemon cannot leak into an
|
// reload is last so that whatever a reload does to the daemon cannot leak into an
|
||||||
// earlier case. In this harness it fails: the config was loaded from a string and
|
// earlier case. In this harness it fails: the config was loaded from a string and
|
||||||
// has no path to re-read, which is what the recorded result says.
|
// has no path to re-read, which is what the recorded result says.
|
||||||
@@ -133,13 +143,19 @@ func TestCommandOutput(t *testing.T) {
|
|||||||
args = append(args, c.args...)
|
args = append(args, c.args...)
|
||||||
|
|
||||||
fmt.Fprintf(got, "$ %s\n", strings.Join(args, " "))
|
fmt.Fprintf(got, "$ %s\n", strings.Join(args, " "))
|
||||||
out := normalise(run(args...))
|
out, err := run(args...)
|
||||||
|
out = normalise(out)
|
||||||
got.WriteString(out)
|
got.WriteString(out)
|
||||||
// Output that does not end in a newline is recorded as such, the way diff marks
|
// Output that does not end in a newline is recorded as such, the way diff marks
|
||||||
// it, so the golden sees that byte too and the next prompt still starts a line.
|
// it, so the golden sees that byte too and the next prompt still starts a line.
|
||||||
if out != "" && !strings.HasSuffix(out, "\n") {
|
if out != "" && !strings.HasSuffix(out, "\n") {
|
||||||
got.WriteString("\\ No newline at end of output\n")
|
got.WriteString("\\ No newline at end of output\n")
|
||||||
}
|
}
|
||||||
|
// A failed command is recorded as the exit status an ssh caller sees, so the
|
||||||
|
// golden pins which outcomes count as failures.
|
||||||
|
if err != nil {
|
||||||
|
got.WriteString("exit status 1\n")
|
||||||
|
}
|
||||||
got.WriteString("\n")
|
got.WriteString("\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+177
@@ -158,66 +158,84 @@ $ print-cert -pretty -raw
|
|||||||
|
|
||||||
$ print-cert 10.222.0.9
|
$ print-cert 10.222.0.9
|
||||||
Could not find tunnel for vpn addr: 10.222.0.9
|
Could not find tunnel for vpn addr: 10.222.0.9
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ print-cert -json 10.222.0.9
|
$ print-cert -json 10.222.0.9
|
||||||
Could not find tunnel for vpn addr: 10.222.0.9
|
Could not find tunnel for vpn addr: 10.222.0.9
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ print-cert -pretty 10.222.0.9
|
$ print-cert -pretty 10.222.0.9
|
||||||
Could not find tunnel for vpn addr: 10.222.0.9
|
Could not find tunnel for vpn addr: 10.222.0.9
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ print-cert 10.222.0.250
|
$ print-cert 10.222.0.250
|
||||||
Could not find tunnel for vpn addr: 10.222.0.250
|
Could not find tunnel for vpn addr: 10.222.0.250
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ print-cert -json 10.222.0.250
|
$ print-cert -json 10.222.0.250
|
||||||
Could not find tunnel for vpn addr: 10.222.0.250
|
Could not find tunnel for vpn addr: 10.222.0.250
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ print-cert -pretty 10.222.0.250
|
$ print-cert -pretty 10.222.0.250
|
||||||
Could not find tunnel for vpn addr: 10.222.0.250
|
Could not find tunnel for vpn addr: 10.222.0.250
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ print-cert not-an-address
|
$ print-cert not-an-address
|
||||||
The provided vpn addr could not be parsed: not-an-address
|
The provided vpn addr could not be parsed: not-an-address
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ print-cert -json not-an-address
|
$ print-cert -json not-an-address
|
||||||
The provided vpn addr could not be parsed: not-an-address
|
The provided vpn addr could not be parsed: not-an-address
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ print-cert -pretty not-an-address
|
$ print-cert -pretty not-an-address
|
||||||
The provided vpn addr could not be parsed: not-an-address
|
The provided vpn addr could not be parsed: not-an-address
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ print-tunnel
|
$ print-tunnel
|
||||||
No vpn address was provided
|
No vpn address was provided
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ print-tunnel -json
|
$ print-tunnel -json
|
||||||
flag provided but not defined: -json
|
flag provided but not defined: -json
|
||||||
Usage:
|
Usage:
|
||||||
-pretty
|
-pretty
|
||||||
pretty prints json
|
pretty prints json
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ print-tunnel -pretty
|
$ print-tunnel -pretty
|
||||||
No vpn address was provided
|
No vpn address was provided
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ print-tunnel 10.222.0.9
|
$ print-tunnel 10.222.0.9
|
||||||
Could not find tunnel for vpn addr: 10.222.0.9
|
Could not find tunnel for vpn addr: 10.222.0.9
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ print-tunnel -json 10.222.0.9
|
$ print-tunnel -json 10.222.0.9
|
||||||
flag provided but not defined: -json
|
flag provided but not defined: -json
|
||||||
Usage:
|
Usage:
|
||||||
-pretty
|
-pretty
|
||||||
pretty prints json
|
pretty prints json
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ print-tunnel -pretty 10.222.0.9
|
$ print-tunnel -pretty 10.222.0.9
|
||||||
Could not find tunnel for vpn addr: 10.222.0.9
|
Could not find tunnel for vpn addr: 10.222.0.9
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ print-tunnel 10.222.0.250
|
$ print-tunnel 10.222.0.250
|
||||||
Could not find tunnel for vpn addr: 10.222.0.250
|
Could not find tunnel for vpn addr: 10.222.0.250
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ print-tunnel -json 10.222.0.250
|
$ print-tunnel -json 10.222.0.250
|
||||||
flag provided but not defined: -json
|
flag provided but not defined: -json
|
||||||
Usage:
|
Usage:
|
||||||
-pretty
|
-pretty
|
||||||
pretty prints json
|
pretty prints json
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ print-tunnel -pretty 10.222.0.250
|
$ print-tunnel -pretty 10.222.0.250
|
||||||
Could not find tunnel for vpn addr: 10.222.0.250
|
Could not find tunnel for vpn addr: 10.222.0.250
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ print-relays
|
$ print-relays
|
||||||
{"Relays":null}
|
{"Relays":null}
|
||||||
@@ -227,6 +245,7 @@ flag provided but not defined: -json
|
|||||||
Usage:
|
Usage:
|
||||||
-pretty
|
-pretty
|
||||||
pretty prints json
|
pretty prints json
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ print-relays -pretty
|
$ print-relays -pretty
|
||||||
{
|
{
|
||||||
@@ -235,183 +254,230 @@ $ print-relays -pretty
|
|||||||
|
|
||||||
$ query-lighthouse
|
$ query-lighthouse
|
||||||
No vpn address was provided
|
No vpn address was provided
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ query-lighthouse -json
|
$ query-lighthouse -json
|
||||||
The provided vpn address could not be parsed: -json
|
The provided vpn address could not be parsed: -json
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ query-lighthouse -pretty
|
$ query-lighthouse -pretty
|
||||||
The provided vpn address could not be parsed: -pretty
|
The provided vpn address could not be parsed: -pretty
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ query-lighthouse 10.222.0.9
|
$ query-lighthouse 10.222.0.9
|
||||||
{}
|
{}
|
||||||
|
|
||||||
$ query-lighthouse -json 10.222.0.9
|
$ query-lighthouse -json 10.222.0.9
|
||||||
The provided vpn address could not be parsed: -json
|
The provided vpn address could not be parsed: -json
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ query-lighthouse -pretty 10.222.0.9
|
$ query-lighthouse -pretty 10.222.0.9
|
||||||
The provided vpn address could not be parsed: -pretty
|
The provided vpn address could not be parsed: -pretty
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ query-lighthouse not-an-address
|
$ query-lighthouse not-an-address
|
||||||
The provided vpn address could not be parsed: not-an-address
|
The provided vpn address could not be parsed: not-an-address
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ query-lighthouse -json not-an-address
|
$ query-lighthouse -json not-an-address
|
||||||
The provided vpn address could not be parsed: -json
|
The provided vpn address could not be parsed: -json
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ query-lighthouse -pretty not-an-address
|
$ query-lighthouse -pretty not-an-address
|
||||||
The provided vpn address could not be parsed: -pretty
|
The provided vpn address could not be parsed: -pretty
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ log-level
|
$ log-level
|
||||||
Log level is not reconfigurable on this logger
|
Log level is not reconfigurable on this logger
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ log-level -json
|
$ log-level -json
|
||||||
Log level is not reconfigurable on this logger
|
Log level is not reconfigurable on this logger
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ log-level -pretty
|
$ log-level -pretty
|
||||||
Log level is not reconfigurable on this logger
|
Log level is not reconfigurable on this logger
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ log-level debug
|
$ log-level debug
|
||||||
Log level is not reconfigurable on this logger
|
Log level is not reconfigurable on this logger
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ log-level -json debug
|
$ log-level -json debug
|
||||||
Log level is not reconfigurable on this logger
|
Log level is not reconfigurable on this logger
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ log-level -pretty debug
|
$ log-level -pretty debug
|
||||||
Log level is not reconfigurable on this logger
|
Log level is not reconfigurable on this logger
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ log-level bogus
|
$ log-level bogus
|
||||||
Log level is not reconfigurable on this logger
|
Log level is not reconfigurable on this logger
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ log-level -json bogus
|
$ log-level -json bogus
|
||||||
Log level is not reconfigurable on this logger
|
Log level is not reconfigurable on this logger
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ log-level -pretty bogus
|
$ log-level -pretty bogus
|
||||||
Log level is not reconfigurable on this logger
|
Log level is not reconfigurable on this logger
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ log-level info
|
$ log-level info
|
||||||
Log level is not reconfigurable on this logger
|
Log level is not reconfigurable on this logger
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ log-level -json info
|
$ log-level -json info
|
||||||
Log level is not reconfigurable on this logger
|
Log level is not reconfigurable on this logger
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ log-level -pretty info
|
$ log-level -pretty info
|
||||||
Log level is not reconfigurable on this logger
|
Log level is not reconfigurable on this logger
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ log-format
|
$ log-format
|
||||||
Log format is not reconfigurable on this logger
|
Log format is not reconfigurable on this logger
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ log-format -json
|
$ log-format -json
|
||||||
Log format is not reconfigurable on this logger
|
Log format is not reconfigurable on this logger
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ log-format -pretty
|
$ log-format -pretty
|
||||||
Log format is not reconfigurable on this logger
|
Log format is not reconfigurable on this logger
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ log-format json
|
$ log-format json
|
||||||
Log format is not reconfigurable on this logger
|
Log format is not reconfigurable on this logger
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ log-format -json json
|
$ log-format -json json
|
||||||
Log format is not reconfigurable on this logger
|
Log format is not reconfigurable on this logger
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ log-format -pretty json
|
$ log-format -pretty json
|
||||||
Log format is not reconfigurable on this logger
|
Log format is not reconfigurable on this logger
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ log-format text
|
$ log-format text
|
||||||
Log format is not reconfigurable on this logger
|
Log format is not reconfigurable on this logger
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ log-format -json text
|
$ log-format -json text
|
||||||
Log format is not reconfigurable on this logger
|
Log format is not reconfigurable on this logger
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ log-format -pretty text
|
$ log-format -pretty text
|
||||||
Log format is not reconfigurable on this logger
|
Log format is not reconfigurable on this logger
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ change-remote
|
$ change-remote
|
||||||
No vpn address was provided
|
No vpn address was provided
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ change-remote -json
|
$ change-remote -json
|
||||||
flag provided but not defined: -json
|
flag provided but not defined: -json
|
||||||
Usage:
|
Usage:
|
||||||
-address string
|
-address string
|
||||||
The new remote address, ip:port
|
The new remote address, ip:port
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ change-remote -pretty
|
$ change-remote -pretty
|
||||||
flag provided but not defined: -pretty
|
flag provided but not defined: -pretty
|
||||||
Usage:
|
Usage:
|
||||||
-address string
|
-address string
|
||||||
The new remote address, ip:port
|
The new remote address, ip:port
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ change-remote 10.222.0.9
|
$ change-remote 10.222.0.9
|
||||||
No address was provided
|
No address was provided
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ change-remote -json 10.222.0.9
|
$ change-remote -json 10.222.0.9
|
||||||
flag provided but not defined: -json
|
flag provided but not defined: -json
|
||||||
Usage:
|
Usage:
|
||||||
-address string
|
-address string
|
||||||
The new remote address, ip:port
|
The new remote address, ip:port
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ change-remote -pretty 10.222.0.9
|
$ change-remote -pretty 10.222.0.9
|
||||||
flag provided but not defined: -pretty
|
flag provided but not defined: -pretty
|
||||||
Usage:
|
Usage:
|
||||||
-address string
|
-address string
|
||||||
The new remote address, ip:port
|
The new remote address, ip:port
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ change-remote -address 10.94.0.10:4242 10.222.0.9
|
$ change-remote -address 10.94.0.10:4242 10.222.0.9
|
||||||
Could not find tunnel for vpn address: 10.222.0.9
|
Could not find tunnel for vpn address: 10.222.0.9
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ change-remote -json -address 10.94.0.10:4242 10.222.0.9
|
$ change-remote -json -address 10.94.0.10:4242 10.222.0.9
|
||||||
flag provided but not defined: -json
|
flag provided but not defined: -json
|
||||||
Usage:
|
Usage:
|
||||||
-address string
|
-address string
|
||||||
The new remote address, ip:port
|
The new remote address, ip:port
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ change-remote -pretty -address 10.94.0.10:4242 10.222.0.9
|
$ change-remote -pretty -address 10.94.0.10:4242 10.222.0.9
|
||||||
flag provided but not defined: -pretty
|
flag provided but not defined: -pretty
|
||||||
Usage:
|
Usage:
|
||||||
-address string
|
-address string
|
||||||
The new remote address, ip:port
|
The new remote address, ip:port
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ change-remote -address 10.94.0.10:4242 10.222.0.250
|
$ change-remote -address 10.94.0.10:4242 10.222.0.250
|
||||||
Could not find tunnel for vpn address: 10.222.0.250
|
Could not find tunnel for vpn address: 10.222.0.250
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ change-remote -json -address 10.94.0.10:4242 10.222.0.250
|
$ change-remote -json -address 10.94.0.10:4242 10.222.0.250
|
||||||
flag provided but not defined: -json
|
flag provided but not defined: -json
|
||||||
Usage:
|
Usage:
|
||||||
-address string
|
-address string
|
||||||
The new remote address, ip:port
|
The new remote address, ip:port
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ change-remote -pretty -address 10.94.0.10:4242 10.222.0.250
|
$ change-remote -pretty -address 10.94.0.10:4242 10.222.0.250
|
||||||
flag provided but not defined: -pretty
|
flag provided but not defined: -pretty
|
||||||
Usage:
|
Usage:
|
||||||
-address string
|
-address string
|
||||||
The new remote address, ip:port
|
The new remote address, ip:port
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ create-tunnel
|
$ create-tunnel
|
||||||
No vpn address was provided
|
No vpn address was provided
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ create-tunnel -json
|
$ create-tunnel -json
|
||||||
flag provided but not defined: -json
|
flag provided but not defined: -json
|
||||||
Usage:
|
Usage:
|
||||||
-address string
|
-address string
|
||||||
Optionally provide a real remote address, ip:port
|
Optionally provide a real remote address, ip:port
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ create-tunnel -pretty
|
$ create-tunnel -pretty
|
||||||
flag provided but not defined: -pretty
|
flag provided but not defined: -pretty
|
||||||
Usage:
|
Usage:
|
||||||
-address string
|
-address string
|
||||||
Optionally provide a real remote address, ip:port
|
Optionally provide a real remote address, ip:port
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ create-tunnel 10.222.0.9
|
$ create-tunnel 10.222.0.9
|
||||||
Tunnel already handshaking
|
Tunnel already handshaking
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ create-tunnel -json 10.222.0.9
|
$ create-tunnel -json 10.222.0.9
|
||||||
flag provided but not defined: -json
|
flag provided but not defined: -json
|
||||||
Usage:
|
Usage:
|
||||||
-address string
|
-address string
|
||||||
Optionally provide a real remote address, ip:port
|
Optionally provide a real remote address, ip:port
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ create-tunnel -pretty 10.222.0.9
|
$ create-tunnel -pretty 10.222.0.9
|
||||||
flag provided but not defined: -pretty
|
flag provided but not defined: -pretty
|
||||||
Usage:
|
Usage:
|
||||||
-address string
|
-address string
|
||||||
Optionally provide a real remote address, ip:port
|
Optionally provide a real remote address, ip:port
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ create-tunnel -address 10.94.0.10:4242 10.222.0.10
|
$ create-tunnel -address 10.94.0.10:4242 10.222.0.10
|
||||||
Created
|
Created
|
||||||
@@ -421,72 +487,86 @@ flag provided but not defined: -json
|
|||||||
Usage:
|
Usage:
|
||||||
-address string
|
-address string
|
||||||
Optionally provide a real remote address, ip:port
|
Optionally provide a real remote address, ip:port
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ create-tunnel -pretty -address 10.94.0.10:4242 10.222.0.10
|
$ create-tunnel -pretty -address 10.94.0.10:4242 10.222.0.10
|
||||||
flag provided but not defined: -pretty
|
flag provided but not defined: -pretty
|
||||||
Usage:
|
Usage:
|
||||||
-address string
|
-address string
|
||||||
Optionally provide a real remote address, ip:port
|
Optionally provide a real remote address, ip:port
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ create-tunnel not-an-address
|
$ create-tunnel not-an-address
|
||||||
The provided vpn address could not be parsed: not-an-address
|
The provided vpn address could not be parsed: not-an-address
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ create-tunnel -json not-an-address
|
$ create-tunnel -json not-an-address
|
||||||
flag provided but not defined: -json
|
flag provided but not defined: -json
|
||||||
Usage:
|
Usage:
|
||||||
-address string
|
-address string
|
||||||
Optionally provide a real remote address, ip:port
|
Optionally provide a real remote address, ip:port
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ create-tunnel -pretty not-an-address
|
$ create-tunnel -pretty not-an-address
|
||||||
flag provided but not defined: -pretty
|
flag provided but not defined: -pretty
|
||||||
Usage:
|
Usage:
|
||||||
-address string
|
-address string
|
||||||
Optionally provide a real remote address, ip:port
|
Optionally provide a real remote address, ip:port
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ close-tunnel
|
$ close-tunnel
|
||||||
No vpn address was provided
|
No vpn address was provided
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ close-tunnel -json
|
$ close-tunnel -json
|
||||||
flag provided but not defined: -json
|
flag provided but not defined: -json
|
||||||
Usage:
|
Usage:
|
||||||
-local-only
|
-local-only
|
||||||
Disables notifying the remote that the tunnel is shutting down
|
Disables notifying the remote that the tunnel is shutting down
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ close-tunnel -pretty
|
$ close-tunnel -pretty
|
||||||
flag provided but not defined: -pretty
|
flag provided but not defined: -pretty
|
||||||
Usage:
|
Usage:
|
||||||
-local-only
|
-local-only
|
||||||
Disables notifying the remote that the tunnel is shutting down
|
Disables notifying the remote that the tunnel is shutting down
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ close-tunnel 10.222.0.250
|
$ close-tunnel 10.222.0.250
|
||||||
Could not find tunnel for vpn address: 10.222.0.250
|
Could not find tunnel for vpn address: 10.222.0.250
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ close-tunnel -json 10.222.0.250
|
$ close-tunnel -json 10.222.0.250
|
||||||
flag provided but not defined: -json
|
flag provided but not defined: -json
|
||||||
Usage:
|
Usage:
|
||||||
-local-only
|
-local-only
|
||||||
Disables notifying the remote that the tunnel is shutting down
|
Disables notifying the remote that the tunnel is shutting down
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ close-tunnel -pretty 10.222.0.250
|
$ close-tunnel -pretty 10.222.0.250
|
||||||
flag provided but not defined: -pretty
|
flag provided but not defined: -pretty
|
||||||
Usage:
|
Usage:
|
||||||
-local-only
|
-local-only
|
||||||
Disables notifying the remote that the tunnel is shutting down
|
Disables notifying the remote that the tunnel is shutting down
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ close-tunnel not-an-address
|
$ close-tunnel not-an-address
|
||||||
The provided vpn address could not be parsed: not-an-address
|
The provided vpn address could not be parsed: not-an-address
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ close-tunnel -json not-an-address
|
$ close-tunnel -json not-an-address
|
||||||
flag provided but not defined: -json
|
flag provided but not defined: -json
|
||||||
Usage:
|
Usage:
|
||||||
-local-only
|
-local-only
|
||||||
Disables notifying the remote that the tunnel is shutting down
|
Disables notifying the remote that the tunnel is shutting down
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ close-tunnel -pretty not-an-address
|
$ close-tunnel -pretty not-an-address
|
||||||
flag provided but not defined: -pretty
|
flag provided but not defined: -pretty
|
||||||
Usage:
|
Usage:
|
||||||
-local-only
|
-local-only
|
||||||
Disables notifying the remote that the tunnel is shutting down
|
Disables notifying the remote that the tunnel is shutting down
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ help
|
$ help
|
||||||
Available commands:
|
Available commands:
|
||||||
@@ -500,6 +580,7 @@ list-lighthouse-addrmap - List all lighthouse map entries
|
|||||||
list-pending-hostmap - List all handshaking hosts
|
list-pending-hostmap - List all handshaking hosts
|
||||||
log-format - Gets or sets the current log format
|
log-format - Gets or sets the current log format
|
||||||
log-level - Gets or sets the current log level
|
log-level - Gets or sets the current log level
|
||||||
|
logout - Ends the current session
|
||||||
print-cert - Prints the current certificate being used or the certificate for the provided vpn addr
|
print-cert - Prints the current certificate being used or the certificate for the provided vpn addr
|
||||||
print-relays - Prints json details about all relay info
|
print-relays - Prints json details about all relay info
|
||||||
print-tunnel - Prints json details about a tunnel for the provided vpn addr
|
print-tunnel - Prints json details about a tunnel for the provided vpn addr
|
||||||
@@ -510,9 +591,11 @@ version - Prints the currently running version of nebula
|
|||||||
|
|
||||||
$ help -json
|
$ help -json
|
||||||
Command not available -json
|
Command not available -json
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ help -pretty
|
$ help -pretty
|
||||||
Command not available -pretty
|
Command not available -pretty
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ help print-cert
|
$ help print-cert
|
||||||
print-cert - Prints the current certificate being used or the certificate for the provided vpn addr
|
print-cert - Prints the current certificate being used or the certificate for the provided vpn addr
|
||||||
@@ -525,19 +608,113 @@ print-cert - Prints the current certificate being used or the certificate for th
|
|||||||
|
|
||||||
$ help -json print-cert
|
$ help -json print-cert
|
||||||
Command not available -json
|
Command not available -json
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ help -pretty print-cert
|
$ help -pretty print-cert
|
||||||
Command not available -pretty
|
Command not available -pretty
|
||||||
|
exit status 1
|
||||||
|
|
||||||
|
$ help logout
|
||||||
|
logout - Ends the current session
|
||||||
|
|
||||||
|
$ help -json logout
|
||||||
|
Command not available -json
|
||||||
|
exit status 1
|
||||||
|
|
||||||
|
$ help -pretty logout
|
||||||
|
Command not available -pretty
|
||||||
|
exit status 1
|
||||||
|
|
||||||
|
$ logout -h
|
||||||
|
logout - Ends the current session
|
||||||
|
|
||||||
|
$ logout -json -h
|
||||||
|
logout - Ends the current session
|
||||||
|
|
||||||
|
$ logout -pretty -h
|
||||||
|
logout - Ends the current session
|
||||||
|
|
||||||
|
$ not-a-command 10.222.0.9
|
||||||
|
did not understand: not-a-command
|
||||||
|
Available commands:
|
||||||
|
change-remote - Changes the remote address used in the tunnel for the provided vpn addr
|
||||||
|
close-tunnel - Closes a tunnel for the provided vpn addr
|
||||||
|
create-tunnel - Creates a tunnel for the provided vpn address
|
||||||
|
device-info - Prints information about the network device.
|
||||||
|
help - prints available commands or help <command> for specific usage info
|
||||||
|
list-hostmap - List all known previously connected hosts
|
||||||
|
list-lighthouse-addrmap - List all lighthouse map entries
|
||||||
|
list-pending-hostmap - List all handshaking hosts
|
||||||
|
log-format - Gets or sets the current log format
|
||||||
|
log-level - Gets or sets the current log level
|
||||||
|
logout - Ends the current session
|
||||||
|
print-cert - Prints the current certificate being used or the certificate for the provided vpn addr
|
||||||
|
print-relays - Prints json details about all relay info
|
||||||
|
print-tunnel - Prints json details about a tunnel for the provided vpn addr
|
||||||
|
query-lighthouse - Query the lighthouses for the provided vpn address
|
||||||
|
reload - Reloads configuration from disk, same as sending HUP to the process
|
||||||
|
version - Prints the currently running version of nebula
|
||||||
|
|
||||||
|
exit status 1
|
||||||
|
|
||||||
|
$ not-a-command -json 10.222.0.9
|
||||||
|
did not understand: not-a-command
|
||||||
|
Available commands:
|
||||||
|
change-remote - Changes the remote address used in the tunnel for the provided vpn addr
|
||||||
|
close-tunnel - Closes a tunnel for the provided vpn addr
|
||||||
|
create-tunnel - Creates a tunnel for the provided vpn address
|
||||||
|
device-info - Prints information about the network device.
|
||||||
|
help - prints available commands or help <command> for specific usage info
|
||||||
|
list-hostmap - List all known previously connected hosts
|
||||||
|
list-lighthouse-addrmap - List all lighthouse map entries
|
||||||
|
list-pending-hostmap - List all handshaking hosts
|
||||||
|
log-format - Gets or sets the current log format
|
||||||
|
log-level - Gets or sets the current log level
|
||||||
|
logout - Ends the current session
|
||||||
|
print-cert - Prints the current certificate being used or the certificate for the provided vpn addr
|
||||||
|
print-relays - Prints json details about all relay info
|
||||||
|
print-tunnel - Prints json details about a tunnel for the provided vpn addr
|
||||||
|
query-lighthouse - Query the lighthouses for the provided vpn address
|
||||||
|
reload - Reloads configuration from disk, same as sending HUP to the process
|
||||||
|
version - Prints the currently running version of nebula
|
||||||
|
|
||||||
|
exit status 1
|
||||||
|
|
||||||
|
$ not-a-command -pretty 10.222.0.9
|
||||||
|
did not understand: not-a-command
|
||||||
|
Available commands:
|
||||||
|
change-remote - Changes the remote address used in the tunnel for the provided vpn addr
|
||||||
|
close-tunnel - Closes a tunnel for the provided vpn addr
|
||||||
|
create-tunnel - Creates a tunnel for the provided vpn address
|
||||||
|
device-info - Prints information about the network device.
|
||||||
|
help - prints available commands or help <command> for specific usage info
|
||||||
|
list-hostmap - List all known previously connected hosts
|
||||||
|
list-lighthouse-addrmap - List all lighthouse map entries
|
||||||
|
list-pending-hostmap - List all handshaking hosts
|
||||||
|
log-format - Gets or sets the current log format
|
||||||
|
log-level - Gets or sets the current log level
|
||||||
|
logout - Ends the current session
|
||||||
|
print-cert - Prints the current certificate being used or the certificate for the provided vpn addr
|
||||||
|
print-relays - Prints json details about all relay info
|
||||||
|
print-tunnel - Prints json details about a tunnel for the provided vpn addr
|
||||||
|
query-lighthouse - Query the lighthouses for the provided vpn address
|
||||||
|
reload - Reloads configuration from disk, same as sending HUP to the process
|
||||||
|
version - Prints the currently running version of nebula
|
||||||
|
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ reload
|
$ reload
|
||||||
Reloading config
|
Reloading config
|
||||||
Config reload failed, the new config did not load: no config files found at
|
Config reload failed, the new config did not load: no config files found at
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ reload -json
|
$ reload -json
|
||||||
Reloading config
|
Reloading config
|
||||||
Config reload failed, the new config did not load: no config files found at
|
Config reload failed, the new config did not load: no config files found at
|
||||||
|
exit status 1
|
||||||
|
|
||||||
$ reload -pretty
|
$ reload -pretty
|
||||||
Reloading config
|
Reloading config
|
||||||
Config reload failed, the new config did not load: no config files found at
|
Config reload failed, the new config did not load: no config files found at
|
||||||
|
exit status 1
|
||||||
|
|
||||||
+20
-9
@@ -7,6 +7,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/slackhq/nebula/commands"
|
||||||
"github.com/slackhq/nebula/config"
|
"github.com/slackhq/nebula/config"
|
||||||
"github.com/slackhq/nebula/test"
|
"github.com/slackhq/nebula/test"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -26,7 +27,7 @@ func (w *lineWriter) Write(s string) error { return w.WriteLine(s) }
|
|||||||
func (w *lineWriter) WriteBytes(b []byte) error { return w.WriteLine(string(b)) }
|
func (w *lineWriter) WriteBytes(b []byte) error { return w.WriteLine(string(b)) }
|
||||||
func (w *lineWriter) GetWriter() io.Writer { return io.Discard }
|
func (w *lineWriter) GetWriter() io.Writer { return io.Discard }
|
||||||
|
|
||||||
func TestSSHReloadReportsTheResult(t *testing.T) {
|
func TestReloadCommandReportsTheResult(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
write := func(body string) {
|
write := func(body string) {
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "config.yml"), []byte(body), 0o600))
|
require.NoError(t, os.WriteFile(filepath.Join(dir, "config.yml"), []byte(body), 0o600))
|
||||||
@@ -43,32 +44,42 @@ func TestSSHReloadReportsTheResult(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
reload := func() []string {
|
// reload returns what the client was told. A reload that failed is a commands.Failure
|
||||||
|
// to the surface, exit 1 over ssh, with the result line as its message; one that
|
||||||
|
// merely needs a restart is not.
|
||||||
|
reload := func(failed bool) []string {
|
||||||
w := &lineWriter{}
|
w := &lineWriter{}
|
||||||
require.NoError(t, sshReload(l, c, w))
|
err := reloadCommand(l, c, w)
|
||||||
|
if failed {
|
||||||
|
var f *commands.Failure
|
||||||
|
require.ErrorAs(t, err, &f)
|
||||||
|
assert.Equal(t, w.lines[len(w.lines)-1], f.Msg)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
return w.lines
|
return w.lines
|
||||||
}
|
}
|
||||||
|
|
||||||
write("a: 2\nb: 1")
|
write("a: 2\nb: 1")
|
||||||
assert.Equal(t, []string{"Reloading config", "Config reload complete"}, reload())
|
assert.Equal(t, []string{"Reloading config", "Config reload complete"}, reload(false))
|
||||||
|
|
||||||
fail = true
|
fail = true
|
||||||
write("a: 3\nb: 1")
|
write("a: 3\nb: 1")
|
||||||
assert.Equal(t, []string{"Reloading config", "Config reload completed with errors, review the logs"}, reload())
|
assert.Equal(t, []string{"Reloading config", "Config reload completed with errors, review the logs"}, reload(true))
|
||||||
|
|
||||||
fail = false
|
fail = false
|
||||||
write("a: 3\nb: 2")
|
write("a: 3\nb: 2")
|
||||||
assert.Equal(t, []string{"Reloading config", "Config reload was completed but requires a restart to fully apply, keys [b]"}, reload())
|
assert.Equal(t, []string{"Reloading config", "Config reload was completed but requires a restart to fully apply, keys [b]"}, reload(false))
|
||||||
|
|
||||||
write("a: [")
|
write("a: [")
|
||||||
lines := reload()
|
lines := reload(true)
|
||||||
require.Len(t, lines, 2)
|
require.Len(t, lines, 2)
|
||||||
assert.Contains(t, lines[1], "Config reload failed, the new config did not load")
|
assert.Contains(t, lines[1], "Config reload failed, the new config did not load")
|
||||||
assert.Contains(t, lines[1], dir, "names the config that did not load")
|
assert.Contains(t, lines[1], dir, "names the config that did not load")
|
||||||
}
|
}
|
||||||
|
|
||||||
// A client that went away still gets the reload it asked for
|
// A client that went away still gets the reload it asked for
|
||||||
func TestSSHReloadReloadsWhenTheClientIsGone(t *testing.T) {
|
func TestReloadCommandReloadsWhenTheClientIsGone(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "config.yml"), []byte("a: 1"), 0o600))
|
require.NoError(t, os.WriteFile(filepath.Join(dir, "config.yml"), []byte("a: 1"), 0o600))
|
||||||
l := test.NewLogger()
|
l := test.NewLogger()
|
||||||
@@ -78,6 +89,6 @@ func TestSSHReloadReloadsWhenTheClientIsGone(t *testing.T) {
|
|||||||
c.RegisterReloadCallback(func(*config.C, *config.Reload) { reloaded = true })
|
c.RegisterReloadCallback(func(*config.C, *config.Reload) { reloaded = true })
|
||||||
|
|
||||||
gone := errors.New("client is gone")
|
gone := errors.New("client is gone")
|
||||||
require.ErrorIs(t, sshReload(l, c, &lineWriter{err: gone}), gone)
|
require.ErrorIs(t, reloadCommand(l, c, &lineWriter{err: gone}), gone)
|
||||||
assert.True(t, reloaded)
|
assert.True(t, reloaded)
|
||||||
}
|
}
|
||||||
+34
-11
@@ -1,6 +1,7 @@
|
|||||||
package sshd
|
package sshd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -25,19 +26,25 @@ func NewSession(reg *commands.Registry, conn *ssh.ServerConn, chans <-chan ssh.N
|
|||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
}
|
}
|
||||||
|
|
||||||
s.commands.Register(&commands.Command{
|
s.commands.Register(LogoutCommand(s.Close))
|
||||||
Name: commands.Logout,
|
|
||||||
ShortDescription: "Ends the current session",
|
|
||||||
Callback: func(a any, args []string, w commands.StringWriter) error {
|
|
||||||
s.Close()
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
go s.handleChannels(chans)
|
go s.handleChannels(chans)
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LogoutCommand is the logout command a session registers on its clone of the registry,
|
||||||
|
// with end as what it does. Exported so a test can serve what a session serves.
|
||||||
|
func LogoutCommand(end func()) *commands.Command {
|
||||||
|
return &commands.Command{
|
||||||
|
Name: commands.Logout,
|
||||||
|
ShortDescription: "Ends the current session",
|
||||||
|
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
||||||
|
end()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *session) handleChannels(chans <-chan ssh.NewChannel) {
|
func (s *session) handleChannels(chans <-chan ssh.NewChannel) {
|
||||||
defer s.Close()
|
defer s.Close()
|
||||||
for newChannel := range chans {
|
for newChannel := range chans {
|
||||||
@@ -84,9 +91,13 @@ func (s *session) handleRequests(in <-chan *ssh.Request, channel ssh.Channel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.Reply(true, nil)
|
req.Reply(true, nil)
|
||||||
s.dispatchCommand(payload.Value, commands.NewStringWriter(channel))
|
err := s.dispatchCommand(payload.Value, commands.NewStringWriter(channel))
|
||||||
|
|
||||||
|
// A command that failed exits 1, so `ssh host cmd` callers can tell.
|
||||||
status := struct{ Status uint32 }{uint32(0)}
|
status := struct{ Status uint32 }{uint32(0)}
|
||||||
|
if err != nil {
|
||||||
|
status.Status = 1
|
||||||
|
}
|
||||||
channel.SendRequest("exit-status", false, ssh.Marshal(status))
|
channel.SendRequest("exit-status", false, ssh.Marshal(status))
|
||||||
channel.Close()
|
channel.Close()
|
||||||
return
|
return
|
||||||
@@ -135,8 +146,20 @@ func (s *session) handleInput() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *session) dispatchCommand(line string, w commands.StringWriter) {
|
// dispatchCommand runs line and returns the command's error, which it has logged unless
|
||||||
s.commands.Dispatch(line, w)
|
// the command already told the user everything: the log line is for the operator reading
|
||||||
|
// the daemon's log, who learns nothing from a failure the user caused.
|
||||||
|
func (s *session) dispatchCommand(line string, w commands.StringWriter) error {
|
||||||
|
err := s.commands.Dispatch(line, w)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var f *commands.Failure
|
||||||
|
if !errors.As(err, &f) {
|
||||||
|
s.l.Debug("Command returned an error", "line", line, "error", err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *session) Close() {
|
func (s *session) Close() {
|
||||||
|
|||||||
+41
-1
@@ -6,10 +6,12 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -24,6 +26,7 @@ import (
|
|||||||
|
|
||||||
type sshdReloadHarness struct {
|
type sshdReloadHarness struct {
|
||||||
c *config.C
|
c *config.C
|
||||||
|
reg *commands.Registry
|
||||||
server *sshd.SSHServer
|
server *sshd.SSHServer
|
||||||
keyFile string
|
keyFile string
|
||||||
signer ssh.Signer
|
signer ssh.Signer
|
||||||
@@ -45,7 +48,8 @@ func newSSHDReloadHarness(t *testing.T) *sshdReloadHarness {
|
|||||||
|
|
||||||
h.c = config.NewC(l)
|
h.c = config.NewC(l)
|
||||||
require.NoError(t, h.c.LoadString(h.config()))
|
require.NoError(t, h.c.LoadString(h.config()))
|
||||||
h.server, err = sshd.NewSSHServer(t.Context(), l, commands.New())
|
h.reg = commands.New()
|
||||||
|
h.server, err = sshd.NewSSHServer(t.Context(), l, h.reg)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
wireSSHReload(l, h.server, h.c)
|
wireSSHReload(l, h.server, h.c)
|
||||||
run, err := configSSH(l, h.server, h.c, nil)
|
run, err := configSSH(l, h.server, h.c, nil)
|
||||||
@@ -239,3 +243,39 @@ func TestSSHDReload_SkippedEntriesFail(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestSSHExecExitStatusReflectsTheCommand: an exec request exits 1 when the command
|
||||||
|
// returned an error and 0 otherwise, with the command's output either way.
|
||||||
|
func TestSSHExecExitStatusReflectsTheCommand(t *testing.T) {
|
||||||
|
h := newSSHDReloadHarness(t)
|
||||||
|
h.reg.Register(&commands.Command{
|
||||||
|
Name: "fails",
|
||||||
|
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
||||||
|
_ = w.WriteLine("nope")
|
||||||
|
return errors.New("failed")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
client, _ := h.connect(t)
|
||||||
|
|
||||||
|
sess, err := client.NewSession()
|
||||||
|
require.NoError(t, err)
|
||||||
|
out, err := sess.CombinedOutput("fails")
|
||||||
|
var exit *ssh.ExitError
|
||||||
|
require.ErrorAs(t, err, &exit)
|
||||||
|
assert.Equal(t, 1, exit.ExitStatus())
|
||||||
|
assert.Equal(t, "nope\n", string(out))
|
||||||
|
|
||||||
|
sess, err = client.NewSession()
|
||||||
|
require.NoError(t, err)
|
||||||
|
out, err = sess.CombinedOutput("help fails")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "fails - \n", string(out))
|
||||||
|
|
||||||
|
// A word that is not a command is a failure too, with the command list as before.
|
||||||
|
sess, err = client.NewSession()
|
||||||
|
require.NoError(t, err)
|
||||||
|
out, err = sess.CombinedOutput("nope")
|
||||||
|
require.ErrorAs(t, err, &exit)
|
||||||
|
assert.Equal(t, 1, exit.ExitStatus())
|
||||||
|
assert.True(t, strings.HasPrefix(string(out), "did not understand: nope\nAvailable commands:\n"), string(out))
|
||||||
|
}
|
||||||
Reference in new issue
Block a user