Skip to content

Commit

Permalink
implement ssh client in virtctl
Browse files Browse the repository at this point in the history
Signed-off-by: Kevin Wiesmueller <[email protected]>
  • Loading branch information
Kevin Wiesmueller committed Jul 12, 2021
1 parent 3276e54 commit 5559ea1
Show file tree
Hide file tree
Showing 19 changed files with 2,407 additions and 1 deletion.
21 changes: 21 additions & 0 deletions docs/rdp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# RDP into a VirtualMachineInstance

Every VM and VMI provides a `/portforward` subresource that can be used to create a websocket backed
network tunnel to a port inside the instance similar to Kubernetes pods.

One use-case for this subresource is to forward RDP traffic into the VMI either from the CLI
or a web-UI.

## Usage

To connect to a Windows Guest via RDP, first open a `port-forward` tunnel:

```sh
virtctl port-forward vm/win10 udp/3389 tcp/3389
```

Then you can use the tunnel with an RDP client of your preference:

```sh
freerdp /u:Administrator /p:YourPassword /v:127.0.0.1:3389
```
141 changes: 141 additions & 0 deletions docs/ssh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# SSH into a VirtualMachineInstance

Every VM and VMI provides a `/portforward` subresource that can be used to create a websocket backed
network tunnel to a port inside the instance similar to Kubernetes pods.

One use-case for this subresource is to forward SSH traffic into the VMI either from the CLI
or a web-UI.

## Usage

### virtcli

To connect to a VMI from your local machine, `virtcli` provides the `ssh` command as a lightweight
SSH client. Refer to the commands help for more details.

```sh
virtctl ssh testvmi
```

#### Port Forward

If you prefer to use your local OpenSSH client, the `virtctl port-forward` command provides an option
to tunnel a single port to your local Stdout/Stdin.
This allows for the command to be used in the `ProxyCommand` option.

```sh
ssh -o 'ProxyCommand=virtctl port-forward --stdio=true testvmi 22' [email protected]
```

This can also be used with `scp`.

To provide easier access to different VMs you can add the following to your `ssh-config`:

```
Host vmi/*
ProxyCommand virtctl port-forward --stdio %h %p
Host vm/*
ProxyCommand virtctl port-forward --stdio %h %p
```

This allows you to simply call `ssh fedora@vmi/testvmi.default` and your config and virtctl will do the rest.
Using this setup it also becomes trivial to setup different identities for different namespaces inside your `ssh-config`

Note that all traffic sent over those tunnels will be proxied over the Kubernetes controlplane.
A high amount of traffic and connections can increase pressure on the apiserver.
If you need regular high amount of connections and traffic, consider using a dedicated Kubernetes Service instead.

### Example

1. Create VM
```yaml
# ssh-test.vm.yaml
apiVersion: kubevirt.io/v1alpha3
kind: VirtualMachine
metadata:
annotations:
kubevirt.io/latest-observed-api-version: v1alpha3
kubevirt.io/storage-observed-api-version: v1alpha3
name.os.template.kubevirt.io/fedora32: Fedora 31 or higher
name: ssh-test
labels:
app: ssh-test
flavor.template.kubevirt.io/tiny: 'true'
os.template.kubevirt.io/fedora32: 'true'
vm.kubevirt.io/template: fedora-server-tiny-v0.11.3
vm.kubevirt.io/template.namespace: openshift
vm.kubevirt.io/template.revision: '1'
vm.kubevirt.io/template.version: v0.12.4
workload.template.kubevirt.io/server: 'true'
spec:
running: false
template:
metadata:
labels:
flavor.template.kubevirt.io/tiny: 'true'
kubevirt.io/domain: ssh-test
kubevirt.io/size: tiny
os.template.kubevirt.io/fedora32: 'true'
vm.kubevirt.io/name: ssh-test
workload.template.kubevirt.io/server: 'true'
spec:
domain:
cpu:
cores: 1
sockets: 1
threads: 1
devices:
disks:
- disk:
bus: virtio
name: cloudinitdisk
- bootOrder: 1
disk:
bus: virtio
name: rootdisk
interfaces:
- masquerade: {}
model: virtio
name: nic-0
networkInterfaceMultiqueue: true
rng: {}
machine:
type: pc-q35-rhel8.2.0
resources:
requests:
memory: 1Gi
hostname: ssh-test
networks:
- name: nic-0
pod: {}
terminationGracePeriodSeconds: 180
volumes:
- cloudInitNoCloud:
userData: |
#cloud-config
user: fedora
password: ssh-demo
chpasswd:
expire: false
name: cloudinitdisk
- containerDisk:
image: kubevirt/fedora-cloud-container-disk-demo
name: rootdisk
```
```sh
kubectl apply -f ssh-test.vm.yaml
```

2. Start VM
```sh
kubectl virt start ssh-test
```

3. SSH into VM
```sh
kubectl virt ssh --username=fedora ssh-test
```
or
```sh
ssh -o 'ProxyCommand=kubectl virt port-forward --stdio=true ssh-test 22' [email protected]
```
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ require (
golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f
golang.org/x/net v0.0.0-20210119194325-5f4716e94777
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e
google.golang.org/grpc v1.30.0
gopkg.in/cheggaaa/pb.v1 v1.0.28
Expand Down
1 change: 1 addition & 0 deletions pkg/virtctl/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ go_library(
"//pkg/virtctl/imageupload:go_default_library",
"//pkg/virtctl/pause:go_default_library",
"//pkg/virtctl/portforward:go_default_library",
"//pkg/virtctl/ssh:go_default_library",
"//pkg/virtctl/templates:go_default_library",
"//pkg/virtctl/version:go_default_library",
"//pkg/virtctl/vm:go_default_library",
Expand Down
2 changes: 2 additions & 0 deletions pkg/virtctl/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"kubevirt.io/kubevirt/pkg/virtctl/imageupload"
"kubevirt.io/kubevirt/pkg/virtctl/pause"
"kubevirt.io/kubevirt/pkg/virtctl/portforward"
"kubevirt.io/kubevirt/pkg/virtctl/ssh"
"kubevirt.io/kubevirt/pkg/virtctl/templates"
"kubevirt.io/kubevirt/pkg/virtctl/version"
"kubevirt.io/kubevirt/pkg/virtctl/vm"
Expand Down Expand Up @@ -70,6 +71,7 @@ func NewVirtctlCommand() *cobra.Command {
rootCmd.AddCommand(
console.NewCommand(clientConfig),
vnc.NewCommand(clientConfig),
ssh.NewCommand(clientConfig),
portforward.NewCommand(clientConfig),
vm.NewStartCommand(clientConfig),
vm.NewStopCommand(clientConfig),
Expand Down
24 changes: 24 additions & 0 deletions pkg/virtctl/ssh/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")

go_library(
name = "go_default_library",
srcs = [
"native.go",
"resize_unix.go",
"resize_windows.go",
"ssh.go",
"wrapped.go",
],
importpath = "kubevirt.io/kubevirt/pkg/virtctl/ssh",
visibility = ["//visibility:public"],
deps = [
"//pkg/virtctl/templates:go_default_library",
"//staging/src/github.com/golang/glog:go_default_library",
"//staging/src/kubevirt.io/client-go/kubecli:go_default_library",
"//vendor/github.com/spf13/cobra:go_default_library",
"//vendor/golang.org/x/crypto/ssh:go_default_library",
"//vendor/golang.org/x/crypto/ssh/agent:go_default_library",
"//vendor/golang.org/x/term:go_default_library",
"//vendor/k8s.io/client-go/tools/clientcmd:go_default_library",
],
)
169 changes: 169 additions & 0 deletions pkg/virtctl/ssh/native.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package ssh

import (
"fmt"
"io/ioutil"
"net"
"os"

"github.com/golang/glog"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
"golang.org/x/term"
)

func (o *SSH) prepareSSHClient(kind, namespace, name string) (*ssh.Client, error) {
streamer, err := o.prepareSSHTunnel(kind, namespace, name)
if err != nil {
return nil, err
}

conn := streamer.AsConn()
addr := fmt.Sprintf("%s/%s.%s", kind, name, namespace)
authMethods, err := o.getAuthMethods(kind, namespace, name)
if err != nil {
return nil, err
}

sshConn, chans, reqs, err := ssh.NewClientConn(conn,
addr,
&ssh.ClientConfig{
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Auth: authMethods,
User: sshUsername,
},
)
if err != nil {
return nil, err
}

return ssh.NewClient(sshConn, chans, reqs), nil
}

func (o *SSH) getAuthMethods(kind, namespace, name string) ([]ssh.AuthMethod, error) {
var (
methods []ssh.AuthMethod
err error
)

if len(identityFilePath) < 1 {
methods = trySSHAgent(methods)
} else {
methods, err = usePrivateKey(methods)
if err != nil {
return nil, err
}
}

methods = append(methods, ssh.PasswordCallback(func() (secret string, err error) {
password, err := readPassword(fmt.Sprintf("%s@%s/%s.%s's password: ", sshUsername, kind, name, namespace))
fmt.Println()
return string(password), err
}))

return methods, nil
}

func trySSHAgent(methods []ssh.AuthMethod) []ssh.AuthMethod {
socket := os.Getenv("SSH_AUTH_SOCK")
if len(socket) < 1 {
return methods
}
conn, err := net.Dial("unix", socket)
if err != nil {
glog.Error(err)
return methods
}
agentClient := agent.NewClient(conn)

return append(methods, ssh.PublicKeysCallback(agentClient.Signers))
}

func usePrivateKey(methods []ssh.AuthMethod) ([]ssh.AuthMethod, error) {
key, err := ioutil.ReadFile(identityFilePath)
if err != nil {
return methods, err
}

signer, err := ssh.ParsePrivateKey(key)
if _, isPassErr := err.(*ssh.PassphraseMissingError); isPassErr {
signer, err = parsePrivateKeyWithPassphrase(key)
if err != nil {
return methods, err
}
}
return append(methods, ssh.PublicKeys(signer)), nil
}

func parsePrivateKeyWithPassphrase(key []byte) (ssh.Signer, error) {
password, err := readPassword(fmt.Sprintf("Key %s requires a password: ", identityFilePath))
fmt.Println()
if err != nil {
return nil, err
}

return ssh.ParsePrivateKeyWithPassphrase(key, password)
}

func readPassword(reason string) ([]byte, error) {
fmt.Print(reason)
return term.ReadPassword(int(os.Stdin.Fd()))
}

func (o *SSH) startSession(client *ssh.Client) error {
session, err := client.NewSession()
if err != nil {
return err
}
defer session.Close()

session.Stdin = os.Stdin
session.Stderr = os.Stderr
session.Stdout = os.Stdout

restore, err := setupTerminal(int(os.Stdin.Fd()))
if err != nil {
return err
}
defer restore()

if err := requestPty(session); err != nil {
return err
}

if err := session.Shell(); err != nil {
return err
}

err = session.Wait()
if _, exited := err.(*ssh.ExitError); !exited {
return err
}
return nil
}

func setupTerminal(fd int) (func(), error) {
state, err := term.MakeRaw(fd)
if err != nil {
return nil, err
}
return func() { term.Restore(fd, state) }, nil
}

func requestPty(session *ssh.Session) error {
w, h, err := term.GetSize(int(os.Stdin.Fd()))
if err != nil {
return err
}
if err := session.RequestPty(
os.Getenv("TERM"),
h, w,
ssh.TerminalModes{},
); err != nil {
return err
}

go resizeSessionOnWindowChange(session, os.Stdin.Fd())

return nil
}
Loading

0 comments on commit 5559ea1

Please sign in to comment.