Skip to content

Commit

Permalink
Use ssh via vpn from metal-lib (#243)
Browse files Browse the repository at this point in the history
  • Loading branch information
majst01 authored Jul 25, 2023
1 parent 853620f commit 0cea1d5
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 314 deletions.
69 changes: 32 additions & 37 deletions cmd/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -1780,51 +1780,46 @@ func (c *config) clusterMachineSSH(args []string, console bool) error {
ms := shoot.Payload.Machines
ms = append(ms, shoot.Payload.Firewalls...)
for _, m := range ms {
if *m.ID == mid {
if *m.ID != mid {
continue
}
if console {
fmt.Printf("access console via ssh\n")
authContext, err := api.GetAuthContext(viper.GetString("kubeconfig"))
if err != nil {
return fmt.Errorf("unable determine home directory:%w", err)
}
if console {
fmt.Printf("access console via ssh\n")
authContext, err := api.GetAuthContext(viper.GetString("kubeconfig"))
if err != nil {
return err
}
env := &env{
key: "LC_METAL_STACK_OIDC_TOKEN",
value: authContext.IDToken,
}
bmcConsolePort := 5222
err = sshClient(mid, c.consoleHost, keypair.privatekey, bmcConsolePort, env)
return err
}
networks := m.Allocation.Networks
switch *m.Allocation.Role {
case "firewall":
if keypair.vpn != nil {
return c.firewallSSHViaVPN(*m.ID, keypair.privatekey, keypair.vpn)
}
bmcConsolePort := 5222
err = c.sshClient(mid, c.consoleHost, keypair.privatekey, bmcConsolePort, &authContext.IDToken)
return err
}
networks := m.Allocation.Networks
switch *m.Allocation.Role {
case "firewall":
if keypair.vpn != nil {
return c.firewallSSHViaVPN(*m.ID, keypair.privatekey, keypair.vpn)
}

for _, nw := range networks {
if *nw.Underlay || *nw.Private {
continue
}
for _, ip := range nw.Ips {
if portOpen(ip, "22", time.Second) {
err := sshClient("metal", ip, keypair.privatekey, 22, nil)
return err
}
for _, nw := range networks {
if *nw.Underlay || *nw.Private {
continue
}
for _, ip := range nw.Ips {
if portOpen(ip, "22", time.Second) {
err := c.sshClient("metal", ip, keypair.privatekey, 22, nil)
return err
}
}
return fmt.Errorf("no ip with a open ssh port found")
case "machine":
// FIXME metal user is not allowed to execute
// ip vrf exec <tenantvrf> ssh <machineip>
return fmt.Errorf("machine access via ssh not implemented")
default:
return fmt.Errorf("unknown machine role:%s", *m.Allocation.Role)
}
return fmt.Errorf("no ip with a open ssh port found")
case "machine":
// FIXME metal user is not allowed to execute
// ip vrf exec <tenantvrf> ssh <machineip>
return fmt.Errorf("machine access via ssh not implemented")
default:
return fmt.Errorf("unknown machine role:%s", *m.Allocation.Role)
}

}

return fmt.Errorf("machine:%s not found in cluster:%s", mid, cid)
Expand Down
196 changes: 12 additions & 184 deletions cmd/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,208 +3,36 @@ package cmd
import (
"context"
"fmt"
"net"
"net/netip"
"os"
"strings"
"time"

"github.com/avast/retry-go/v4"
"github.com/google/uuid"
"github.com/tailscale/golang-x-crypto/ssh"

"tailscale.com/tsnet"

"github.com/fi-ts/cloud-go/api/models"
"golang.org/x/term"
metalssh "github.com/metal-stack/metal-lib/pkg/ssh"
metalvpn "github.com/metal-stack/metal-lib/pkg/vpn"
)

func (c *config) firewallSSHViaVPN(firewallID string, privateKey []byte, vpn *models.V1VPN) (err error) {
fmt.Printf("accessing firewall through vpn ")
hostname, err := os.Hostname()
if err != nil {
return err
}

randomSuffix, _, _ := strings.Cut(uuid.NewString(), "-")
hostname = fmt.Sprintf("cloudctl-%s-%s", hostname, randomSuffix)
tempDir, err := os.MkdirTemp("", hostname)
if err != nil {
return err
}
defer os.RemoveAll(tempDir)
s := &tsnet.Server{
Hostname: hostname,
ControlURL: *vpn.Address,
AuthKey: *vpn.AuthKey,
Dir: tempDir,
}
defer s.Close()

// now disable logging, maybe altogether later
if os.Getenv("DEBUG") == "" {
s.Logf = func(format string, args ...any) {}
}

start := time.Now()
lc, err := s.LocalClient()
if err != nil {
return err
}
ctx := context.Background()

var firewallVPNIP netip.Addr
err = retry.Do(
func() error {
fmt.Printf(".")
status, err := lc.Status(ctx)
if err != nil {
return err
}
if status.Self.Online {
for _, peer := range status.Peer {
if strings.HasPrefix(peer.HostName, firewallID) {
firewallVPNIP = peer.TailscaleIPs[0]
fmt.Printf(" connected to %s (ip %s) took: %s\n", firewallID, firewallVPNIP, time.Since(start))
return nil
}
}
}
return fmt.Errorf("did not get online")
},
retry.Attempts(50),
)
v, err := metalvpn.Connect(ctx, firewallID, *vpn.Address, *vpn.AuthKey)
if err != nil {
return err
}
// disable logging after successful connect
s.Logf = func(format string, args ...any) {}
defer v.Close()

conn, err := lc.DialTCP(ctx, firewallVPNIP.String(), 22)
s, err := metalssh.NewClientWithConnection("metal", v.TargetIP, privateKey, v.Conn)
if err != nil {
return err
}

return sshClientWithConn("metal", hostname, privateKey, conn)
return s.Connect(nil)
}

// sshClient opens an interactive ssh session to the host on port with user, authenticated by the key.
func sshClientWithConn(user, host string, privateKey []byte, conn net.Conn) error {
sshConfig, err := getSSHConfig(user, privateKey)
if err != nil {
return fmt.Errorf("failed to create SSH config: %w", err)
}

sshConn, sshChan, req, err := ssh.NewClientConn(conn, host, sshConfig)
if err != nil {
return err
}
client := ssh.NewClient(sshConn, sshChan, req)
func (c *config) sshClient(user, host string, privateKey []byte, port int, idToken *string) error {
s, err := metalssh.NewClient(user, host, privateKey, port)
if err != nil {
return err
}
defer client.Close()

return createSSHSession(client, nil)
}

func sshClient(user, host string, privateKey []byte, port int, env *env) error {
fmt.Printf("ssh to %s@%s:%d\n", user, host, port)
sshConfig, err := getSSHConfig(user, privateKey)
if err != nil {
return fmt.Errorf("failed to create SSH config: %w", err)
var env *metalssh.Env
if idToken != nil {
env = &metalssh.Env{"LC_METAL_STACK_OIDC_TOKEN": *idToken}
}
sshServerAddress := fmt.Sprintf("%s:%d", host, port)
client, err := ssh.Dial("tcp", sshServerAddress, sshConfig)
if err != nil {
return err
}

return createSSHSession(client, env)
}

type env struct {
key string
value string
}

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

if env != nil {
err = session.Setenv(env.key, env.value)
if err != nil {
return err
}
}
// Set IO
session.Stdout = os.Stdout
session.Stderr = os.Stderr
session.Stdin = os.Stdin
// Set up terminal modes
// https://net-ssh.github.io/net-ssh/classes/Net/SSH/Connection/Term.html
// https://www.ietf.org/rfc/rfc4254.txt
// https://godoc.org/golang.org/x/crypto/ssh
// THIS IS THE TITLE
// https://pythonhosted.org/ANSIColors-balises/ANSIColors.html
modes := ssh.TerminalModes{
ssh.ECHO: 1, // enable echoing
ssh.TTY_OP_ISPEED: 115200, // input speed = 14.4kbaud
ssh.TTY_OP_OSPEED: 115200, // output speed = 14.4kbaud
}

fileDescriptor := int(os.Stdin.Fd())

if term.IsTerminal(fileDescriptor) {
originalState, err := term.MakeRaw(fileDescriptor)
if err != nil {
return err
}
defer func() {
err = term.Restore(fileDescriptor, originalState)
if err != nil {
fmt.Printf("error restoring ssh terminal:%v\n", err)
}
}()

termWidth, termHeight, err := term.GetSize(fileDescriptor)
if err != nil {
return err
}

err = session.RequestPty("xterm-256color", termHeight, termWidth, modes)
if err != nil {
return err
}
}

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

// You should now be connected via SSH with a fully-interactive terminal
// This call blocks until the user exits the session (e.g. via CTRL + D)
return session.Wait()
}

func getSSHConfig(user string, privateKey []byte) (*ssh.ClientConfig, error) {
signer, err := ssh.ParsePrivateKey(privateKey)
if err != nil {
return nil, err
}

return &ssh.ClientConfig{
User: user,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(signer),
},
//nolint:gosec
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: 10 * time.Second,
}, nil
return s.Connect(env)
}
Loading

0 comments on commit 0cea1d5

Please sign in to comment.