Skip to content
Open
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
3 changes: 2 additions & 1 deletion ssh/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@ module github.com/fluxcd/pkg/ssh
go 1.26.0

require (
github.com/firefart/gosocks v0.4.2
github.com/onsi/gomega v1.40.0
golang.org/x/crypto v0.47.0
golang.org/x/net v0.49.0
)

require (
github.com/google/go-cmp v0.7.0 // indirect
github.com/kr/text v0.2.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.33.0 // indirect
)
2 changes: 2 additions & 0 deletions ssh/go.sum
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/firefart/gosocks v0.4.2 h1:HduMZGxEVsBFEHa57rNwqg8S8M878Pe2Og1TfQKaIR8=
github.com/firefart/gosocks v0.4.2/go.mod h1:9k5AYic+qFxo1W9hxw3vFbRTBEVIT3nWepdFvqv0uc4=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
Expand Down
27 changes: 23 additions & 4 deletions ssh/host_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ limitations under the License.
package ssh

import (
"context"
"encoding/base64"
"fmt"
"net"
"time"

"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/knownhosts"
"golang.org/x/net/proxy"
)

// ScanHostKey collects the given host's preferred public key for the
Expand All @@ -45,16 +47,33 @@ func ScanHostKey(host string, timeout time.Duration, clientHostKeyAlgos []string
config.HostKeyAlgorithms = clientHostKeyAlgos
}

client, err := ssh.Dial("tcp", host, config)
if err == nil {
defer client.Close()
}
err := sshDial(host, config)

if len(col.knownKeys) > 0 {
return col.knownKeys, nil
}

return col.knownKeys, err
}

func sshDial(host string, config *ssh.ClientConfig) error {
ctx, cancel := context.WithTimeout(context.Background(), config.Timeout)
defer cancel()
// this reads the ALL_PROXY environment varaible
conn, err := proxy.Dial(ctx, "tcp", host)
if err != nil {
return err
}
c, chans, reqs, err := ssh.NewClientConn(conn, host, config)
if err != nil {
return err
}
client := ssh.NewClient(c, chans, reqs)
defer client.Close()

return nil
}

// HostKeyCollector offers a StoreKey method which provides an
// HostKeyCallBack to collect public keys from an SSH server.
type HostKeyCollector struct {
Expand Down
112 changes: 96 additions & 16 deletions ssh/host_key_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build !proxy

/*
Copyright 2022 The Flux authors

Expand All @@ -17,32 +19,36 @@ limitations under the License.
package ssh

import (
"context"
"fmt"
"io"
"net"
"os"
"os/exec"
"testing"
"time"

socks "github.com/firefart/gosocks"
. "github.com/onsi/gomega"
"golang.org/x/crypto/ssh"
)

func TestScanHost(t *testing.T) {
g := NewWithT(t)

startSSH := func(listener net.Listener, cfg *ssh.ServerConfig) {
conn, err := listener.Accept()
g.Expect(err).ToNot(HaveOccurred())
func startSSH(listener net.Listener, cfg *ssh.ServerConfig, g *WithT) {
conn, err := listener.Accept()
g.Expect(err).ToNot(HaveOccurred())

sConn, _, _, err := ssh.NewServerConn(conn, cfg)
if err != nil {
// the only expected error
g.Expect(err.Error()).To(ContainSubstring("no common algorithm for host key"))
return
}

sConn.Close()
listener.Close()
sConn, _, _, err := ssh.NewServerConn(conn, cfg)
if err != nil {
// the only expected error
g.Expect(err.Error()).To(ContainSubstring("no common algorithm for host key"))
return
}

sConn.Close()
listener.Close()
}

func TestScanHost(t *testing.T) {
tests := []struct {
keyType KeyPairType
sshKeyTypeName string
Expand Down Expand Up @@ -83,7 +89,7 @@ func TestScanHost(t *testing.T) {
g.Expect(err).NotTo(HaveOccurred())
sshConfig.AddHostKey(signer)

go startSSH(listener, sshConfig)
go startSSH(listener, sshConfig, g)

kh, err := ScanHostKey(serverAddr, 5*time.Second, []string{tt.sshKeyTypeName}, false)
if tt.wantErr == "" {
Expand All @@ -100,3 +106,77 @@ func TestScanHost(t *testing.T) {
})
}
}

// this test is partially based on a go-git's TestSOCKS5Proxy
// see https://github.com/go-git/go-git/blob/5f90b841aef24f235002e2fc71bfb1e142f804cf/plumbing/transport/ssh/proxy_test.go#L21
func TestScanHostWithProxy(t *testing.T) {
g := NewWithT(t)

listener, err := net.Listen("tcp", "127.0.0.1:0")
g.Expect(err).ToNot(HaveOccurred())

serverAddr := listener.Addr().String()
g.Expect(serverAddr).ToNot(BeEmpty())

sshConfig := &ssh.ServerConfig{
NoClientAuth: true,
}

// Generate new keypair for the server to use for HostKeys.
hkp, err := GenerateKeyPair(RSA_4096)
g.Expect(err).NotTo(HaveOccurred())
p, err := ssh.ParseRawPrivateKey(hkp.PrivateKey)
g.Expect(err).NotTo(HaveOccurred())

// Add key to server.
signer, err := ssh.NewSignerFromKey(p)
g.Expect(err).NotTo(HaveOccurred())
sshConfig.AddHostKey(signer)

go startSSH(listener, sshConfig, g)

handler := &CustomHandler{
DefaultHandler: socks.DefaultHandler{
Timeout: 1 * time.Second,
},
}
socksAddress := "127.0.0.1:1080"
socksProxy := socks.Proxy{
ServerAddr: socksAddress,
Proxyhandler: handler,
Timeout: 1 * time.Second,
}
g.Expect(err).NotTo(HaveOccurred())
socksProxy.Start(context.TODO())

// we can't set ENV only for this test
// because Golang proxy package caches ENV checks
// and there is no method to reset this cache outside of proxy package
// https://cs.opensource.google/go/x/net/+/refs/tags/v0.57.0:proxy/proxy.go;l=132
// so we have to run an additional process
// and then check the request counter in our socks server
cmd := exec.Command("go", "test", "-tags=proxy")
cmd.Env = append(os.Environ(),
fmt.Sprintf("SSH_HOST=%s", serverAddr),
fmt.Sprintf("ALL_PROXY=socks5://%s", socksAddress),
)

output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("Child process failed with %v. Output: %s", err, string(output))
}

g.Expect(handler.proxiedRequests).Should(BeNumerically(">", 0))
listener.Close()
socksProxy.Stop()
}

type CustomHandler struct {
socks.DefaultHandler
proxiedRequests int
}

func (h *CustomHandler) Init(ctx context.Context, request socks.Request) (context.Context, io.ReadWriteCloser, *socks.Error) {
h.proxiedRequests += 1
return h.DefaultHandler.Init(ctx, request)
}
18 changes: 18 additions & 0 deletions ssh/proxy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//go:build proxy

package ssh

import (
"os"
"testing"
"time"

. "github.com/onsi/gomega"
)

func TestWithProxy(t *testing.T) {
g := NewWithT(t)
kh, err := ScanHostKey(os.Getenv("SSH_HOST"), 5*time.Second, []string{"ssh-rsa"}, false)
g.Expect(err).ToNot(HaveOccurred())
g.Expect(string(kh)).To(ContainSubstring("ssh-rsa"))
}