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

Use ssh via vpn from metal-lib #200

Merged
merged 5 commits into from
Jul 25, 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
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
Loading