Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove unbound ssh option for older clients #1721

Merged
merged 3 commits into from
Nov 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions clab/ssh_config.go.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Containerlab SSH Config for the {{ .TopologyName }} lab

{{- range .Nodes }}
Host {{ .Name }}
{{- if ne .Username ""}}
User {{ .Username }}
{{- end }}
StrictHostKeyChecking=no
UserKnownHostsFile=/dev/null
{{- if ne .SSHConfig.PubkeyAuthentication "" }}
PubkeyAuthentication={{ .SSHConfig.PubkeyAuthentication.String }}
{{- end }}
{{ end }}
37 changes: 22 additions & 15 deletions clab/sshconfig.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package clab

import (
_ "embed"
"os"
"path"
"text/template"

log "github.com/sirupsen/logrus"
"github.com/srl-labs/containerlab/types"
"github.com/srl-labs/containerlab/utils"
"golang.org/x/mod/semver"
)

// SSHConfigTmpl is the top-level data structure for the
Expand All @@ -25,20 +27,10 @@ type SSHConfigNodeTmpl struct {
SSHConfig *types.SSHConfig
}

// tmplSshConfig is the SSH config template.
const tmplSshConfig = `# Containerlab SSH Config for the {{ .TopologyName }} lab

{{- range .Nodes }}
Host {{ .Name }}
{{- if ne .Username ""}}
User {{ .Username }}
{{- end }}
StrictHostKeyChecking=no
UserKnownHostsFile=/dev/null
{{- if ne .SSHConfig.PubkeyAuthentication "" }}
PubkeyAuthentication={{ .SSHConfig.PubkeyAuthentication.String }}
{{- end }}
{{ end }}`
// sshConfigTemplate is the SSH config template.
//
//go:embed ssh_config.go.tpl
var sshConfigTemplate string

// RemoveSSHConfig removes the lab specific ssh config file
func (c *CLab) RemoveSSHConfig(topoPaths *types.TopoPaths) error {
Expand All @@ -63,6 +55,12 @@ func (c *CLab) AddSSHConfig() error {
Nodes: make([]SSHConfigNodeTmpl, 0, len(c.Nodes)),
}

// get the ssh client version to determine if are allowed
// to use the PubkeyAuthentication=unbound
// which is only available in OpenSSH 8.9+
// if we fail to parse the version the return value is going to be empty
sshVersion := utils.GetSSHVersion()

// add the data for all nodes to the template input
for _, n := range c.Nodes {
// get the Kind from the KindRegistry and and extract
Expand All @@ -73,10 +71,19 @@ func (c *CLab) AddSSHConfig() error {
Username: NodeRegistryEntry.Credentials().GetUsername(),
SSHConfig: n.GetSSHConfig(),
}

// if we couldn't parse the ssh version we assume we can't use unbound option
// or if the version is lower than 8.9
// and the node has the PubkeyAuthentication set to unbound
// we set it to empty string since it is not supported by the SSH client
if (sshVersion == "" || semver.Compare("v"+sshVersion, "v8.9") < 0) && nodeData.SSHConfig.PubkeyAuthentication == types.PubkeyAuthValueUnbound {
nodeData.SSHConfig.PubkeyAuthentication = ""
}

tmpl.Nodes = append(tmpl.Nodes, nodeData)
}

t, err := template.New("sshconfig").Parse(tmplSshConfig)
t, err := template.New("sshconfig").Parse(sshConfigTemplate)
if err != nil {
return err
}
Expand Down
6 changes: 6 additions & 0 deletions docs/manual/kinds/vr-sros.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,12 @@ A:admin@sros1# info | match boot-goo

By combining file bindings and the automatic script execution of SROS it is possible to create a workaround for persistent BOF settings.

#### SSH keys

Containerlab v0.48.0+ supports SSH key injection into the Nokia SR OS nodes. First containerlab retrieves all public keys from `~/.ssh` directory of a user running containerlab, then it retrieves public keys from the ssh agent if one is running.

Next it will filter out public keys that are not of RSA/ECDSA type. The remaining valid public keys will be configured for the admin user of the Nokia SR OS node. This will enable key-based authentication next time you connect to the node.

### License

Path to a valid license must be provided for all Nokia SR OS nodes with a [`license`](../nodes.md#license) directive.
Expand Down
34 changes: 34 additions & 0 deletions utils/ssh.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package utils

import (
"os/exec"
"regexp"

log "github.com/sirupsen/logrus"
)

// GetSSHVersion returns the version of the ssh client
// that is installed on the host.
func GetSSHVersion() string {
cmd := exec.Command("ssh", "-V")
out, err := cmd.CombinedOutput()
if err != nil {
log.Warnf("Failed to get ssh client version: %v", err)
}

version := parseSSHVersion(string(out))

return version
}

func parseSSHVersion(in string) string {
re := regexp.MustCompile(`OpenSSH_(\d+\.\d+).+`)
match := re.FindStringSubmatch(in)

if len(match) < 2 {
log.Warnf("Failed to parse ssh version from string: %s", in)
return ""
}

return match[1]
}
38 changes: 38 additions & 0 deletions utils/ssh_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package utils

import (
"testing"
)

func TestParseSSHVersion(t *testing.T) {
tests := []struct {
name string
in string
want string
}{
{
name: "valid version",
in: "OpenSSH_8.1p1 Debian-8, OpenSSL 1.1.1d 10 Sep 2019",
want: "8.1",
},
{
name: "another valid version",
in: "OpenSSH_8.9p1 Ubuntu-3ubuntu0.3, OpenSSL 3.0.2 15 Mar 2022",
want: "8.9",
},
{
name: "invalid version",
in: "Invalid version string",
want: "",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := parseSSHVersion(tt.in)
if got != tt.want {
t.Errorf("parseSSHVersion() = %v, want %v", got, tt.want)
}
})
}
}
Loading