Skip to content

Commit

Permalink
Use ssh via vpn from metal-lib (#200)
Browse files Browse the repository at this point in the history
  • Loading branch information
majst01 committed Jul 25, 2023
1 parent 63f433b commit ae20485
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 273 deletions.
89 changes: 1 addition & 88 deletions cmd/firewall.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,18 @@
package cmd

import (
"context"
"encoding/base64"
"fmt"
"net/netip"
"os"
"strings"
"time"

"github.com/avast/retry-go/v4"
"github.com/google/uuid"
"github.com/metal-stack/metal-go/api/client/firewall"
"github.com/metal-stack/metal-go/api/client/vpn"
"github.com/metal-stack/metal-go/api/models"
"github.com/metal-stack/metal-lib/pkg/genericcli"
"github.com/metal-stack/metal-lib/pkg/genericcli/printers"
"github.com/metal-stack/metal-lib/pkg/pointer"
"github.com/metal-stack/metalctl/cmd/sorters"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"tailscale.com/tsnet"
)

type firewallCmd struct {
Expand Down Expand Up @@ -223,7 +215,7 @@ func (c *firewallCmd) firewallPureSSH(fwAllocation *models.V1MachineAllocation)
}
for _, ip := range nw.Ips {
if portOpen(ip, "22", time.Second) {
err = SSHClient("metal", viper.GetString("identity"), ip, 22)
err = sshClient("metal", viper.GetString("identity"), ip, 22, nil)
if err != nil {
return err
}
Expand All @@ -235,82 +227,3 @@ func (c *firewallCmd) firewallPureSSH(fwAllocation *models.V1MachineAllocation)

return fmt.Errorf("no ip with a open ssh port found")
}

func (c *firewallCmd) firewallSSHViaVPN(firewall *models.V1FirewallResponse) (err error) {
if firewall.Allocation == nil || firewall.Allocation.Project == nil {
return fmt.Errorf("firewall allocation or allocation.project is nil")
}
projectID := firewall.Allocation.Project
fmt.Fprintf(c.out, "accessing firewall through vpn ")
authKeyResp, err := c.client.VPN().GetVPNAuthKey(vpn.NewGetVPNAuthKeyParams().WithBody(&models.V1VPNRequest{
Pid: projectID,
Ephemeral: pointer.Pointer(true),
}), nil)
if err != nil {
return fmt.Errorf("failed to get VPN auth key: %w", err)
}
hostname, err := os.Hostname()
if err != nil {
return err
}
randomSuffix, _, _ := strings.Cut(uuid.NewString(), "-")
hostname = fmt.Sprintf("metalctl-%s-%s", hostname, randomSuffix)
tempDir, err := os.MkdirTemp("", hostname)
if err != nil {
return err
}
defer os.RemoveAll(tempDir)
s := &tsnet.Server{
Hostname: hostname,
ControlURL: *authKeyResp.Payload.Address,
AuthKey: *authKeyResp.Payload.AuthKey,
Dir: tempDir,
}
defer s.Close()

// now disable logging, maybe altogether later
if !viper.GetBool("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, *firewall.ID) && len(peer.TailscaleIPs) > 0 {
firewallVPNIP = peer.TailscaleIPs[0]
fmt.Printf(" connected to %s (ip %s) took: %s\n", *firewall.ID, firewallVPNIP, time.Since(start))
return nil
}
}
}
return fmt.Errorf("did not get online")
},
retry.Attempts(50),
)
if err != nil {
return err
}
// disable logging after successful connect
s.Logf = func(format string, args ...any) {}

conn, err := lc.DialTCP(ctx, firewallVPNIP.String(), 22)
if err != nil {
return err
}

return sshClientWithConn("metal", hostname, viper.GetString("identity"), conn)
}
6 changes: 1 addition & 5 deletions cmd/machine.go
Original file line number Diff line number Diff line change
Expand Up @@ -1229,11 +1229,7 @@ func (c *machineCmd) machineConsole(args []string) error {
if err != nil {
return err
}
err = os.Setenv("LC_METAL_STACK_OIDC_TOKEN", authContext.IDToken)
if err != nil {
return err
}
err = SSHClient(id, key, parsedurl.Host, bmcConsolePort)
err = sshClient(id, key, parsedurl.Host, bmcConsolePort, &authContext.IDToken)
if err != nil {
return fmt.Errorf("machine console error:%w", err)
}
Expand Down
146 changes: 39 additions & 107 deletions cmd/ssh.go
Original file line number Diff line number Diff line change
@@ -1,139 +1,71 @@
package cmd

import (
"context"
"fmt"
"net"
"os"
"time"

"github.com/tailscale/golang-x-crypto/ssh"
"golang.org/x/term"
"path/filepath"
"strings"

"github.com/metal-stack/metal-go/api/client/vpn"
"github.com/metal-stack/metal-go/api/models"
"github.com/metal-stack/metal-lib/pkg/pointer"
metalssh "github.com/metal-stack/metal-lib/pkg/ssh"
metalvpn "github.com/metal-stack/metal-lib/pkg/vpn"
"github.com/spf13/viper"
)

// SSHClient opens an interactive ssh session to the host on port with user, authenticated by the key.
func SSHClient(user, keyfile, host string, port int) error {
sshConfig, err := getSSHConfig(user, keyfile)
func (c *firewallCmd) firewallSSHViaVPN(firewall *models.V1FirewallResponse) (err error) {
if firewall.Allocation == nil || firewall.Allocation.Project == nil {
return fmt.Errorf("firewall allocation or allocation.project is nil")
}
projectID := firewall.Allocation.Project
fmt.Fprintf(c.out, "accessing firewall through vpn ")
authKeyResp, err := c.client.VPN().GetVPNAuthKey(vpn.NewGetVPNAuthKeyParams().WithBody(&models.V1VPNRequest{
Pid: projectID,
Ephemeral: pointer.Pointer(true),
}), nil)
if err != nil {
return fmt.Errorf("failed to create SSH config: %w", err)
return fmt.Errorf("failed to get VPN auth key: %w", err)
}

client, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", host, port), sshConfig)
ctx := context.Background()
v, err := metalvpn.Connect(ctx, *firewall.ID, *authKeyResp.Payload.Address, *authKeyResp.Payload.AuthKey)
if err != nil {
return err
}
defer client.Close()

return createSSHSession(client)
}
defer v.Close()

// sshClient opens an interactive ssh session to the host on port with user, authenticated by the key.
func sshClientWithConn(user, host, privateKey string, conn net.Conn) error {
sshConfig, err := getSSHConfig(user, privateKey)
if err != nil {
return fmt.Errorf("failed to create SSH config: %w", err)
privateKeyFile := viper.GetString("identity")
if strings.HasPrefix(privateKeyFile, "~/") {
home, _ := os.UserHomeDir()
privateKeyFile = filepath.Join(home, privateKeyFile[2:])
}

sshConn, sshChan, req, err := ssh.NewClientConn(conn, host, sshConfig)
privateKey, err := os.ReadFile(privateKeyFile)
if err != nil {
return err
}
client := ssh.NewClient(sshConn, sshChan, req)
s, err := metalssh.NewClientWithConnection("metal", v.TargetIP, privateKey, v.Conn)
if err != nil {
return err
}
defer client.Close()

return createSSHSession(client)
return s.Connect(nil)
}

func createSSHSession(client *ssh.Client) error {
session, err := client.NewSession()
// sshClient opens an interactive ssh session to the host on port with user, authenticated by the key.
func sshClient(user, keyfile, host string, port int, idToken *string) error {
privateKey, err := os.ReadFile(keyfile)
if err != nil {
return err
}
defer session.Close()

// 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()
s, err := metalssh.NewClient(user, host, privateKey, port)
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, keyfile string) (*ssh.ClientConfig, error) {
keyfile, err := expandFilepath(keyfile)
if err != nil {
return nil, err
}

publicKeyAuthMethod, err := publicKey(keyfile)
if err != nil {
return nil, err
}

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

func publicKey(path string) (ssh.AuthMethod, error) {
key, err := os.ReadFile(path)
if err != nil {
return nil, err
}
signer, err := ssh.ParsePrivateKey(key)
if err != nil {
return nil, err
var env *metalssh.Env
if idToken != nil {
env = &metalssh.Env{"LC_METAL_STACK_OIDC_TOKEN": *idToken}
}
return ssh.PublicKeys(signer), nil
return s.Connect(env)
}
Loading

0 comments on commit ae20485

Please sign in to comment.