mirror of
https://github.com/slackhq/nebula.git
synced 2026-11-15 01:26:52 +01:00
Extract the ssh debug commands into a transport neutral commands package (#1958)
Migrate commands from ssh into a transport neutral package - add golden tests validating that output remains unchanged - move commands to commands.go Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
cda3710d5a
commit
b426843f88
21 files changed
+2038
-978
No files matched your search
@@ -0,0 +1,2 @@
|
||||
# Goldens are compared byte for byte, so they must come out of git the way they went in.
|
||||
*.golden -text
|
||||
+756
@@ -0,0 +1,756 @@
|
||||
package nebula
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"maps"
|
||||
"net/netip"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/slackhq/nebula/commands"
|
||||
"github.com/slackhq/nebula/config"
|
||||
"github.com/slackhq/nebula/header"
|
||||
"github.com/slackhq/nebula/logging"
|
||||
)
|
||||
|
||||
// The commands nebula serves. attachCommands builds the one registry every surface
|
||||
// reads. See the commands package for what a Command is.
|
||||
|
||||
type sshListHostMapFlags struct {
|
||||
Json bool
|
||||
Pretty bool
|
||||
ByIndex bool
|
||||
}
|
||||
|
||||
type sshPrintCertFlags struct {
|
||||
Json bool
|
||||
Pretty bool
|
||||
Raw bool
|
||||
}
|
||||
|
||||
type sshPrintTunnelFlags struct {
|
||||
Pretty bool
|
||||
}
|
||||
|
||||
type sshChangeRemoteFlags struct {
|
||||
Address string
|
||||
}
|
||||
|
||||
type sshCloseTunnelFlags struct {
|
||||
LocalOnly bool
|
||||
}
|
||||
|
||||
type sshCreateTunnelFlags struct {
|
||||
Address string
|
||||
}
|
||||
|
||||
type sshDeviceInfoFlags struct {
|
||||
Json bool
|
||||
Pretty bool
|
||||
}
|
||||
|
||||
func attachCommands(l *slog.Logger, c *config.C, reg *commands.Registry, f *Interface) {
|
||||
reg.Register(&commands.Command{
|
||||
Name: commands.ListHostmap,
|
||||
ShortDescription: "List all known previously connected hosts",
|
||||
Flags: func() (*flag.FlagSet, any) {
|
||||
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 {
|
||||
return sshListHostMap(f.hostMap, fs, w)
|
||||
},
|
||||
})
|
||||
|
||||
reg.Register(&commands.Command{
|
||||
Name: commands.ListPendingHostmap,
|
||||
ShortDescription: "List all handshaking hosts",
|
||||
Flags: func() (*flag.FlagSet, any) {
|
||||
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 {
|
||||
return sshListHostMap(f.handshakeManager, fs, w)
|
||||
},
|
||||
})
|
||||
|
||||
reg.Register(&commands.Command{
|
||||
Name: commands.ListLighthouseAddrmap,
|
||||
ShortDescription: "List all lighthouse map entries",
|
||||
Flags: func() (*flag.FlagSet, any) {
|
||||
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")
|
||||
return fl, &s
|
||||
},
|
||||
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
||||
return sshListLighthouseMap(f.lightHouse, fs, w)
|
||||
},
|
||||
})
|
||||
|
||||
reg.Register(&commands.Command{
|
||||
Name: commands.Reload,
|
||||
ShortDescription: "Reloads configuration from disk, same as sending HUP to the process",
|
||||
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
||||
return sshReload(l, c, w)
|
||||
},
|
||||
})
|
||||
|
||||
reg.Register(&commands.Command{
|
||||
Name: commands.LogLevel,
|
||||
ShortDescription: "Gets or sets the current log level",
|
||||
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
||||
return sshLogLevel(l, fs, a, w)
|
||||
},
|
||||
})
|
||||
|
||||
reg.Register(&commands.Command{
|
||||
Name: commands.LogFormat,
|
||||
ShortDescription: "Gets or sets the current log format",
|
||||
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
||||
return sshLogFormat(l, fs, a, w)
|
||||
},
|
||||
})
|
||||
|
||||
reg.Register(&commands.Command{
|
||||
Name: commands.Version,
|
||||
ShortDescription: "Prints the currently running version of nebula",
|
||||
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
||||
return sshVersion(f, fs, a, w)
|
||||
},
|
||||
})
|
||||
|
||||
reg.Register(&commands.Command{
|
||||
Name: commands.DeviceInfo,
|
||||
ShortDescription: "Prints information about the network device.",
|
||||
Flags: func() (*flag.FlagSet, any) {
|
||||
fl := flag.NewFlagSet("", flag.ContinueOnError)
|
||||
s := sshDeviceInfoFlags{}
|
||||
fl.BoolVar(&s.Json, "json", false, "outputs as json with more information")
|
||||
fl.BoolVar(&s.Pretty, "pretty", false, "pretty prints json, assumes -json")
|
||||
return fl, &s
|
||||
},
|
||||
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
||||
return sshDeviceInfo(f, fs, w)
|
||||
},
|
||||
})
|
||||
|
||||
reg.Register(&commands.Command{
|
||||
Name: commands.PrintCert,
|
||||
ShortDescription: "Prints the current certificate being used or the certificate for the provided vpn addr",
|
||||
Flags: func() (*flag.FlagSet, any) {
|
||||
fl := flag.NewFlagSet("", flag.ContinueOnError)
|
||||
s := sshPrintCertFlags{}
|
||||
fl.BoolVar(&s.Json, "json", false, "outputs as 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")
|
||||
return fl, &s
|
||||
},
|
||||
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
||||
return sshPrintCert(f, fs, a, w)
|
||||
},
|
||||
})
|
||||
|
||||
reg.Register(&commands.Command{
|
||||
Name: commands.PrintTunnel,
|
||||
ShortDescription: "Prints json details about a tunnel for the provided vpn addr",
|
||||
Flags: func() (*flag.FlagSet, any) {
|
||||
fl := flag.NewFlagSet("", flag.ContinueOnError)
|
||||
s := sshPrintTunnelFlags{}
|
||||
fl.BoolVar(&s.Pretty, "pretty", false, "pretty prints json")
|
||||
return fl, &s
|
||||
},
|
||||
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
||||
return sshPrintTunnel(f, fs, a, w)
|
||||
},
|
||||
})
|
||||
|
||||
reg.Register(&commands.Command{
|
||||
Name: commands.PrintRelays,
|
||||
ShortDescription: "Prints json details about all relay info",
|
||||
Flags: func() (*flag.FlagSet, any) {
|
||||
fl := flag.NewFlagSet("", flag.ContinueOnError)
|
||||
s := sshPrintTunnelFlags{}
|
||||
fl.BoolVar(&s.Pretty, "pretty", false, "pretty prints json")
|
||||
return fl, &s
|
||||
},
|
||||
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
||||
return sshPrintRelays(f, fs, a, w)
|
||||
},
|
||||
})
|
||||
|
||||
reg.Register(&commands.Command{
|
||||
Name: commands.ChangeRemote,
|
||||
ShortDescription: "Changes the remote address used in the tunnel for the provided vpn addr",
|
||||
Flags: func() (*flag.FlagSet, any) {
|
||||
fl := flag.NewFlagSet("", flag.ContinueOnError)
|
||||
s := sshChangeRemoteFlags{}
|
||||
fl.StringVar(&s.Address, "address", "", "The new remote address, ip:port")
|
||||
return fl, &s
|
||||
},
|
||||
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
||||
return sshChangeRemote(f, fs, a, w)
|
||||
},
|
||||
})
|
||||
|
||||
reg.Register(&commands.Command{
|
||||
Name: commands.CloseTunnel,
|
||||
ShortDescription: "Closes a tunnel for the provided vpn addr",
|
||||
Flags: func() (*flag.FlagSet, any) {
|
||||
fl := flag.NewFlagSet("", flag.ContinueOnError)
|
||||
s := sshCloseTunnelFlags{}
|
||||
fl.BoolVar(&s.LocalOnly, "local-only", false, "Disables notifying the remote that the tunnel is shutting down")
|
||||
return fl, &s
|
||||
},
|
||||
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
||||
return sshCloseTunnel(f, fs, a, w)
|
||||
},
|
||||
})
|
||||
|
||||
reg.Register(&commands.Command{
|
||||
Name: commands.CreateTunnel,
|
||||
ShortDescription: "Creates a tunnel for the provided vpn address",
|
||||
Help: "The lighthouses will be queried for real addresses but you can provide one as well.",
|
||||
Flags: func() (*flag.FlagSet, any) {
|
||||
fl := flag.NewFlagSet("", flag.ContinueOnError)
|
||||
s := sshCreateTunnelFlags{}
|
||||
fl.StringVar(&s.Address, "address", "", "Optionally provide a real remote address, ip:port ")
|
||||
return fl, &s
|
||||
},
|
||||
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
||||
return sshCreateTunnel(f, fs, a, w)
|
||||
},
|
||||
})
|
||||
|
||||
reg.Register(&commands.Command{
|
||||
Name: commands.QueryLighthouse,
|
||||
ShortDescription: "Query the lighthouses for the provided vpn address",
|
||||
Help: "This command is asynchronous. Only currently known udp addresses will be printed.",
|
||||
Callback: func(fs any, a []string, w commands.StringWriter) error {
|
||||
return sshQueryLighthouse(f, fs, a, w)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func sshListHostMap(hl controlHostLister, a any, w commands.StringWriter) error {
|
||||
fs, ok := a.(*sshListHostMapFlags)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
var hm []ControlHostInfo
|
||||
if fs.ByIndex {
|
||||
hm = listHostMapIndexes(hl)
|
||||
} else {
|
||||
hm = listHostMapHosts(hl)
|
||||
}
|
||||
|
||||
sort.Slice(hm, func(i, j int) bool {
|
||||
return hm[i].VpnAddrs[0].Compare(hm[j].VpnAddrs[0]) < 0
|
||||
})
|
||||
|
||||
if fs.Json || fs.Pretty {
|
||||
js := json.NewEncoder(w.GetWriter())
|
||||
if fs.Pretty {
|
||||
js.SetIndent("", " ")
|
||||
}
|
||||
|
||||
err := js.Encode(hm)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
} else {
|
||||
for _, v := range hm {
|
||||
err := w.WriteLine(fmt.Sprintf("%s: %s", v.VpnAddrs, v.RemoteAddrs))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func sshListLighthouseMap(lightHouse *LightHouse, a any, w commands.StringWriter) error {
|
||||
fs, ok := a.(*sshListHostMapFlags)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
type lighthouseInfo struct {
|
||||
VpnAddr string `json:"vpnAddr"`
|
||||
Addrs *CacheMap `json:"addrs"`
|
||||
}
|
||||
|
||||
lightHouse.RLock()
|
||||
addrMap := make([]lighthouseInfo, len(lightHouse.addrMap))
|
||||
x := 0
|
||||
for k, v := range lightHouse.addrMap {
|
||||
addrMap[x] = lighthouseInfo{
|
||||
VpnAddr: k.String(),
|
||||
Addrs: v.CopyCache(),
|
||||
}
|
||||
x++
|
||||
}
|
||||
lightHouse.RUnlock()
|
||||
|
||||
sort.Slice(addrMap, func(i, j int) bool {
|
||||
return strings.Compare(addrMap[i].VpnAddr, addrMap[j].VpnAddr) < 0
|
||||
})
|
||||
|
||||
if fs.Json || fs.Pretty {
|
||||
js := json.NewEncoder(w.GetWriter())
|
||||
if fs.Pretty {
|
||||
js.SetIndent("", " ")
|
||||
}
|
||||
|
||||
err := js.Encode(addrMap)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
} else {
|
||||
for _, v := range addrMap {
|
||||
b, err := json.Marshal(v.Addrs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = w.WriteLine(fmt.Sprintf("%s: %s", v.VpnAddr, string(b)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func sshVersion(ifce *Interface, fs any, a []string, w commands.StringWriter) error {
|
||||
return w.WriteLine(fmt.Sprintf("%s", ifce.version))
|
||||
}
|
||||
|
||||
func sshQueryLighthouse(ifce *Interface, fs any, a []string, w commands.StringWriter) error {
|
||||
if len(a) == 0 {
|
||||
return w.WriteLine("No vpn address was provided")
|
||||
}
|
||||
|
||||
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]))
|
||||
}
|
||||
|
||||
var cm *CacheMap
|
||||
rl := ifce.lightHouse.Query(vpnAddr)
|
||||
if rl != nil {
|
||||
cm = rl.CopyCache()
|
||||
}
|
||||
return json.NewEncoder(w.GetWriter()).Encode(cm)
|
||||
}
|
||||
|
||||
func sshCloseTunnel(ifce *Interface, fs any, a []string, w commands.StringWriter) error {
|
||||
flags, ok := fs.(*sshCloseTunnelFlags)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(a) == 0 {
|
||||
return w.WriteLine("No vpn address was provided")
|
||||
}
|
||||
|
||||
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)
|
||||
if hostInfo == nil {
|
||||
return w.WriteLine(fmt.Sprintf("Could not find tunnel for vpn address: %v", a[0]))
|
||||
}
|
||||
|
||||
if !flags.LocalOnly {
|
||||
ifce.send(
|
||||
header.CloseTunnel,
|
||||
0,
|
||||
hostInfo.ConnectionState,
|
||||
hostInfo,
|
||||
[]byte{},
|
||||
make([]byte, 12, 12),
|
||||
make([]byte, mtu),
|
||||
)
|
||||
}
|
||||
|
||||
ifce.closeTunnel(hostInfo)
|
||||
return w.WriteLine("Closed")
|
||||
}
|
||||
|
||||
func sshCreateTunnel(ifce *Interface, fs any, a []string, w commands.StringWriter) error {
|
||||
flags, ok := fs.(*sshCreateTunnelFlags)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(a) == 0 {
|
||||
return w.WriteLine("No vpn address was provided")
|
||||
}
|
||||
|
||||
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)
|
||||
if hostInfo != nil {
|
||||
return w.WriteLine(fmt.Sprintf("Tunnel already exists"))
|
||||
}
|
||||
|
||||
hostInfo = ifce.handshakeManager.QueryVpnAddr(vpnAddr)
|
||||
if hostInfo != nil {
|
||||
return w.WriteLine(fmt.Sprintf("Tunnel already handshaking"))
|
||||
}
|
||||
|
||||
var addr netip.AddrPort
|
||||
if flags.Address != "" {
|
||||
addr, err = netip.ParseAddrPort(flags.Address)
|
||||
if err != nil {
|
||||
return w.WriteLine("Address could not be parsed")
|
||||
}
|
||||
}
|
||||
|
||||
// Set the remote from the callback, which StartHandshake runs under the handshake manager lock
|
||||
// before the hostinfo reaches the handshake loop or the static-host trigger, so the address is
|
||||
// in the RemoteList before the first handshake is sent.
|
||||
ifce.handshakeManager.StartHandshake(vpnAddr, func(hh *HandshakeHostInfo) {
|
||||
if addr.IsValid() {
|
||||
hh.hostinfo.SetRemote(addr)
|
||||
}
|
||||
})
|
||||
|
||||
return w.WriteLine("Created")
|
||||
}
|
||||
|
||||
func sshChangeRemote(ifce *Interface, fs any, a []string, w commands.StringWriter) error {
|
||||
flags, ok := fs.(*sshChangeRemoteFlags)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(a) == 0 {
|
||||
return w.WriteLine("No vpn address was provided")
|
||||
}
|
||||
|
||||
if flags.Address == "" {
|
||||
return w.WriteLine("No address was provided")
|
||||
}
|
||||
|
||||
addr, err := netip.ParseAddrPort(flags.Address)
|
||||
if err != nil {
|
||||
return w.WriteLine("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)
|
||||
if hostInfo == nil {
|
||||
return w.WriteLine(fmt.Sprintf("Could not find tunnel for vpn address: %v", a[0]))
|
||||
}
|
||||
|
||||
hostInfo.SetRemote(addr)
|
||||
return w.WriteLine("Changed")
|
||||
}
|
||||
|
||||
func sshLogLevel(l *slog.Logger, fs any, a []string, w commands.StringWriter) error {
|
||||
ctrl, ok := l.Handler().(interface {
|
||||
GetLevel() slog.Level
|
||||
SetLevel(slog.Level)
|
||||
})
|
||||
if !ok {
|
||||
return w.WriteLine("Log level is not reconfigurable on this logger")
|
||||
}
|
||||
|
||||
if len(a) == 0 {
|
||||
return w.WriteLine(fmt.Sprintf("Log level is: %s", logging.LevelName(ctrl.GetLevel())))
|
||||
}
|
||||
|
||||
level, err := logging.ParseLevel(strings.ToLower(a[0]))
|
||||
if err != nil {
|
||||
return w.WriteLine(fmt.Sprintf("Unknown log level %s. Possible log levels: trace, debug, info, warn, error", a))
|
||||
}
|
||||
|
||||
ctrl.SetLevel(level)
|
||||
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 {
|
||||
ctrl, ok := l.Handler().(interface {
|
||||
GetFormat() string
|
||||
SetFormat(string) error
|
||||
})
|
||||
if !ok {
|
||||
return w.WriteLine("Log format is not reconfigurable on this logger")
|
||||
}
|
||||
|
||||
if len(a) == 0 {
|
||||
return w.WriteLine(fmt.Sprintf("Log format is: %s", ctrl.GetFormat()))
|
||||
}
|
||||
|
||||
if err := ctrl.SetFormat(strings.ToLower(a[0])); err != nil {
|
||||
return err
|
||||
}
|
||||
return w.WriteLine(fmt.Sprintf("Log format is: %s", ctrl.GetFormat()))
|
||||
}
|
||||
|
||||
func sshPrintCert(ifce *Interface, fs any, a []string, w commands.StringWriter) error {
|
||||
args, ok := fs.(*sshPrintCertFlags)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
cert := ifce.pki.getCertState().GetDefaultCertificate()
|
||||
if len(a) > 0 {
|
||||
vpnAddr, err := netip.ParseAddr(a[0])
|
||||
if err != nil {
|
||||
return w.WriteLine(fmt.Sprintf("The provided vpn addr could not be parsed: %s", a[0]))
|
||||
}
|
||||
|
||||
if !vpnAddr.IsValid() {
|
||||
return w.WriteLine(fmt.Sprintf("The provided vpn addr could not be parsed: %s", a[0]))
|
||||
}
|
||||
|
||||
hostInfo := ifce.hostMap.QueryVpnAddr(vpnAddr)
|
||||
if hostInfo == nil {
|
||||
return w.WriteLine(fmt.Sprintf("Could not find tunnel for vpn addr: %v", a[0]))
|
||||
}
|
||||
|
||||
cert = hostInfo.GetCert().Certificate
|
||||
}
|
||||
|
||||
if args.Json || args.Pretty {
|
||||
b, err := cert.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if args.Pretty {
|
||||
buf := new(bytes.Buffer)
|
||||
err := json.Indent(buf, b, "", " ")
|
||||
b = buf.Bytes()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return w.WriteBytes(b)
|
||||
}
|
||||
|
||||
if args.Raw {
|
||||
b, err := cert.MarshalPEM()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return w.WriteBytes(b)
|
||||
}
|
||||
|
||||
return w.WriteLine(cert.String())
|
||||
}
|
||||
|
||||
func sshPrintRelays(ifce *Interface, fs any, a []string, w commands.StringWriter) error {
|
||||
args, ok := fs.(*sshPrintTunnelFlags)
|
||||
if !ok {
|
||||
w.WriteLine(fmt.Sprintf("sshPrintRelays failed to convert args type"))
|
||||
return nil
|
||||
}
|
||||
|
||||
relays := map[uint32]*HostInfo{}
|
||||
ifce.hostMap.Lock()
|
||||
maps.Copy(relays, ifce.hostMap.Relays)
|
||||
ifce.hostMap.Unlock()
|
||||
|
||||
type RelayFor struct {
|
||||
Error error
|
||||
Type string
|
||||
State string
|
||||
PeerAddr netip.Addr
|
||||
LocalIndex uint32
|
||||
RemoteIndex uint32
|
||||
RelayedThrough []netip.Addr
|
||||
}
|
||||
|
||||
type RelayOutput struct {
|
||||
NebulaAddr netip.Addr
|
||||
RelayForAddrs []RelayFor
|
||||
}
|
||||
|
||||
type CmdOutput struct {
|
||||
Relays []*RelayOutput
|
||||
}
|
||||
|
||||
co := CmdOutput{}
|
||||
|
||||
enc := json.NewEncoder(w.GetWriter())
|
||||
|
||||
if args.Pretty {
|
||||
enc.SetIndent("", " ")
|
||||
}
|
||||
|
||||
for k, v := range relays {
|
||||
ro := RelayOutput{NebulaAddr: v.vpnAddrs[0]}
|
||||
co.Relays = append(co.Relays, &ro)
|
||||
relayHI := ifce.hostMap.QueryVpnAddr(v.vpnAddrs[0])
|
||||
if relayHI == nil {
|
||||
ro.RelayForAddrs = append(ro.RelayForAddrs, RelayFor{Error: errors.New("could not find hostinfo")})
|
||||
continue
|
||||
}
|
||||
for _, vpnAddr := range relayHI.relayState.CopyRelayForIps() {
|
||||
rf := RelayFor{Error: nil}
|
||||
r, ok := relayHI.relayState.GetRelayForByAddr(vpnAddr)
|
||||
if ok {
|
||||
t := ""
|
||||
switch r.Type {
|
||||
case ForwardingType:
|
||||
t = "forwarding"
|
||||
case TerminalType:
|
||||
t = "terminal"
|
||||
default:
|
||||
t = "unknown"
|
||||
}
|
||||
|
||||
s := ""
|
||||
switch r.State {
|
||||
case Requested:
|
||||
s = "requested"
|
||||
case Established:
|
||||
s = "established"
|
||||
default:
|
||||
s = "unknown"
|
||||
}
|
||||
|
||||
rf.LocalIndex = r.LocalIndex
|
||||
rf.RemoteIndex = r.RemoteIndex
|
||||
rf.PeerAddr = r.PeerAddr
|
||||
rf.Type = t
|
||||
rf.State = s
|
||||
if rf.LocalIndex != k {
|
||||
rf.Error = fmt.Errorf("hostmap LocalIndex '%v' does not match RelayState LocalIndex", k)
|
||||
}
|
||||
}
|
||||
relayedHI := ifce.hostMap.QueryVpnAddr(vpnAddr)
|
||||
if relayedHI != nil {
|
||||
rf.RelayedThrough = append(rf.RelayedThrough, relayedHI.relayState.CopyRelayIps()...)
|
||||
}
|
||||
|
||||
ro.RelayForAddrs = append(ro.RelayForAddrs, rf)
|
||||
}
|
||||
}
|
||||
err := enc.Encode(co)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func sshPrintTunnel(ifce *Interface, fs any, a []string, w commands.StringWriter) error {
|
||||
args, ok := fs.(*sshPrintTunnelFlags)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(a) == 0 {
|
||||
return w.WriteLine("No vpn address was provided")
|
||||
}
|
||||
|
||||
vpnAddr, err := netip.ParseAddr(a[0])
|
||||
if err != nil {
|
||||
return w.WriteLine(fmt.Sprintf("The provided vpn addr could not be parsed: %s", a[0]))
|
||||
}
|
||||
|
||||
if !vpnAddr.IsValid() {
|
||||
return w.WriteLine(fmt.Sprintf("The provided vpn addr could not be parsed: %s", a[0]))
|
||||
}
|
||||
|
||||
hostInfo := ifce.hostMap.QueryVpnAddr(vpnAddr)
|
||||
if hostInfo == nil {
|
||||
return w.WriteLine(fmt.Sprintf("Could not find tunnel for vpn addr: %v", a[0]))
|
||||
}
|
||||
|
||||
enc := json.NewEncoder(w.GetWriter())
|
||||
if args.Pretty {
|
||||
enc.SetIndent("", " ")
|
||||
}
|
||||
|
||||
return enc.Encode(copyHostInfo(hostInfo, ifce.hostMap.GetPreferredRanges()))
|
||||
}
|
||||
|
||||
func sshDeviceInfo(ifce *Interface, fs any, w commands.StringWriter) error {
|
||||
|
||||
data := struct {
|
||||
Name string `json:"name"`
|
||||
Cidr []netip.Prefix `json:"cidr"`
|
||||
}{
|
||||
Name: ifce.inside.Name(),
|
||||
Cidr: make([]netip.Prefix, len(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 {
|
||||
js := json.NewEncoder(w.GetWriter())
|
||||
if flags.Pretty {
|
||||
js.SetIndent("", " ")
|
||||
}
|
||||
|
||||
return js.Encode(data)
|
||||
} else {
|
||||
return w.WriteLine(fmt.Sprintf("name=%v cidr=%v", data.Name, data.Cidr))
|
||||
}
|
||||
}
|
||||
|
||||
// sshReload 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 {
|
||||
writeErr := w.WriteLine("Reloading config")
|
||||
res := c.ReloadConfig()
|
||||
res.Log(l)
|
||||
if err := w.WriteLine(res.String()); writeErr == nil {
|
||||
writeErr = err
|
||||
}
|
||||
return writeErr
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"flag"
|
||||
)
|
||||
|
||||
// CommandFlags is called before help or command execution to parse command line flags.
|
||||
// It should return a flag.FlagSet instance and a pointer to the struct that will contain parsed flags
|
||||
type CommandFlags func() (*flag.FlagSet, any)
|
||||
|
||||
// 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
|
||||
// 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.
|
||||
// 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
|
||||
// where appropriate
|
||||
type CommandCallback func(fs any, a []string, w StringWriter) error
|
||||
|
||||
type Command struct {
|
||||
Name Name
|
||||
ShortDescription string
|
||||
Help string
|
||||
|
||||
Flags CommandFlags
|
||||
Callback CommandCallback
|
||||
}
|
||||
|
||||
func execCommand(c *Command, args []string, w StringWriter) error {
|
||||
var (
|
||||
fl *flag.FlagSet
|
||||
fs any
|
||||
)
|
||||
|
||||
if c.Flags != nil {
|
||||
fl, fs = c.Flags()
|
||||
if fl != nil {
|
||||
// SetOutput() here in case fl.Parse dumps usage.
|
||||
fl.SetOutput(w.GetWriter())
|
||||
err := fl.Parse(args)
|
||||
if err != nil {
|
||||
// fl.Parse has dumped error information to the user via the w writer.
|
||||
return err
|
||||
}
|
||||
args = fl.Args()
|
||||
}
|
||||
}
|
||||
|
||||
return c.Callback(fs, args, w)
|
||||
}
|
||||
|
||||
func checkHelpArgs(args []string) bool {
|
||||
for _, a := range args {
|
||||
if a == "-h" || a == "-help" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package commands
|
||||
|
||||
// Name identifies a command. Every command nebula serves is a constant here, so a
|
||||
// surface, a client and a test spell it the same way and are told at compile time when
|
||||
// they do not. It is a string rather than an integer so a test can register a throwaway
|
||||
// command without adding it here, and so the wire carries the name as the string it has
|
||||
// always been.
|
||||
type Name string
|
||||
|
||||
const (
|
||||
ChangeRemote Name = "change-remote"
|
||||
CloseTunnel Name = "close-tunnel"
|
||||
CreateTunnel Name = "create-tunnel"
|
||||
DeviceInfo Name = "device-info"
|
||||
Help Name = "help"
|
||||
ListHostmap Name = "list-hostmap"
|
||||
ListLighthouseAddrmap Name = "list-lighthouse-addrmap"
|
||||
ListPendingHostmap Name = "list-pending-hostmap"
|
||||
LogFormat Name = "log-format"
|
||||
LogLevel Name = "log-level"
|
||||
Logout Name = "logout"
|
||||
PrintCert Name = "print-cert"
|
||||
PrintRelays Name = "print-relays"
|
||||
PrintTunnel Name = "print-tunnel"
|
||||
QueryLighthouse Name = "query-lighthouse"
|
||||
Reload Name = "reload"
|
||||
Version Name = "version"
|
||||
)
|
||||
@@ -0,0 +1,179 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/anmitsu/go-shlex"
|
||||
)
|
||||
|
||||
// Registry holds a set of commands. New is the only way to make one.
|
||||
//
|
||||
// It is safe for concurrent use. Commands may be registered while a surface is serving
|
||||
// from the registry: the ssh server clones it per session, and a clone may overlap a
|
||||
// Register.
|
||||
type Registry struct {
|
||||
mu sync.RWMutex
|
||||
commands map[Name]*Command
|
||||
}
|
||||
|
||||
// New returns an empty Registry with only `help` available.
|
||||
func New() *Registry {
|
||||
r := &Registry{commands: map[Name]*Command{}}
|
||||
r.registerHelp()
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Registry) registerHelp() {
|
||||
r.Register(&Command{
|
||||
Name: Help,
|
||||
ShortDescription: "prints available commands or help <command> for specific usage info",
|
||||
Callback: func(a any, args []string, w StringWriter) error {
|
||||
return r.help(args, w)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Register adds a command that can be run by a user.
|
||||
func (r *Registry) Register(c *Command) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.commands[c.Name] = c
|
||||
}
|
||||
|
||||
// Get returns the command registered under name, or nil if there is no such command.
|
||||
func (r *Registry) Get(name Name) *Command {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return r.commands[name]
|
||||
}
|
||||
|
||||
// Match returns the names of every command starting with prefix, sorted, as strings for
|
||||
// a prompt to complete and print.
|
||||
func (r *Registry) Match(prefix string) []string {
|
||||
cmds := make([]string, 0)
|
||||
r.mu.RLock()
|
||||
for n := range r.commands {
|
||||
if strings.HasPrefix(string(n), prefix) {
|
||||
cmds = append(cmds, string(n))
|
||||
}
|
||||
}
|
||||
r.mu.RUnlock()
|
||||
slices.Sort(cmds)
|
||||
return cmds
|
||||
}
|
||||
|
||||
// All returns every registered command sorted by name, which is the order help lists them.
|
||||
func (r *Registry) All() []*Command {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
cmds := slices.Collect(maps.Values(r.commands))
|
||||
slices.SortFunc(cmds, func(a, b *Command) int { return cmp.Compare(a.Name, b.Name) })
|
||||
return cmds
|
||||
}
|
||||
|
||||
// Clone returns a copy holding the same commands.
|
||||
func (r *Registry) Clone() *Registry {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return &Registry{commands: maps.Clone(r.commands)}
|
||||
}
|
||||
|
||||
// Dispatch parses a command line and runs it, reporting problems to w.
|
||||
func (r *Registry) Dispatch(line string, w StringWriter) {
|
||||
// A line shlex cannot split, an unbalanced quote, gets no answer. sshd was silent
|
||||
// here too; this is a relocation, so it stays silent.
|
||||
args, err := shlex.Split(line, true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_ = r.DispatchArgs(args, line, w)
|
||||
}
|
||||
|
||||
// DispatchArgs runs an already tokenized command line. orig is what the user typed,
|
||||
// used only for the "did not understand" message; callers that have no raw line may
|
||||
// pass "" and the joined args are used instead.
|
||||
func (r *Registry) DispatchArgs(args []string, orig string, w StringWriter) error {
|
||||
if len(args) == 0 {
|
||||
r.dumpCommands(w)
|
||||
return nil
|
||||
}
|
||||
|
||||
c := r.Get(Name(args[0]))
|
||||
if c == nil {
|
||||
if orig == "" {
|
||||
orig = strings.Join(args, " ")
|
||||
}
|
||||
|
||||
if err := w.WriteLine(fmt.Sprintf("did not understand: %s", orig)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.dumpCommands(w)
|
||||
return nil
|
||||
}
|
||||
|
||||
if checkHelpArgs(args) {
|
||||
// Through the registered help command, so -h and `help <name>` are one path.
|
||||
return execCommand(r.Get(Help), []string{string(c.Name)}, w)
|
||||
}
|
||||
|
||||
return execCommand(c, args[1:], w)
|
||||
}
|
||||
|
||||
// dumpCommands writes the list of available commands to w.
|
||||
func (r *Registry) dumpCommands(w StringWriter) {
|
||||
err := w.WriteLine("Available commands:")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// In the order All gives, which is the display order.
|
||||
cmds := make([]string, 0)
|
||||
for _, l := range r.All() {
|
||||
cmds = append(cmds, fmt.Sprintf("%s - %s", l.Name, l.ShortDescription))
|
||||
}
|
||||
|
||||
_ = w.Write(strings.Join(cmds, "\n") + "\n\n")
|
||||
}
|
||||
|
||||
// help writes the command list, or usage for the single command named in a.
|
||||
func (r *Registry) help(a []string, w StringWriter) error {
|
||||
// Just typed help
|
||||
if len(a) == 0 {
|
||||
r.dumpCommands(w)
|
||||
return nil
|
||||
}
|
||||
|
||||
// We are printing a specific commands help text
|
||||
cmd := r.Get(Name(a[0]))
|
||||
if cmd == nil {
|
||||
return w.WriteLine("Command not available " + a[0])
|
||||
}
|
||||
|
||||
if err := w.WriteLine(fmt.Sprintf("%s - %s", cmd.Name, cmd.ShortDescription)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cmd.Help != "" {
|
||||
if err := w.WriteLine(fmt.Sprintf(" %s", cmd.Help)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if cmd.Flags != nil {
|
||||
fs, _ := cmd.Flags()
|
||||
if fs != nil {
|
||||
fs.SetOutput(w.GetWriter())
|
||||
fs.PrintDefaults()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func testRegistry() *Registry {
|
||||
r := New()
|
||||
r.Register(&Command{
|
||||
Name: "served",
|
||||
ShortDescription: "a command this surface serves",
|
||||
Callback: func(fs any, a []string, w StringWriter) error {
|
||||
return w.WriteLine("read " + strings.Join(a, ","))
|
||||
},
|
||||
})
|
||||
r.Register(&Command{
|
||||
Name: "withheld",
|
||||
ShortDescription: "a command this surface withholds",
|
||||
Callback: func(fs any, a []string, w StringWriter) error {
|
||||
return w.WriteLine("wrote")
|
||||
},
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
// TestFlagUsageGoesToTheWriter pins that a parse failure's usage lands on the command's
|
||||
// writer.
|
||||
func TestFlagUsageGoesToTheWriter(t *testing.T) {
|
||||
r := New()
|
||||
r.Register(&Command{
|
||||
Name: "emit",
|
||||
ShortDescription: "emits",
|
||||
Flags: func() (*flag.FlagSet, any) {
|
||||
fl := flag.NewFlagSet("", flag.ContinueOnError)
|
||||
fl.Bool("json", false, "emit json")
|
||||
return fl, nil
|
||||
},
|
||||
Callback: func(fs any, a []string, w StringWriter) error { return nil },
|
||||
})
|
||||
|
||||
merged := &bytes.Buffer{}
|
||||
require.Error(t, r.DispatchArgs([]string{"emit", "-nope"}, "", NewStringWriter(merged)))
|
||||
assert.Contains(t, merged.String(), "flag provided but not defined")
|
||||
}
|
||||
|
||||
// TestDispatchDropsUnparseableLine pins sshd's behaviour for a line shlex rejects: no
|
||||
// answer at all. An operator with an unbalanced quote gets silence, as they always have.
|
||||
func TestDispatchDropsUnparseableLine(t *testing.T) {
|
||||
out := &bytes.Buffer{}
|
||||
testRegistry().Dispatch(`served "unterminated`, NewStringWriter(out))
|
||||
assert.Empty(t, out.String())
|
||||
}
|
||||
|
||||
// TestAllSortsByName: registration order does not matter.
|
||||
func TestAllSortsByName(t *testing.T) {
|
||||
r := New()
|
||||
for _, n := range []Name{"zz", Version, "aa", ChangeRemote} {
|
||||
r.Register(&Command{Name: n})
|
||||
}
|
||||
|
||||
var got []Name
|
||||
for _, c := range r.All() {
|
||||
got = append(got, c.Name)
|
||||
}
|
||||
assert.Equal(t, []Name{"aa", ChangeRemote, Help, Version, "zz"}, got)
|
||||
}
|
||||
|
||||
// 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
|
||||
// clone is known to neither -h nor `help <name>`, and the two never disagree.
|
||||
func TestHelpFlagAgreesWithHelpCommand(t *testing.T) {
|
||||
r := testRegistry()
|
||||
c := r.Clone()
|
||||
c.Register(&Command{
|
||||
Name: Logout,
|
||||
ShortDescription: "ends the session",
|
||||
Callback: func(fs any, a []string, w StringWriter) error { return nil },
|
||||
})
|
||||
|
||||
flag := &bytes.Buffer{}
|
||||
c.Dispatch("logout -h", NewStringWriter(flag))
|
||||
cmd := &bytes.Buffer{}
|
||||
c.Dispatch("help logout", NewStringWriter(cmd))
|
||||
assert.Equal(t, cmd.String(), flag.String())
|
||||
assert.Equal(t, "Command not available logout\n", flag.String())
|
||||
|
||||
// And a command both hold is described either way.
|
||||
flag.Reset()
|
||||
c.Dispatch("served -h", NewStringWriter(flag))
|
||||
assert.Equal(t, "served - a command this surface serves\n", flag.String())
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
package sshd
|
||||
package commands
|
||||
|
||||
import "io"
|
||||
|
||||
// StringWriter is how a command talks back to whoever invoked it: one stream, which the
|
||||
// ssh surface points at its channel.
|
||||
type StringWriter interface {
|
||||
WriteLine(string) error
|
||||
Write(string) error
|
||||
@@ -13,6 +15,11 @@ type stringWriter struct {
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
// NewStringWriter returns a StringWriter that writes to w.
|
||||
func NewStringWriter(w io.Writer) StringWriter {
|
||||
return &stringWriter{w: w}
|
||||
}
|
||||
|
||||
func (w *stringWriter) WriteLine(s string) error {
|
||||
return w.Write(s + "\n")
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"syscall"
|
||||
|
||||
"github.com/slackhq/nebula/cert"
|
||||
"github.com/slackhq/nebula/commands"
|
||||
"github.com/slackhq/nebula/header"
|
||||
"github.com/slackhq/nebula/overlay"
|
||||
)
|
||||
@@ -47,6 +48,7 @@ type Control struct {
|
||||
|
||||
f *Interface
|
||||
l *slog.Logger
|
||||
commands *commands.Registry
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
sshStart func()
|
||||
@@ -372,6 +374,10 @@ func (c *Control) CloseAllTunnels(excludeLighthouses bool) (closed int) {
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Control) Commands() *commands.Registry {
|
||||
return c.commands
|
||||
}
|
||||
|
||||
func (c *Control) Device() overlay.Device {
|
||||
return c.f.inside
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
//go:build e2e_testing
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/slackhq/nebula/cert"
|
||||
"github.com/slackhq/nebula/cert_test"
|
||||
"github.com/slackhq/nebula/commands"
|
||||
"github.com/slackhq/nebula/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// volatile lists everything in command output that legitimately differs between runs.
|
||||
// Each run mints fresh certificates and indexes, so these are replaced with a marker
|
||||
// before comparison. Keep the list short: anything added here is something the golden
|
||||
// stops guarding.
|
||||
var volatile = []struct {
|
||||
re *regexp.Regexp
|
||||
with string
|
||||
}{
|
||||
{regexp.MustCompile(`[0-9a-f]{40,}`), "<hex>"}, // fingerprints, keys, signatures
|
||||
{regexp.MustCompile(`\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})`), "<time>"}, // RFC 3339
|
||||
{regexp.MustCompile(`\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)? [+-]\d{4} \w+`), "<time>"}, // time.Time.String()
|
||||
{regexp.MustCompile(`(?s)(-----BEGIN [A-Z ]+-----\n).*?(\n-----END [A-Z ]+-----)`), "${1}<pem>${2}"}, // print-cert -raw
|
||||
{regexp.MustCompile(`(-----BEGIN [A-Z ]+-----\\n).*?(\\n-----END [A-Z ]+-----)`), "${1}<pem>${2}"}, // the same, inside a JSON string
|
||||
{regexp.MustCompile(`"(localIndex|remoteIndex|messageCounter)":\d+`), `"$1":<n>`},
|
||||
{regexp.MustCompile(`(?m)^(\s*"?(?:LocalIndex|RemoteIndex|localIndex|remoteIndex|messageCounter)"?:?\s*)\d+`), "${1}<n>"}, // and the -pretty form
|
||||
}
|
||||
|
||||
func normalise(s string) string {
|
||||
for _, v := range volatile {
|
||||
s = v.re.ReplaceAllString(s, v.with)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// outputCase is one command line to run through the ssh dispatch path.
|
||||
type outputCase struct {
|
||||
name string
|
||||
args []string
|
||||
}
|
||||
|
||||
// TestCommandOutput pins the bytes every command produces, in every format, through the
|
||||
// ssh dispatch path. It is the safety net for moving rendering out of the commands: a
|
||||
// change here is a change someone at an ssh prompt would see. Regenerate with:
|
||||
//
|
||||
// go test -tags e2e_testing ./e2e -run TestCommandOutput -update
|
||||
func TestCommandOutput(t *testing.T) {
|
||||
ca, _, caKey, _ := cert_test.NewTestCaCert(
|
||||
cert.Version1, cert.Curve_CURVE25519,
|
||||
time.Now(), time.Now().Add(10*time.Minute),
|
||||
nil, nil, []string{},
|
||||
)
|
||||
|
||||
control, _, _, _ := newSimpleServer(cert.Version1, ca, caKey, "output-golden", "10.222.0.1/24", nil)
|
||||
control.Start()
|
||||
t.Cleanup(func() { control.Stop() })
|
||||
|
||||
reg := control.Commands().Clone()
|
||||
|
||||
run := func(args ...string) string {
|
||||
buf := &bytes.Buffer{}
|
||||
_ = reg.DispatchArgs(args, strings.Join(args, " "), commands.NewStringWriter(buf))
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// Seed a pending tunnel so the listing and per-address commands have something to show.
|
||||
seed := run("create-tunnel", "10.222.0.9")
|
||||
require.Equal(t, "Created\n", seed, "seeding a tunnel should succeed before anything else is recorded")
|
||||
|
||||
cases := []outputCase{
|
||||
{"version", nil},
|
||||
{"device-info", nil},
|
||||
{"list-hostmap", nil},
|
||||
{"list-hostmap", []string{"-by-index"}},
|
||||
{"list-pending-hostmap", nil},
|
||||
{"list-pending-hostmap", []string{"-by-index"}},
|
||||
{"list-lighthouse-addrmap", nil},
|
||||
{"print-cert", nil},
|
||||
{"print-cert", []string{"-raw"}},
|
||||
{"print-cert", []string{"10.222.0.9"}},
|
||||
{"print-cert", []string{"10.222.0.250"}},
|
||||
{"print-cert", []string{"not-an-address"}},
|
||||
{"print-tunnel", nil},
|
||||
{"print-tunnel", []string{"10.222.0.9"}},
|
||||
{"print-tunnel", []string{"10.222.0.250"}},
|
||||
{"print-relays", nil},
|
||||
{"query-lighthouse", nil},
|
||||
{"query-lighthouse", []string{"10.222.0.9"}},
|
||||
{"query-lighthouse", []string{"not-an-address"}},
|
||||
// The test logger cannot change level or format, so these cases record the
|
||||
// refusal each command prints on it, not a successful change.
|
||||
{"log-level", nil},
|
||||
{"log-level", []string{"debug"}},
|
||||
{"log-level", []string{"bogus"}},
|
||||
{"log-level", []string{"info"}},
|
||||
{"log-format", nil},
|
||||
{"log-format", []string{"json"}},
|
||||
{"log-format", []string{"text"}},
|
||||
{"change-remote", nil},
|
||||
{"change-remote", []string{"10.222.0.9"}},
|
||||
{"change-remote", []string{"-address", "10.94.0.10:4242", "10.222.0.9"}},
|
||||
{"change-remote", []string{"-address", "10.94.0.10:4242", "10.222.0.250"}},
|
||||
{"create-tunnel", nil},
|
||||
{"create-tunnel", []string{"10.222.0.9"}},
|
||||
// A fresh address, so this exercises the handshake start with a remote attached
|
||||
// rather than the "already handshaking" path above.
|
||||
{"create-tunnel", []string{"-address", "10.94.0.10:4242", "10.222.0.10"}},
|
||||
{"create-tunnel", []string{"not-an-address"}},
|
||||
{"close-tunnel", nil},
|
||||
{"close-tunnel", []string{"10.222.0.250"}},
|
||||
{"close-tunnel", []string{"not-an-address"}},
|
||||
{"help", nil},
|
||||
{"help", []string{"print-cert"}},
|
||||
// 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
|
||||
// has no path to re-read, which is what the recorded result says.
|
||||
{"reload", nil},
|
||||
}
|
||||
|
||||
got := &strings.Builder{}
|
||||
for _, c := range cases {
|
||||
for _, fmtFlags := range [][]string{nil, {"-json"}, {"-pretty"}} {
|
||||
args := append([]string{c.name}, fmtFlags...)
|
||||
args = append(args, c.args...)
|
||||
|
||||
fmt.Fprintf(got, "$ %s\n", strings.Join(args, " "))
|
||||
out := normalise(run(args...))
|
||||
got.WriteString(out)
|
||||
// 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.
|
||||
if out != "" && !strings.HasSuffix(out, "\n") {
|
||||
got.WriteString("\\ No newline at end of output\n")
|
||||
}
|
||||
got.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
test.Golden(t, filepath.Join("testdata", "commands_output.golden"), got.String())
|
||||
}
|
||||
Vendored
+543
@@ -0,0 +1,543 @@
|
||||
$ version
|
||||
e2e-test
|
||||
|
||||
$ version -json
|
||||
e2e-test
|
||||
|
||||
$ version -pretty
|
||||
e2e-test
|
||||
|
||||
$ device-info
|
||||
name= cidr=[10.222.0.1/24]
|
||||
|
||||
$ device-info -json
|
||||
{"name":"","cidr":["10.222.0.1/24"]}
|
||||
|
||||
$ device-info -pretty
|
||||
{
|
||||
"name": "",
|
||||
"cidr": [
|
||||
"10.222.0.1/24"
|
||||
]
|
||||
}
|
||||
|
||||
$ list-hostmap
|
||||
|
||||
$ list-hostmap -json
|
||||
[]
|
||||
|
||||
$ list-hostmap -pretty
|
||||
[]
|
||||
|
||||
$ list-hostmap -by-index
|
||||
|
||||
$ list-hostmap -json -by-index
|
||||
[]
|
||||
|
||||
$ list-hostmap -pretty -by-index
|
||||
[]
|
||||
|
||||
$ list-pending-hostmap
|
||||
[10.222.0.9]: []
|
||||
|
||||
$ list-pending-hostmap -json
|
||||
[{"vpnAddrs":["10.222.0.9"],"localIndex":<n>,"remoteIndex":<n>,"remoteAddrs":[],"cert":null,"messageCounter":<n>,"currentRemote":"","currentRelaysToMe":[],"currentRelaysThroughMe":[]}]
|
||||
|
||||
$ list-pending-hostmap -pretty
|
||||
[
|
||||
{
|
||||
"vpnAddrs": [
|
||||
"10.222.0.9"
|
||||
],
|
||||
"localIndex": <n>,
|
||||
"remoteIndex": <n>,
|
||||
"remoteAddrs": [],
|
||||
"cert": null,
|
||||
"messageCounter": <n>,
|
||||
"currentRemote": "",
|
||||
"currentRelaysToMe": [],
|
||||
"currentRelaysThroughMe": []
|
||||
}
|
||||
]
|
||||
|
||||
$ list-pending-hostmap -by-index
|
||||
|
||||
$ list-pending-hostmap -json -by-index
|
||||
[]
|
||||
|
||||
$ list-pending-hostmap -pretty -by-index
|
||||
[]
|
||||
|
||||
$ list-lighthouse-addrmap
|
||||
10.222.0.9: {}
|
||||
|
||||
$ list-lighthouse-addrmap -json
|
||||
[{"vpnAddr":"10.222.0.9","addrs":{}}]
|
||||
|
||||
$ list-lighthouse-addrmap -pretty
|
||||
[
|
||||
{
|
||||
"vpnAddr": "10.222.0.9",
|
||||
"addrs": {}
|
||||
}
|
||||
]
|
||||
|
||||
$ print-cert
|
||||
{
|
||||
"details": {
|
||||
"curve": "CURVE25519",
|
||||
"groups": [],
|
||||
"isCa": false,
|
||||
"issuer": "<hex>",
|
||||
"name": "output-golden",
|
||||
"networks": [
|
||||
"10.222.0.1/24"
|
||||
],
|
||||
"notAfter": "<time>",
|
||||
"notBefore": "<time>",
|
||||
"publicKey": "<hex>",
|
||||
"unsafeNetworks": []
|
||||
},
|
||||
"fingerprint": "<hex>",
|
||||
"signature": "<hex>",
|
||||
"version": 1
|
||||
}
|
||||
|
||||
$ print-cert -json
|
||||
{"details":{"curve":"CURVE25519","groups":[],"isCa":false,"issuer":"<hex>","name":"output-golden","networks":["10.222.0.1/24"],"notAfter":"<time>","notBefore":"<time>","publicKey":"<hex>","unsafeNetworks":[]},"fingerprint":"<hex>","signature":"<hex>","version":1}\ No newline at end of output
|
||||
|
||||
$ print-cert -pretty
|
||||
{
|
||||
"details": {
|
||||
"curve": "CURVE25519",
|
||||
"groups": [],
|
||||
"isCa": false,
|
||||
"issuer": "<hex>",
|
||||
"name": "output-golden",
|
||||
"networks": [
|
||||
"10.222.0.1/24"
|
||||
],
|
||||
"notAfter": "<time>",
|
||||
"notBefore": "<time>",
|
||||
"publicKey": "<hex>",
|
||||
"unsafeNetworks": []
|
||||
},
|
||||
"fingerprint": "<hex>",
|
||||
"signature": "<hex>",
|
||||
"version": 1
|
||||
}\ No newline at end of output
|
||||
|
||||
$ print-cert -raw
|
||||
-----BEGIN NEBULA CERTIFICATE-----
|
||||
<pem>
|
||||
-----END NEBULA CERTIFICATE-----
|
||||
|
||||
$ print-cert -json -raw
|
||||
{"details":{"curve":"CURVE25519","groups":[],"isCa":false,"issuer":"<hex>","name":"output-golden","networks":["10.222.0.1/24"],"notAfter":"<time>","notBefore":"<time>","publicKey":"<hex>","unsafeNetworks":[]},"fingerprint":"<hex>","signature":"<hex>","version":1}\ No newline at end of output
|
||||
|
||||
$ print-cert -pretty -raw
|
||||
{
|
||||
"details": {
|
||||
"curve": "CURVE25519",
|
||||
"groups": [],
|
||||
"isCa": false,
|
||||
"issuer": "<hex>",
|
||||
"name": "output-golden",
|
||||
"networks": [
|
||||
"10.222.0.1/24"
|
||||
],
|
||||
"notAfter": "<time>",
|
||||
"notBefore": "<time>",
|
||||
"publicKey": "<hex>",
|
||||
"unsafeNetworks": []
|
||||
},
|
||||
"fingerprint": "<hex>",
|
||||
"signature": "<hex>",
|
||||
"version": 1
|
||||
}\ No newline at end of output
|
||||
|
||||
$ print-cert 10.222.0.9
|
||||
Could not find tunnel for vpn addr: 10.222.0.9
|
||||
|
||||
$ print-cert -json 10.222.0.9
|
||||
Could not find tunnel for vpn addr: 10.222.0.9
|
||||
|
||||
$ print-cert -pretty 10.222.0.9
|
||||
Could not find tunnel for vpn addr: 10.222.0.9
|
||||
|
||||
$ print-cert 10.222.0.250
|
||||
Could not find tunnel for vpn addr: 10.222.0.250
|
||||
|
||||
$ print-cert -json 10.222.0.250
|
||||
Could not find tunnel for vpn addr: 10.222.0.250
|
||||
|
||||
$ print-cert -pretty 10.222.0.250
|
||||
Could not find tunnel for vpn addr: 10.222.0.250
|
||||
|
||||
$ print-cert not-an-address
|
||||
The provided vpn addr could not be parsed: not-an-address
|
||||
|
||||
$ print-cert -json not-an-address
|
||||
The provided vpn addr could not be parsed: not-an-address
|
||||
|
||||
$ print-cert -pretty not-an-address
|
||||
The provided vpn addr could not be parsed: not-an-address
|
||||
|
||||
$ print-tunnel
|
||||
No vpn address was provided
|
||||
|
||||
$ print-tunnel -json
|
||||
flag provided but not defined: -json
|
||||
Usage:
|
||||
-pretty
|
||||
pretty prints json
|
||||
|
||||
$ print-tunnel -pretty
|
||||
No vpn address was provided
|
||||
|
||||
$ print-tunnel 10.222.0.9
|
||||
Could not find tunnel for vpn addr: 10.222.0.9
|
||||
|
||||
$ print-tunnel -json 10.222.0.9
|
||||
flag provided but not defined: -json
|
||||
Usage:
|
||||
-pretty
|
||||
pretty prints json
|
||||
|
||||
$ print-tunnel -pretty 10.222.0.9
|
||||
Could not find tunnel for vpn addr: 10.222.0.9
|
||||
|
||||
$ print-tunnel 10.222.0.250
|
||||
Could not find tunnel for vpn addr: 10.222.0.250
|
||||
|
||||
$ print-tunnel -json 10.222.0.250
|
||||
flag provided but not defined: -json
|
||||
Usage:
|
||||
-pretty
|
||||
pretty prints json
|
||||
|
||||
$ print-tunnel -pretty 10.222.0.250
|
||||
Could not find tunnel for vpn addr: 10.222.0.250
|
||||
|
||||
$ print-relays
|
||||
{"Relays":null}
|
||||
|
||||
$ print-relays -json
|
||||
flag provided but not defined: -json
|
||||
Usage:
|
||||
-pretty
|
||||
pretty prints json
|
||||
|
||||
$ print-relays -pretty
|
||||
{
|
||||
"Relays": null
|
||||
}
|
||||
|
||||
$ query-lighthouse
|
||||
No vpn address was provided
|
||||
|
||||
$ query-lighthouse -json
|
||||
The provided vpn address could not be parsed: -json
|
||||
|
||||
$ query-lighthouse -pretty
|
||||
The provided vpn address could not be parsed: -pretty
|
||||
|
||||
$ query-lighthouse 10.222.0.9
|
||||
{}
|
||||
|
||||
$ query-lighthouse -json 10.222.0.9
|
||||
The provided vpn address could not be parsed: -json
|
||||
|
||||
$ query-lighthouse -pretty 10.222.0.9
|
||||
The provided vpn address could not be parsed: -pretty
|
||||
|
||||
$ query-lighthouse not-an-address
|
||||
The provided vpn address could not be parsed: not-an-address
|
||||
|
||||
$ query-lighthouse -json not-an-address
|
||||
The provided vpn address could not be parsed: -json
|
||||
|
||||
$ query-lighthouse -pretty not-an-address
|
||||
The provided vpn address could not be parsed: -pretty
|
||||
|
||||
$ log-level
|
||||
Log level is not reconfigurable on this logger
|
||||
|
||||
$ log-level -json
|
||||
Log level is not reconfigurable on this logger
|
||||
|
||||
$ log-level -pretty
|
||||
Log level is not reconfigurable on this logger
|
||||
|
||||
$ log-level debug
|
||||
Log level is not reconfigurable on this logger
|
||||
|
||||
$ log-level -json debug
|
||||
Log level is not reconfigurable on this logger
|
||||
|
||||
$ log-level -pretty debug
|
||||
Log level is not reconfigurable on this logger
|
||||
|
||||
$ log-level bogus
|
||||
Log level is not reconfigurable on this logger
|
||||
|
||||
$ log-level -json bogus
|
||||
Log level is not reconfigurable on this logger
|
||||
|
||||
$ log-level -pretty bogus
|
||||
Log level is not reconfigurable on this logger
|
||||
|
||||
$ log-level info
|
||||
Log level is not reconfigurable on this logger
|
||||
|
||||
$ log-level -json info
|
||||
Log level is not reconfigurable on this logger
|
||||
|
||||
$ log-level -pretty info
|
||||
Log level is not reconfigurable on this logger
|
||||
|
||||
$ log-format
|
||||
Log format is not reconfigurable on this logger
|
||||
|
||||
$ log-format -json
|
||||
Log format is not reconfigurable on this logger
|
||||
|
||||
$ log-format -pretty
|
||||
Log format is not reconfigurable on this logger
|
||||
|
||||
$ log-format json
|
||||
Log format is not reconfigurable on this logger
|
||||
|
||||
$ log-format -json json
|
||||
Log format is not reconfigurable on this logger
|
||||
|
||||
$ log-format -pretty json
|
||||
Log format is not reconfigurable on this logger
|
||||
|
||||
$ log-format text
|
||||
Log format is not reconfigurable on this logger
|
||||
|
||||
$ log-format -json text
|
||||
Log format is not reconfigurable on this logger
|
||||
|
||||
$ log-format -pretty text
|
||||
Log format is not reconfigurable on this logger
|
||||
|
||||
$ change-remote
|
||||
No vpn address was provided
|
||||
|
||||
$ change-remote -json
|
||||
flag provided but not defined: -json
|
||||
Usage:
|
||||
-address string
|
||||
The new remote address, ip:port
|
||||
|
||||
$ change-remote -pretty
|
||||
flag provided but not defined: -pretty
|
||||
Usage:
|
||||
-address string
|
||||
The new remote address, ip:port
|
||||
|
||||
$ change-remote 10.222.0.9
|
||||
No address was provided
|
||||
|
||||
$ change-remote -json 10.222.0.9
|
||||
flag provided but not defined: -json
|
||||
Usage:
|
||||
-address string
|
||||
The new remote address, ip:port
|
||||
|
||||
$ change-remote -pretty 10.222.0.9
|
||||
flag provided but not defined: -pretty
|
||||
Usage:
|
||||
-address string
|
||||
The new remote address, ip:port
|
||||
|
||||
$ change-remote -address 10.94.0.10:4242 10.222.0.9
|
||||
Could not find tunnel for vpn address: 10.222.0.9
|
||||
|
||||
$ change-remote -json -address 10.94.0.10:4242 10.222.0.9
|
||||
flag provided but not defined: -json
|
||||
Usage:
|
||||
-address string
|
||||
The new remote address, ip:port
|
||||
|
||||
$ change-remote -pretty -address 10.94.0.10:4242 10.222.0.9
|
||||
flag provided but not defined: -pretty
|
||||
Usage:
|
||||
-address string
|
||||
The new remote address, ip:port
|
||||
|
||||
$ change-remote -address 10.94.0.10:4242 10.222.0.250
|
||||
Could not find tunnel for vpn address: 10.222.0.250
|
||||
|
||||
$ change-remote -json -address 10.94.0.10:4242 10.222.0.250
|
||||
flag provided but not defined: -json
|
||||
Usage:
|
||||
-address string
|
||||
The new remote address, ip:port
|
||||
|
||||
$ change-remote -pretty -address 10.94.0.10:4242 10.222.0.250
|
||||
flag provided but not defined: -pretty
|
||||
Usage:
|
||||
-address string
|
||||
The new remote address, ip:port
|
||||
|
||||
$ create-tunnel
|
||||
No vpn address was provided
|
||||
|
||||
$ create-tunnel -json
|
||||
flag provided but not defined: -json
|
||||
Usage:
|
||||
-address string
|
||||
Optionally provide a real remote address, ip:port
|
||||
|
||||
$ create-tunnel -pretty
|
||||
flag provided but not defined: -pretty
|
||||
Usage:
|
||||
-address string
|
||||
Optionally provide a real remote address, ip:port
|
||||
|
||||
$ create-tunnel 10.222.0.9
|
||||
Tunnel already handshaking
|
||||
|
||||
$ create-tunnel -json 10.222.0.9
|
||||
flag provided but not defined: -json
|
||||
Usage:
|
||||
-address string
|
||||
Optionally provide a real remote address, ip:port
|
||||
|
||||
$ create-tunnel -pretty 10.222.0.9
|
||||
flag provided but not defined: -pretty
|
||||
Usage:
|
||||
-address string
|
||||
Optionally provide a real remote address, ip:port
|
||||
|
||||
$ create-tunnel -address 10.94.0.10:4242 10.222.0.10
|
||||
Created
|
||||
|
||||
$ create-tunnel -json -address 10.94.0.10:4242 10.222.0.10
|
||||
flag provided but not defined: -json
|
||||
Usage:
|
||||
-address string
|
||||
Optionally provide a real remote address, ip:port
|
||||
|
||||
$ create-tunnel -pretty -address 10.94.0.10:4242 10.222.0.10
|
||||
flag provided but not defined: -pretty
|
||||
Usage:
|
||||
-address string
|
||||
Optionally provide a real remote address, ip:port
|
||||
|
||||
$ create-tunnel not-an-address
|
||||
The provided vpn address could not be parsed: not-an-address
|
||||
|
||||
$ create-tunnel -json not-an-address
|
||||
flag provided but not defined: -json
|
||||
Usage:
|
||||
-address string
|
||||
Optionally provide a real remote address, ip:port
|
||||
|
||||
$ create-tunnel -pretty not-an-address
|
||||
flag provided but not defined: -pretty
|
||||
Usage:
|
||||
-address string
|
||||
Optionally provide a real remote address, ip:port
|
||||
|
||||
$ close-tunnel
|
||||
No vpn address was provided
|
||||
|
||||
$ close-tunnel -json
|
||||
flag provided but not defined: -json
|
||||
Usage:
|
||||
-local-only
|
||||
Disables notifying the remote that the tunnel is shutting down
|
||||
|
||||
$ close-tunnel -pretty
|
||||
flag provided but not defined: -pretty
|
||||
Usage:
|
||||
-local-only
|
||||
Disables notifying the remote that the tunnel is shutting down
|
||||
|
||||
$ close-tunnel 10.222.0.250
|
||||
Could not find tunnel for vpn address: 10.222.0.250
|
||||
|
||||
$ close-tunnel -json 10.222.0.250
|
||||
flag provided but not defined: -json
|
||||
Usage:
|
||||
-local-only
|
||||
Disables notifying the remote that the tunnel is shutting down
|
||||
|
||||
$ close-tunnel -pretty 10.222.0.250
|
||||
flag provided but not defined: -pretty
|
||||
Usage:
|
||||
-local-only
|
||||
Disables notifying the remote that the tunnel is shutting down
|
||||
|
||||
$ close-tunnel not-an-address
|
||||
The provided vpn address could not be parsed: not-an-address
|
||||
|
||||
$ close-tunnel -json not-an-address
|
||||
flag provided but not defined: -json
|
||||
Usage:
|
||||
-local-only
|
||||
Disables notifying the remote that the tunnel is shutting down
|
||||
|
||||
$ close-tunnel -pretty not-an-address
|
||||
flag provided but not defined: -pretty
|
||||
Usage:
|
||||
-local-only
|
||||
Disables notifying the remote that the tunnel is shutting down
|
||||
|
||||
$ help
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
$ help -json
|
||||
Command not available -json
|
||||
|
||||
$ help -pretty
|
||||
Command not available -pretty
|
||||
|
||||
$ help print-cert
|
||||
print-cert - Prints the current certificate being used or the certificate for the provided vpn addr
|
||||
-json
|
||||
outputs as json
|
||||
-pretty
|
||||
pretty prints json, assumes -json
|
||||
-raw
|
||||
raw prints the PEM encoded certificate, not compatible with -json or -pretty
|
||||
|
||||
$ help -json print-cert
|
||||
Command not available -json
|
||||
|
||||
$ help -pretty print-cert
|
||||
Command not available -pretty
|
||||
|
||||
$ reload
|
||||
Reloading config
|
||||
Config reload failed, the new config did not load: no config files found at
|
||||
|
||||
$ reload -json
|
||||
Reloading config
|
||||
Config reload failed, the new config did not load: no config files found at
|
||||
|
||||
$ reload -pretty
|
||||
Reloading config
|
||||
Config reload failed, the new config did not load: no config files found at
|
||||
|
||||
@@ -7,7 +7,6 @@ require (
|
||||
filippo.io/bigmod v0.1.0
|
||||
github.com/DefinedNet/netlink v1.3.2-dn.1
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
|
||||
github.com/armon/go-radix v1.0.0
|
||||
github.com/flynn/noise v1.1.0
|
||||
github.com/gaissmai/bart v0.29.0
|
||||
github.com/gogo/protobuf v1.3.2
|
||||
|
||||
@@ -12,8 +12,6 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
|
||||
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/slackhq/nebula/commands"
|
||||
"github.com/slackhq/nebula/config"
|
||||
"github.com/slackhq/nebula/cpupick"
|
||||
"github.com/slackhq/nebula/noiseutil"
|
||||
@@ -71,7 +72,11 @@ func Main(c *config.C, configTest bool, buildVersion string, l *slog.Logger, dev
|
||||
}
|
||||
l.Info("Firewall started", "firewallHashes", fw.GetRuleHashes())
|
||||
|
||||
ssh, err := sshd.NewSSHServer(ctx, l.With("subsystem", "sshd"))
|
||||
// The command registry is shared by every surface that exposes these commands, so a
|
||||
// command cannot reach one of them without reaching the others.
|
||||
cmdRegistry := commands.New()
|
||||
|
||||
ssh, err := sshd.NewSSHServer(ctx, l.With("subsystem", "sshd"), cmdRegistry)
|
||||
if err != nil {
|
||||
return nil, util.ContextualizeIfNeeded("Error while creating SSH server", err)
|
||||
}
|
||||
@@ -343,13 +348,14 @@ func Main(c *config.C, configTest bool, buildVersion string, l *slog.Logger, dev
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
attachCommands(l, c, ssh, ifce)
|
||||
attachCommands(l, c, cmdRegistry, ifce)
|
||||
|
||||
networkChanges := udp.NewNetworkChangeMonitor(ctx, l, c)
|
||||
|
||||
return &Control{
|
||||
state: StateReady,
|
||||
f: ifce,
|
||||
commands: cmdRegistry,
|
||||
l: l,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
|
||||
@@ -1,59 +1,17 @@
|
||||
package nebula
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"maps"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/slackhq/nebula/config"
|
||||
"github.com/slackhq/nebula/header"
|
||||
"github.com/slackhq/nebula/logging"
|
||||
"github.com/slackhq/nebula/sshd"
|
||||
)
|
||||
|
||||
type sshListHostMapFlags struct {
|
||||
Json bool
|
||||
Pretty bool
|
||||
ByIndex bool
|
||||
}
|
||||
|
||||
type sshPrintCertFlags struct {
|
||||
Json bool
|
||||
Pretty bool
|
||||
Raw bool
|
||||
}
|
||||
|
||||
type sshPrintTunnelFlags struct {
|
||||
Pretty bool
|
||||
}
|
||||
|
||||
type sshChangeRemoteFlags struct {
|
||||
Address string
|
||||
}
|
||||
|
||||
type sshCloseTunnelFlags struct {
|
||||
LocalOnly bool
|
||||
}
|
||||
|
||||
type sshCreateTunnelFlags struct {
|
||||
Address string
|
||||
}
|
||||
|
||||
type sshDeviceInfoFlags struct {
|
||||
Json bool
|
||||
Pretty bool
|
||||
}
|
||||
|
||||
func wireSSHReload(l *slog.Logger, ssh *sshd.SSHServer, c *config.C) {
|
||||
c.RegisterReloadCallback(func(c *config.C, r *config.Reload) {
|
||||
if c.GetBool("sshd.enabled", false) {
|
||||
@@ -227,704 +185,3 @@ func configSSH(l *slog.Logger, ssh *sshd.SSHServer, c *config.C, r *config.Reloa
|
||||
|
||||
return runner, nil
|
||||
}
|
||||
|
||||
func attachCommands(l *slog.Logger, c *config.C, ssh *sshd.SSHServer, f *Interface) {
|
||||
ssh.RegisterCommand(&sshd.Command{
|
||||
Name: "list-hostmap",
|
||||
ShortDescription: "List all known previously connected hosts",
|
||||
Flags: func() (*flag.FlagSet, any) {
|
||||
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 sshd.StringWriter) error {
|
||||
return sshListHostMap(f.hostMap, fs, w)
|
||||
},
|
||||
})
|
||||
|
||||
ssh.RegisterCommand(&sshd.Command{
|
||||
Name: "list-pending-hostmap",
|
||||
ShortDescription: "List all handshaking hosts",
|
||||
Flags: func() (*flag.FlagSet, any) {
|
||||
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 sshd.StringWriter) error {
|
||||
return sshListHostMap(f.handshakeManager, fs, w)
|
||||
},
|
||||
})
|
||||
|
||||
ssh.RegisterCommand(&sshd.Command{
|
||||
Name: "list-lighthouse-addrmap",
|
||||
ShortDescription: "List all lighthouse map entries",
|
||||
Flags: func() (*flag.FlagSet, any) {
|
||||
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")
|
||||
return fl, &s
|
||||
},
|
||||
Callback: func(fs any, a []string, w sshd.StringWriter) error {
|
||||
return sshListLighthouseMap(f.lightHouse, fs, w)
|
||||
},
|
||||
})
|
||||
|
||||
ssh.RegisterCommand(&sshd.Command{
|
||||
Name: "reload",
|
||||
ShortDescription: "Reloads configuration from disk, same as sending HUP to the process",
|
||||
Callback: func(fs any, a []string, w sshd.StringWriter) error {
|
||||
return sshReload(l, c, w)
|
||||
},
|
||||
})
|
||||
|
||||
ssh.RegisterCommand(&sshd.Command{
|
||||
Name: "log-level",
|
||||
ShortDescription: "Gets or sets the current log level",
|
||||
Callback: func(fs any, a []string, w sshd.StringWriter) error {
|
||||
return sshLogLevel(l, fs, a, w)
|
||||
},
|
||||
})
|
||||
|
||||
ssh.RegisterCommand(&sshd.Command{
|
||||
Name: "log-format",
|
||||
ShortDescription: "Gets or sets the current log format",
|
||||
Callback: func(fs any, a []string, w sshd.StringWriter) error {
|
||||
return sshLogFormat(l, fs, a, w)
|
||||
},
|
||||
})
|
||||
|
||||
ssh.RegisterCommand(&sshd.Command{
|
||||
Name: "version",
|
||||
ShortDescription: "Prints the currently running version of nebula",
|
||||
Callback: func(fs any, a []string, w sshd.StringWriter) error {
|
||||
return sshVersion(f, fs, a, w)
|
||||
},
|
||||
})
|
||||
|
||||
ssh.RegisterCommand(&sshd.Command{
|
||||
Name: "device-info",
|
||||
ShortDescription: "Prints information about the network device.",
|
||||
Flags: func() (*flag.FlagSet, any) {
|
||||
fl := flag.NewFlagSet("", flag.ContinueOnError)
|
||||
s := sshDeviceInfoFlags{}
|
||||
fl.BoolVar(&s.Json, "json", false, "outputs as json with more information")
|
||||
fl.BoolVar(&s.Pretty, "pretty", false, "pretty prints json, assumes -json")
|
||||
return fl, &s
|
||||
},
|
||||
Callback: func(fs any, a []string, w sshd.StringWriter) error {
|
||||
return sshDeviceInfo(f, fs, w)
|
||||
},
|
||||
})
|
||||
|
||||
ssh.RegisterCommand(&sshd.Command{
|
||||
Name: "print-cert",
|
||||
ShortDescription: "Prints the current certificate being used or the certificate for the provided vpn addr",
|
||||
Flags: func() (*flag.FlagSet, any) {
|
||||
fl := flag.NewFlagSet("", flag.ContinueOnError)
|
||||
s := sshPrintCertFlags{}
|
||||
fl.BoolVar(&s.Json, "json", false, "outputs as 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")
|
||||
return fl, &s
|
||||
},
|
||||
Callback: func(fs any, a []string, w sshd.StringWriter) error {
|
||||
return sshPrintCert(f, fs, a, w)
|
||||
},
|
||||
})
|
||||
|
||||
ssh.RegisterCommand(&sshd.Command{
|
||||
Name: "print-tunnel",
|
||||
ShortDescription: "Prints json details about a tunnel for the provided vpn addr",
|
||||
Flags: func() (*flag.FlagSet, any) {
|
||||
fl := flag.NewFlagSet("", flag.ContinueOnError)
|
||||
s := sshPrintTunnelFlags{}
|
||||
fl.BoolVar(&s.Pretty, "pretty", false, "pretty prints json")
|
||||
return fl, &s
|
||||
},
|
||||
Callback: func(fs any, a []string, w sshd.StringWriter) error {
|
||||
return sshPrintTunnel(f, fs, a, w)
|
||||
},
|
||||
})
|
||||
|
||||
ssh.RegisterCommand(&sshd.Command{
|
||||
Name: "print-relays",
|
||||
ShortDescription: "Prints json details about all relay info",
|
||||
Flags: func() (*flag.FlagSet, any) {
|
||||
fl := flag.NewFlagSet("", flag.ContinueOnError)
|
||||
s := sshPrintTunnelFlags{}
|
||||
fl.BoolVar(&s.Pretty, "pretty", false, "pretty prints json")
|
||||
return fl, &s
|
||||
},
|
||||
Callback: func(fs any, a []string, w sshd.StringWriter) error {
|
||||
return sshPrintRelays(f, fs, a, w)
|
||||
},
|
||||
})
|
||||
|
||||
ssh.RegisterCommand(&sshd.Command{
|
||||
Name: "change-remote",
|
||||
ShortDescription: "Changes the remote address used in the tunnel for the provided vpn addr",
|
||||
Flags: func() (*flag.FlagSet, any) {
|
||||
fl := flag.NewFlagSet("", flag.ContinueOnError)
|
||||
s := sshChangeRemoteFlags{}
|
||||
fl.StringVar(&s.Address, "address", "", "The new remote address, ip:port")
|
||||
return fl, &s
|
||||
},
|
||||
Callback: func(fs any, a []string, w sshd.StringWriter) error {
|
||||
return sshChangeRemote(f, fs, a, w)
|
||||
},
|
||||
})
|
||||
|
||||
ssh.RegisterCommand(&sshd.Command{
|
||||
Name: "close-tunnel",
|
||||
ShortDescription: "Closes a tunnel for the provided vpn addr",
|
||||
Flags: func() (*flag.FlagSet, any) {
|
||||
fl := flag.NewFlagSet("", flag.ContinueOnError)
|
||||
s := sshCloseTunnelFlags{}
|
||||
fl.BoolVar(&s.LocalOnly, "local-only", false, "Disables notifying the remote that the tunnel is shutting down")
|
||||
return fl, &s
|
||||
},
|
||||
Callback: func(fs any, a []string, w sshd.StringWriter) error {
|
||||
return sshCloseTunnel(f, fs, a, w)
|
||||
},
|
||||
})
|
||||
|
||||
ssh.RegisterCommand(&sshd.Command{
|
||||
Name: "create-tunnel",
|
||||
ShortDescription: "Creates a tunnel for the provided vpn address",
|
||||
Help: "The lighthouses will be queried for real addresses but you can provide one as well.",
|
||||
Flags: func() (*flag.FlagSet, any) {
|
||||
fl := flag.NewFlagSet("", flag.ContinueOnError)
|
||||
s := sshCreateTunnelFlags{}
|
||||
fl.StringVar(&s.Address, "address", "", "Optionally provide a real remote address, ip:port ")
|
||||
return fl, &s
|
||||
},
|
||||
Callback: func(fs any, a []string, w sshd.StringWriter) error {
|
||||
return sshCreateTunnel(f, fs, a, w)
|
||||
},
|
||||
})
|
||||
|
||||
ssh.RegisterCommand(&sshd.Command{
|
||||
Name: "query-lighthouse",
|
||||
ShortDescription: "Query the lighthouses for the provided vpn address",
|
||||
Help: "This command is asynchronous. Only currently known udp addresses will be printed.",
|
||||
Callback: func(fs any, a []string, w sshd.StringWriter) error {
|
||||
return sshQueryLighthouse(f, fs, a, w)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func sshListHostMap(hl controlHostLister, a any, w sshd.StringWriter) error {
|
||||
fs, ok := a.(*sshListHostMapFlags)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
var hm []ControlHostInfo
|
||||
if fs.ByIndex {
|
||||
hm = listHostMapIndexes(hl)
|
||||
} else {
|
||||
hm = listHostMapHosts(hl)
|
||||
}
|
||||
|
||||
sort.Slice(hm, func(i, j int) bool {
|
||||
return hm[i].VpnAddrs[0].Compare(hm[j].VpnAddrs[0]) < 0
|
||||
})
|
||||
|
||||
if fs.Json || fs.Pretty {
|
||||
js := json.NewEncoder(w.GetWriter())
|
||||
if fs.Pretty {
|
||||
js.SetIndent("", " ")
|
||||
}
|
||||
|
||||
err := js.Encode(hm)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
} else {
|
||||
for _, v := range hm {
|
||||
err := w.WriteLine(fmt.Sprintf("%s: %s", v.VpnAddrs, v.RemoteAddrs))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func sshListLighthouseMap(lightHouse *LightHouse, a any, w sshd.StringWriter) error {
|
||||
fs, ok := a.(*sshListHostMapFlags)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
type lighthouseInfo struct {
|
||||
VpnAddr string `json:"vpnAddr"`
|
||||
Addrs *CacheMap `json:"addrs"`
|
||||
}
|
||||
|
||||
lightHouse.RLock()
|
||||
addrMap := make([]lighthouseInfo, len(lightHouse.addrMap))
|
||||
x := 0
|
||||
for k, v := range lightHouse.addrMap {
|
||||
addrMap[x] = lighthouseInfo{
|
||||
VpnAddr: k.String(),
|
||||
Addrs: v.CopyCache(),
|
||||
}
|
||||
x++
|
||||
}
|
||||
lightHouse.RUnlock()
|
||||
|
||||
sort.Slice(addrMap, func(i, j int) bool {
|
||||
return strings.Compare(addrMap[i].VpnAddr, addrMap[j].VpnAddr) < 0
|
||||
})
|
||||
|
||||
if fs.Json || fs.Pretty {
|
||||
js := json.NewEncoder(w.GetWriter())
|
||||
if fs.Pretty {
|
||||
js.SetIndent("", " ")
|
||||
}
|
||||
|
||||
err := js.Encode(addrMap)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
} else {
|
||||
for _, v := range addrMap {
|
||||
b, err := json.Marshal(v.Addrs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = w.WriteLine(fmt.Sprintf("%s: %s", v.VpnAddr, string(b)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func sshVersion(ifce *Interface, fs any, a []string, w sshd.StringWriter) error {
|
||||
return w.WriteLine(fmt.Sprintf("%s", ifce.version))
|
||||
}
|
||||
|
||||
func sshQueryLighthouse(ifce *Interface, fs any, a []string, w sshd.StringWriter) error {
|
||||
if len(a) == 0 {
|
||||
return w.WriteLine("No vpn address was provided")
|
||||
}
|
||||
|
||||
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]))
|
||||
}
|
||||
|
||||
var cm *CacheMap
|
||||
rl := ifce.lightHouse.Query(vpnAddr)
|
||||
if rl != nil {
|
||||
cm = rl.CopyCache()
|
||||
}
|
||||
return json.NewEncoder(w.GetWriter()).Encode(cm)
|
||||
}
|
||||
|
||||
func sshCloseTunnel(ifce *Interface, fs any, a []string, w sshd.StringWriter) error {
|
||||
flags, ok := fs.(*sshCloseTunnelFlags)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(a) == 0 {
|
||||
return w.WriteLine("No vpn address was provided")
|
||||
}
|
||||
|
||||
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)
|
||||
if hostInfo == nil {
|
||||
return w.WriteLine(fmt.Sprintf("Could not find tunnel for vpn address: %v", a[0]))
|
||||
}
|
||||
|
||||
if !flags.LocalOnly {
|
||||
ifce.send(
|
||||
header.CloseTunnel,
|
||||
0,
|
||||
hostInfo.ConnectionState,
|
||||
hostInfo,
|
||||
[]byte{},
|
||||
make([]byte, 12, 12),
|
||||
make([]byte, mtu),
|
||||
)
|
||||
}
|
||||
|
||||
ifce.closeTunnel(hostInfo)
|
||||
return w.WriteLine("Closed")
|
||||
}
|
||||
|
||||
func sshCreateTunnel(ifce *Interface, fs any, a []string, w sshd.StringWriter) error {
|
||||
flags, ok := fs.(*sshCreateTunnelFlags)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(a) == 0 {
|
||||
return w.WriteLine("No vpn address was provided")
|
||||
}
|
||||
|
||||
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)
|
||||
if hostInfo != nil {
|
||||
return w.WriteLine(fmt.Sprintf("Tunnel already exists"))
|
||||
}
|
||||
|
||||
hostInfo = ifce.handshakeManager.QueryVpnAddr(vpnAddr)
|
||||
if hostInfo != nil {
|
||||
return w.WriteLine(fmt.Sprintf("Tunnel already handshaking"))
|
||||
}
|
||||
|
||||
var addr netip.AddrPort
|
||||
if flags.Address != "" {
|
||||
addr, err = netip.ParseAddrPort(flags.Address)
|
||||
if err != nil {
|
||||
return w.WriteLine("Address could not be parsed")
|
||||
}
|
||||
}
|
||||
|
||||
// Set the remote from the callback, which StartHandshake runs under the handshake manager lock
|
||||
// before the hostinfo reaches the handshake loop or the static-host trigger, so the address is
|
||||
// in the RemoteList before the first handshake is sent.
|
||||
ifce.handshakeManager.StartHandshake(vpnAddr, func(hh *HandshakeHostInfo) {
|
||||
if addr.IsValid() {
|
||||
hh.hostinfo.SetRemote(addr)
|
||||
}
|
||||
})
|
||||
|
||||
return w.WriteLine("Created")
|
||||
}
|
||||
|
||||
func sshChangeRemote(ifce *Interface, fs any, a []string, w sshd.StringWriter) error {
|
||||
flags, ok := fs.(*sshChangeRemoteFlags)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(a) == 0 {
|
||||
return w.WriteLine("No vpn address was provided")
|
||||
}
|
||||
|
||||
if flags.Address == "" {
|
||||
return w.WriteLine("No address was provided")
|
||||
}
|
||||
|
||||
addr, err := netip.ParseAddrPort(flags.Address)
|
||||
if err != nil {
|
||||
return w.WriteLine("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)
|
||||
if hostInfo == nil {
|
||||
return w.WriteLine(fmt.Sprintf("Could not find tunnel for vpn address: %v", a[0]))
|
||||
}
|
||||
|
||||
hostInfo.SetRemote(addr)
|
||||
return w.WriteLine("Changed")
|
||||
}
|
||||
|
||||
func sshLogLevel(l *slog.Logger, fs any, a []string, w sshd.StringWriter) error {
|
||||
ctrl, ok := l.Handler().(interface {
|
||||
GetLevel() slog.Level
|
||||
SetLevel(slog.Level)
|
||||
})
|
||||
if !ok {
|
||||
return w.WriteLine("Log level is not reconfigurable on this logger")
|
||||
}
|
||||
|
||||
if len(a) == 0 {
|
||||
return w.WriteLine(fmt.Sprintf("Log level is: %s", logging.LevelName(ctrl.GetLevel())))
|
||||
}
|
||||
|
||||
level, err := logging.ParseLevel(strings.ToLower(a[0]))
|
||||
if err != nil {
|
||||
return w.WriteLine(fmt.Sprintf("Unknown log level %s. Possible log levels: trace, debug, info, warn, error", a))
|
||||
}
|
||||
|
||||
ctrl.SetLevel(level)
|
||||
return w.WriteLine(fmt.Sprintf("Log level is: %s", logging.LevelName(ctrl.GetLevel())))
|
||||
}
|
||||
|
||||
func sshLogFormat(l *slog.Logger, fs any, a []string, w sshd.StringWriter) error {
|
||||
ctrl, ok := l.Handler().(interface {
|
||||
GetFormat() string
|
||||
SetFormat(string) error
|
||||
})
|
||||
if !ok {
|
||||
return w.WriteLine("Log format is not reconfigurable on this logger")
|
||||
}
|
||||
|
||||
if len(a) == 0 {
|
||||
return w.WriteLine(fmt.Sprintf("Log format is: %s", ctrl.GetFormat()))
|
||||
}
|
||||
|
||||
if err := ctrl.SetFormat(strings.ToLower(a[0])); err != nil {
|
||||
return err
|
||||
}
|
||||
return w.WriteLine(fmt.Sprintf("Log format is: %s", ctrl.GetFormat()))
|
||||
}
|
||||
|
||||
func sshPrintCert(ifce *Interface, fs any, a []string, w sshd.StringWriter) error {
|
||||
args, ok := fs.(*sshPrintCertFlags)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
cert := ifce.pki.getCertState().GetDefaultCertificate()
|
||||
if len(a) > 0 {
|
||||
vpnAddr, err := netip.ParseAddr(a[0])
|
||||
if err != nil {
|
||||
return w.WriteLine(fmt.Sprintf("The provided vpn addr could not be parsed: %s", a[0]))
|
||||
}
|
||||
|
||||
if !vpnAddr.IsValid() {
|
||||
return w.WriteLine(fmt.Sprintf("The provided vpn addr could not be parsed: %s", a[0]))
|
||||
}
|
||||
|
||||
hostInfo := ifce.hostMap.QueryVpnAddr(vpnAddr)
|
||||
if hostInfo == nil {
|
||||
return w.WriteLine(fmt.Sprintf("Could not find tunnel for vpn addr: %v", a[0]))
|
||||
}
|
||||
|
||||
cert = hostInfo.GetCert().Certificate
|
||||
}
|
||||
|
||||
if args.Json || args.Pretty {
|
||||
b, err := cert.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if args.Pretty {
|
||||
buf := new(bytes.Buffer)
|
||||
err := json.Indent(buf, b, "", " ")
|
||||
b = buf.Bytes()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return w.WriteBytes(b)
|
||||
}
|
||||
|
||||
if args.Raw {
|
||||
b, err := cert.MarshalPEM()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return w.WriteBytes(b)
|
||||
}
|
||||
|
||||
return w.WriteLine(cert.String())
|
||||
}
|
||||
|
||||
func sshPrintRelays(ifce *Interface, fs any, a []string, w sshd.StringWriter) error {
|
||||
args, ok := fs.(*sshPrintTunnelFlags)
|
||||
if !ok {
|
||||
w.WriteLine(fmt.Sprintf("sshPrintRelays failed to convert args type"))
|
||||
return nil
|
||||
}
|
||||
|
||||
relays := map[uint32]*HostInfo{}
|
||||
ifce.hostMap.Lock()
|
||||
maps.Copy(relays, ifce.hostMap.Relays)
|
||||
ifce.hostMap.Unlock()
|
||||
|
||||
type RelayFor struct {
|
||||
Error error
|
||||
Type string
|
||||
State string
|
||||
PeerAddr netip.Addr
|
||||
LocalIndex uint32
|
||||
RemoteIndex uint32
|
||||
RelayedThrough []netip.Addr
|
||||
}
|
||||
|
||||
type RelayOutput struct {
|
||||
NebulaAddr netip.Addr
|
||||
RelayForAddrs []RelayFor
|
||||
}
|
||||
|
||||
type CmdOutput struct {
|
||||
Relays []*RelayOutput
|
||||
}
|
||||
|
||||
co := CmdOutput{}
|
||||
|
||||
enc := json.NewEncoder(w.GetWriter())
|
||||
|
||||
if args.Pretty {
|
||||
enc.SetIndent("", " ")
|
||||
}
|
||||
|
||||
for k, v := range relays {
|
||||
ro := RelayOutput{NebulaAddr: v.vpnAddrs[0]}
|
||||
co.Relays = append(co.Relays, &ro)
|
||||
relayHI := ifce.hostMap.QueryVpnAddr(v.vpnAddrs[0])
|
||||
if relayHI == nil {
|
||||
ro.RelayForAddrs = append(ro.RelayForAddrs, RelayFor{Error: errors.New("could not find hostinfo")})
|
||||
continue
|
||||
}
|
||||
for _, vpnAddr := range relayHI.relayState.CopyRelayForIps() {
|
||||
rf := RelayFor{Error: nil}
|
||||
r, ok := relayHI.relayState.GetRelayForByAddr(vpnAddr)
|
||||
if ok {
|
||||
t := ""
|
||||
switch r.Type {
|
||||
case ForwardingType:
|
||||
t = "forwarding"
|
||||
case TerminalType:
|
||||
t = "terminal"
|
||||
default:
|
||||
t = "unknown"
|
||||
}
|
||||
|
||||
s := ""
|
||||
switch r.State {
|
||||
case Requested:
|
||||
s = "requested"
|
||||
case Established:
|
||||
s = "established"
|
||||
default:
|
||||
s = "unknown"
|
||||
}
|
||||
|
||||
rf.LocalIndex = r.LocalIndex
|
||||
rf.RemoteIndex = r.RemoteIndex
|
||||
rf.PeerAddr = r.PeerAddr
|
||||
rf.Type = t
|
||||
rf.State = s
|
||||
if rf.LocalIndex != k {
|
||||
rf.Error = fmt.Errorf("hostmap LocalIndex '%v' does not match RelayState LocalIndex", k)
|
||||
}
|
||||
}
|
||||
relayedHI := ifce.hostMap.QueryVpnAddr(vpnAddr)
|
||||
if relayedHI != nil {
|
||||
rf.RelayedThrough = append(rf.RelayedThrough, relayedHI.relayState.CopyRelayIps()...)
|
||||
}
|
||||
|
||||
ro.RelayForAddrs = append(ro.RelayForAddrs, rf)
|
||||
}
|
||||
}
|
||||
err := enc.Encode(co)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func sshPrintTunnel(ifce *Interface, fs any, a []string, w sshd.StringWriter) error {
|
||||
args, ok := fs.(*sshPrintTunnelFlags)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(a) == 0 {
|
||||
return w.WriteLine("No vpn address was provided")
|
||||
}
|
||||
|
||||
vpnAddr, err := netip.ParseAddr(a[0])
|
||||
if err != nil {
|
||||
return w.WriteLine(fmt.Sprintf("The provided vpn addr could not be parsed: %s", a[0]))
|
||||
}
|
||||
|
||||
if !vpnAddr.IsValid() {
|
||||
return w.WriteLine(fmt.Sprintf("The provided vpn addr could not be parsed: %s", a[0]))
|
||||
}
|
||||
|
||||
hostInfo := ifce.hostMap.QueryVpnAddr(vpnAddr)
|
||||
if hostInfo == nil {
|
||||
return w.WriteLine(fmt.Sprintf("Could not find tunnel for vpn addr: %v", a[0]))
|
||||
}
|
||||
|
||||
enc := json.NewEncoder(w.GetWriter())
|
||||
if args.Pretty {
|
||||
enc.SetIndent("", " ")
|
||||
}
|
||||
|
||||
return enc.Encode(copyHostInfo(hostInfo, ifce.hostMap.GetPreferredRanges()))
|
||||
}
|
||||
|
||||
func sshDeviceInfo(ifce *Interface, fs any, w sshd.StringWriter) error {
|
||||
|
||||
data := struct {
|
||||
Name string `json:"name"`
|
||||
Cidr []netip.Prefix `json:"cidr"`
|
||||
}{
|
||||
Name: ifce.inside.Name(),
|
||||
Cidr: make([]netip.Prefix, len(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 {
|
||||
js := json.NewEncoder(w.GetWriter())
|
||||
if flags.Pretty {
|
||||
js.SetIndent("", " ")
|
||||
}
|
||||
|
||||
return js.Encode(data)
|
||||
} else {
|
||||
return w.WriteLine(fmt.Sprintf("name=%v cidr=%v", data.Name, data.Cidr))
|
||||
}
|
||||
}
|
||||
|
||||
// sshReload 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 sshd.StringWriter) error {
|
||||
writeErr := w.WriteLine("Reloading config")
|
||||
res := c.ReloadConfig()
|
||||
res.Log(l)
|
||||
if err := w.WriteLine(res.String()); writeErr == nil {
|
||||
writeErr = err
|
||||
}
|
||||
return writeErr
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package nebula
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/slackhq/nebula/commands"
|
||||
"github.com/slackhq/nebula/config"
|
||||
"github.com/slackhq/nebula/test"
|
||||
)
|
||||
|
||||
// TestSSHCommandSurface is a characterization test for the ssh command surface.
|
||||
// It pins the registered command set, their descriptions, help text and flag definitions
|
||||
// so that moving the registry to a transport neutral package is provably a relocation
|
||||
// rather than a rewrite. Regenerate with `go test . -run TestSSHCommandSurface -update`.
|
||||
func TestSSHCommandSurface(t *testing.T) {
|
||||
got := renderCommandSurface(registeredCommands(t).All())
|
||||
test.Golden(t, filepath.Join("testdata", "ssh_commands.golden"), got)
|
||||
}
|
||||
|
||||
// registeredCommands returns a registry holding every command nebula serves. The
|
||||
// Interface is never dereferenced: attachCommands only closes over it.
|
||||
func registeredCommands(t *testing.T) *commands.Registry {
|
||||
t.Helper()
|
||||
l := test.NewLogger()
|
||||
reg := commands.New()
|
||||
attachCommands(l, config.NewC(l), reg, &Interface{})
|
||||
return reg
|
||||
}
|
||||
|
||||
func renderCommandSurface(cmds []*commands.Command) string {
|
||||
lines := make([]string, 0, len(cmds))
|
||||
for _, cmd := range cmds {
|
||||
b := &bytes.Buffer{}
|
||||
fmt.Fprintf(b, "%s\n", cmd.Name)
|
||||
fmt.Fprintf(b, " short: %s\n", cmd.ShortDescription)
|
||||
fmt.Fprintf(b, " help: %s\n", cmd.Help)
|
||||
|
||||
if cmd.Flags != nil {
|
||||
fl, _ := cmd.Flags()
|
||||
if fl != nil {
|
||||
fb := &bytes.Buffer{}
|
||||
fl.SetOutput(fb)
|
||||
fl.PrintDefaults()
|
||||
fmt.Fprintf(b, " flags:\n%s", fb.String())
|
||||
}
|
||||
}
|
||||
|
||||
lines = append(lines, b.String())
|
||||
}
|
||||
|
||||
// In the order All gives, so the golden pins display order too.
|
||||
return strings.Join(lines, "")
|
||||
}
|
||||
-161
@@ -1,161 +0,0 @@
|
||||
package sshd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/armon/go-radix"
|
||||
)
|
||||
|
||||
// CommandFlags is a function called before help or command execution to parse command line flags
|
||||
// It should return a flag.FlagSet instance and a pointer to the struct that will contain parsed flags
|
||||
type CommandFlags func() (*flag.FlagSet, any)
|
||||
|
||||
// CommandCallback is the function 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
|
||||
// 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.
|
||||
// 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
|
||||
// where appropriate
|
||||
type CommandCallback func(fs any, a []string, w StringWriter) error
|
||||
|
||||
type Command struct {
|
||||
Name string
|
||||
ShortDescription string
|
||||
Help string
|
||||
Flags CommandFlags
|
||||
Callback CommandCallback
|
||||
}
|
||||
|
||||
func execCommand(c *Command, args []string, w StringWriter) error {
|
||||
var (
|
||||
fl *flag.FlagSet
|
||||
fs any
|
||||
)
|
||||
|
||||
if c.Flags != nil {
|
||||
fl, fs = c.Flags()
|
||||
if fl != nil {
|
||||
// SetOutput() here in case fl.Parse dumps usage.
|
||||
fl.SetOutput(w.GetWriter())
|
||||
err := fl.Parse(args)
|
||||
if err != nil {
|
||||
// fl.Parse has dumped error information to the user via the w writer.
|
||||
return err
|
||||
}
|
||||
args = fl.Args()
|
||||
}
|
||||
}
|
||||
|
||||
return c.Callback(fs, args, w)
|
||||
}
|
||||
|
||||
func dumpCommands(c *radix.Tree, w StringWriter) {
|
||||
err := w.WriteLine("Available commands:")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cmds := make([]string, 0)
|
||||
for _, l := range allCommands(c) {
|
||||
cmds = append(cmds, fmt.Sprintf("%s - %s", l.Name, l.ShortDescription))
|
||||
}
|
||||
|
||||
sort.Strings(cmds)
|
||||
_ = w.Write(strings.Join(cmds, "\n") + "\n\n")
|
||||
}
|
||||
|
||||
func lookupCommand(c *radix.Tree, sCmd string) (*Command, error) {
|
||||
cmd, ok := c.Get(sCmd)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
command, ok := cmd.(*Command)
|
||||
if !ok {
|
||||
return nil, errors.New("failed to cast command")
|
||||
}
|
||||
|
||||
return command, nil
|
||||
}
|
||||
|
||||
func matchCommand(c *radix.Tree, cmd string) []string {
|
||||
cmds := make([]string, 0)
|
||||
c.WalkPrefix(cmd, func(found string, v any) bool {
|
||||
cmds = append(cmds, found)
|
||||
return false
|
||||
})
|
||||
sort.Strings(cmds)
|
||||
return cmds
|
||||
}
|
||||
|
||||
func allCommands(c *radix.Tree) []*Command {
|
||||
cmds := make([]*Command, 0)
|
||||
c.WalkPrefix("", func(found string, v any) bool {
|
||||
cmd, ok := v.(*Command)
|
||||
if ok {
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
return false
|
||||
})
|
||||
return cmds
|
||||
}
|
||||
|
||||
func helpCallback(commands *radix.Tree, a []string, w StringWriter) (err error) {
|
||||
// Just typed help
|
||||
if len(a) == 0 {
|
||||
dumpCommands(commands, w)
|
||||
return nil
|
||||
}
|
||||
|
||||
// We are printing a specific commands help text
|
||||
cmd, err := lookupCommand(commands, a[0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if cmd != nil {
|
||||
err = w.WriteLine(fmt.Sprintf("%s - %s", cmd.Name, cmd.ShortDescription))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cmd.Help != "" {
|
||||
err = w.WriteLine(fmt.Sprintf(" %s", cmd.Help))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if cmd.Flags != nil {
|
||||
fs, _ := cmd.Flags()
|
||||
if fs != nil {
|
||||
fs.SetOutput(w.GetWriter())
|
||||
fs.PrintDefaults()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
err = w.WriteLine("Command not available " + a[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkHelpArgs(args []string) bool {
|
||||
for _, a := range args {
|
||||
if a == "-h" || a == "-help" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
+9
-23
@@ -10,7 +10,7 @@ import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/armon/go-radix"
|
||||
"github.com/slackhq/nebula/commands"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
@@ -29,10 +29,9 @@ type SSHServer struct {
|
||||
trustedKeys map[string]map[string]bool
|
||||
trustedCAs []ssh.PublicKey
|
||||
|
||||
// List of available commands
|
||||
helpCommand *Command
|
||||
commands *radix.Tree
|
||||
listener atomic.Pointer[net.Listener]
|
||||
// Available commands.
|
||||
commands *commands.Registry
|
||||
listener atomic.Pointer[net.Listener]
|
||||
|
||||
// runs counts the Runs that are listening. A reload's Run can start before the one it replaced has returned
|
||||
runs atomic.Int32
|
||||
@@ -44,14 +43,14 @@ type SSHServer struct {
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// NewSSHServer creates a new ssh server rigged with default commands and prepares to listen.
|
||||
// The ssh server's context is parented off the supplied ctx so cancelling it
|
||||
// (e.g. on Control.Stop) tears down active sessions and closes the listener.
|
||||
func NewSSHServer(ctx context.Context, l *slog.Logger) (*SSHServer, error) {
|
||||
// NewSSHServer creates a new ssh server serving the supplied command registry and
|
||||
// prepares to listen. The ssh server's context is parented off the supplied ctx so
|
||||
// cancelling it (e.g. on Control.Stop) tears down active sessions and closes the listener.
|
||||
func NewSSHServer(ctx context.Context, l *slog.Logger, reg *commands.Registry) (*SSHServer, error) {
|
||||
s := &SSHServer{
|
||||
trustedKeys: make(map[string]map[string]bool),
|
||||
l: l,
|
||||
commands: radix.New(),
|
||||
commands: reg,
|
||||
ctx: ctx,
|
||||
}
|
||||
|
||||
@@ -97,14 +96,6 @@ func NewSSHServer(ctx context.Context, l *slog.Logger) (*SSHServer, error) {
|
||||
s.authenticate = cc.Authenticate
|
||||
s.config.Store(s.newConfig())
|
||||
|
||||
s.RegisterCommand(&Command{
|
||||
Name: "help",
|
||||
ShortDescription: "prints available commands or help <command> for specific usage info",
|
||||
Callback: func(a any, args []string, w StringWriter) error {
|
||||
return helpCallback(s.commands, args, w)
|
||||
},
|
||||
})
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
@@ -189,11 +180,6 @@ func (s *SSHServer) AddAuthorizedKey(user, pubKey string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterCommand adds a command that can be run by a user, by default only `help` is available
|
||||
func (s *SSHServer) RegisterCommand(c *Command) {
|
||||
s.commands.Insert(c.Name, c)
|
||||
}
|
||||
|
||||
// Run begins listening and accepting connections. Each invocation derives a fresh per-Run context
|
||||
// from the constructor-supplied ctx so a Stop+Run sequence (used by config reload) starts clean
|
||||
// rather than carrying a permanently-cancelled context across runs.
|
||||
|
||||
+12
-44
@@ -1,13 +1,10 @@
|
||||
package sshd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/anmitsu/go-shlex"
|
||||
"github.com/armon/go-radix"
|
||||
"github.com/slackhq/nebula/commands"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
@@ -16,22 +13,22 @@ type session struct {
|
||||
l *slog.Logger
|
||||
c *ssh.ServerConn
|
||||
term *term.Terminal
|
||||
commands *radix.Tree
|
||||
commands *commands.Registry
|
||||
cancel func()
|
||||
}
|
||||
|
||||
func NewSession(commands *radix.Tree, conn *ssh.ServerConn, chans <-chan ssh.NewChannel, cancel func(), l *slog.Logger) *session {
|
||||
func NewSession(reg *commands.Registry, conn *ssh.ServerConn, chans <-chan ssh.NewChannel, cancel func(), l *slog.Logger) *session {
|
||||
s := &session{
|
||||
commands: radix.NewFromMap(commands.ToMap()),
|
||||
commands: reg.Clone(),
|
||||
l: l,
|
||||
c: conn,
|
||||
cancel: cancel,
|
||||
}
|
||||
|
||||
s.commands.Insert("logout", &Command{
|
||||
Name: "logout",
|
||||
s.commands.Register(&commands.Command{
|
||||
Name: commands.Logout,
|
||||
ShortDescription: "Ends the current session",
|
||||
Callback: func(a any, args []string, w StringWriter) error {
|
||||
Callback: func(a any, args []string, w commands.StringWriter) error {
|
||||
s.Close()
|
||||
return nil
|
||||
},
|
||||
@@ -87,7 +84,7 @@ func (s *session) handleRequests(in <-chan *ssh.Request, channel ssh.Channel) {
|
||||
}
|
||||
|
||||
req.Reply(true, nil)
|
||||
s.dispatchCommand(payload.Value, &stringWriter{channel})
|
||||
s.dispatchCommand(payload.Value, commands.NewStringWriter(channel))
|
||||
|
||||
status := struct{ Status uint32 }{uint32(0)}
|
||||
channel.SendRequest("exit-status", false, ssh.Marshal(status))
|
||||
@@ -111,12 +108,11 @@ func (s *session) createTerm(channel ssh.Channel) *term.Terminal {
|
||||
term.AutoCompleteCallback = func(line string, pos int, key rune) (newLine string, newPos int, ok bool) {
|
||||
// key 9 is tab
|
||||
if key == 9 {
|
||||
cmds := matchCommand(s.commands, line)
|
||||
cmds := s.commands.Match(line)
|
||||
if len(cmds) == 1 {
|
||||
return cmds[0] + " ", len(cmds[0]) + 1, true
|
||||
}
|
||||
|
||||
sort.Strings(cmds)
|
||||
term.Write([]byte(strings.Join(cmds, "\n") + "\n\n"))
|
||||
}
|
||||
|
||||
@@ -128,7 +124,7 @@ func (s *session) createTerm(channel ssh.Channel) *term.Terminal {
|
||||
}
|
||||
|
||||
func (s *session) handleInput() {
|
||||
w := &stringWriter{w: s.term}
|
||||
w := commands.NewStringWriter(s.term)
|
||||
for {
|
||||
line, err := s.term.ReadLine()
|
||||
if err != nil {
|
||||
@@ -139,36 +135,8 @@ func (s *session) handleInput() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *session) dispatchCommand(line string, w StringWriter) {
|
||||
args, err := shlex.Split(line, true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(args) == 0 {
|
||||
dumpCommands(s.commands, w)
|
||||
return
|
||||
}
|
||||
|
||||
c, err := lookupCommand(s.commands, args[0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if c == nil {
|
||||
err := w.WriteLine(fmt.Sprintf("did not understand: %s", line))
|
||||
_ = err
|
||||
|
||||
dumpCommands(s.commands, w)
|
||||
return
|
||||
}
|
||||
|
||||
if checkHelpArgs(args) {
|
||||
s.dispatchCommand(fmt.Sprintf("%s %s", "help", c.Name), w)
|
||||
return
|
||||
}
|
||||
|
||||
_ = execCommand(c, args[1:], w)
|
||||
func (s *session) dispatchCommand(line string, w commands.StringWriter) {
|
||||
s.commands.Dispatch(line, w)
|
||||
}
|
||||
|
||||
func (s *session) Close() {
|
||||
|
||||
+2
-1
@@ -13,6 +13,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/slackhq/nebula/commands"
|
||||
"github.com/slackhq/nebula/config"
|
||||
"github.com/slackhq/nebula/sshd"
|
||||
"github.com/slackhq/nebula/test"
|
||||
@@ -44,7 +45,7 @@ func newSSHDReloadHarness(t *testing.T) *sshdReloadHarness {
|
||||
|
||||
h.c = config.NewC(l)
|
||||
require.NoError(t, h.c.LoadString(h.config()))
|
||||
h.server, err = sshd.NewSSHServer(t.Context(), l)
|
||||
h.server, err = sshd.NewSSHServer(t.Context(), l, commands.New())
|
||||
require.NoError(t, err)
|
||||
wireSSHReload(l, h.server, h.c)
|
||||
run, err := configSSH(l, h.server, h.c, nil)
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Update is the -update flag: Golden rewrites its file instead of comparing against it.
|
||||
var Update = flag.Bool("update", false, "rewrite golden files instead of comparing against them")
|
||||
|
||||
// Golden compares got with the file at path, or rewrites the file when -update is set.
|
||||
func Golden(t *testing.T, path, got string) {
|
||||
t.Helper()
|
||||
|
||||
if *Update {
|
||||
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0755))
|
||||
require.NoError(t, os.WriteFile(path, []byte(got), 0644))
|
||||
return
|
||||
}
|
||||
|
||||
want, err := os.ReadFile(path)
|
||||
require.NoError(t, err, "missing golden file, run with -update")
|
||||
assert.Equal(t, string(want), got)
|
||||
}
|
||||
Vendored
+94
@@ -0,0 +1,94 @@
|
||||
change-remote
|
||||
short: Changes the remote address used in the tunnel for the provided vpn addr
|
||||
help:
|
||||
flags:
|
||||
-address string
|
||||
The new remote address, ip:port
|
||||
close-tunnel
|
||||
short: Closes a tunnel for the provided vpn addr
|
||||
help:
|
||||
flags:
|
||||
-local-only
|
||||
Disables notifying the remote that the tunnel is shutting down
|
||||
create-tunnel
|
||||
short: Creates a tunnel for the provided vpn address
|
||||
help: The lighthouses will be queried for real addresses but you can provide one as well.
|
||||
flags:
|
||||
-address string
|
||||
Optionally provide a real remote address, ip:port
|
||||
device-info
|
||||
short: Prints information about the network device.
|
||||
help:
|
||||
flags:
|
||||
-json
|
||||
outputs as json with more information
|
||||
-pretty
|
||||
pretty prints json, assumes -json
|
||||
help
|
||||
short: prints available commands or help <command> for specific usage info
|
||||
help:
|
||||
list-hostmap
|
||||
short: List all known previously connected hosts
|
||||
help:
|
||||
flags:
|
||||
-by-index
|
||||
gets all hosts in the hostmap from the index table
|
||||
-json
|
||||
outputs as json with more information
|
||||
-pretty
|
||||
pretty prints json, assumes -json
|
||||
list-lighthouse-addrmap
|
||||
short: List all lighthouse map entries
|
||||
help:
|
||||
flags:
|
||||
-json
|
||||
outputs as json with more information
|
||||
-pretty
|
||||
pretty prints json, assumes -json
|
||||
list-pending-hostmap
|
||||
short: List all handshaking hosts
|
||||
help:
|
||||
flags:
|
||||
-by-index
|
||||
gets all hosts in the hostmap from the index table
|
||||
-json
|
||||
outputs as json with more information
|
||||
-pretty
|
||||
pretty prints json, assumes -json
|
||||
log-format
|
||||
short: Gets or sets the current log format
|
||||
help:
|
||||
log-level
|
||||
short: Gets or sets the current log level
|
||||
help:
|
||||
print-cert
|
||||
short: Prints the current certificate being used or the certificate for the provided vpn addr
|
||||
help:
|
||||
flags:
|
||||
-json
|
||||
outputs as json
|
||||
-pretty
|
||||
pretty prints json, assumes -json
|
||||
-raw
|
||||
raw prints the PEM encoded certificate, not compatible with -json or -pretty
|
||||
print-relays
|
||||
short: Prints json details about all relay info
|
||||
help:
|
||||
flags:
|
||||
-pretty
|
||||
pretty prints json
|
||||
print-tunnel
|
||||
short: Prints json details about a tunnel for the provided vpn addr
|
||||
help:
|
||||
flags:
|
||||
-pretty
|
||||
pretty prints json
|
||||
query-lighthouse
|
||||
short: Query the lighthouses for the provided vpn address
|
||||
help: This command is asynchronous. Only currently known udp addresses will be printed.
|
||||
reload
|
||||
short: Reloads configuration from disk, same as sending HUP to the process
|
||||
help:
|
||||
version
|
||||
short: Prints the currently running version of nebula
|
||||
help:
|
||||
Reference in new issue
Block a user