Add tests for win-sshproxy

Signed-off-by: Jason T. Greene <jason.greene@redhat.com>
This commit is contained in:
Jason T. Greene
2022-01-14 01:32:31 -06:00
parent db04e7ddba
commit 23695cfcfb
4 changed files with 362 additions and 0 deletions

View File

@@ -52,3 +52,22 @@ jobs:
with: with:
name: qcon name: qcon
path: test/qcon.log path: test/qcon.log
win-sshproxy-tests:
runs-on: windows-latest # Only builds/runs on windows
timeout-minutes: 30
steps:
- uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.17
- name: Build
run: go build -ldflags -H=windowsgui -o bin/win-sshproxy.exe ./cmd/win-sshproxy
- name: Test
run: go test -v .\test-win-sshproxy

View File

@@ -0,0 +1,89 @@
// +build windows
package e2e
import (
"context"
"io"
"io/ioutil"
"net"
"net/http"
"os"
"os/exec"
"strings"
"time"
winio "github.com/Microsoft/go-winio"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var timeout = 1 * time.Minute
var _ = Describe("connectivity", func() {
It("proxy exits as requested, without a kill", func() {
err := startProxy()
Expect(err).ShouldNot(HaveOccurred())
var pid uint32
for i := 0; i < 20; i++ {
pid, _, err = readTid()
if err == nil {
break
}
time.Sleep(100 * time.Millisecond)
}
Expect(err).ShouldNot(HaveOccurred())
proc, err := os.FindProcess(int(pid))
Expect(err).ShouldNot(HaveOccurred())
Expect(proc).ShouldNot(BeNil())
err = stopProxy(true)
Expect(err).ShouldNot(HaveOccurred())
})
It("proxies over a windows pipe", func() {
err := startProxy()
Expect(err).ShouldNot(HaveOccurred())
defer stopProxy(false)
httpClient := &http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return winio.DialPipe(`\\.\pipe\fake_docker_engine`, &timeout)
},
},
}
Eventually(func(g Gomega) {
resp, err := httpClient.Get("http://host/ping")
g.Expect(err).ShouldNot(HaveOccurred())
defer resp.Body.Close()
g.Expect(resp.StatusCode).To(Equal(http.StatusOK))
g.Expect(resp.ContentLength).To(Equal(int64(4)))
reply := make([]byte, resp.ContentLength)
_, err = io.ReadAtLeast(resp.Body, reply, len(reply))
g.Expect(err).ShouldNot(HaveOccurred())
g.Expect(string(reply)).To(Equal("pong"))
}).Should(Succeed())
err = stopProxy(true)
Expect(err).ShouldNot(HaveOccurred())
})
It("windows event logs were created", func() {
cmd := exec.Command("powershell", "-Command", "&{Get-WinEvent -ProviderName \".NET Runtime\" -MaxEvents 10 | Where-Object -Property Message -Match \"test:\"}")
reader, err := cmd.StdoutPipe()
Expect(err).ShouldNot(HaveOccurred())
cmd.Start()
output, err := ioutil.ReadAll(reader)
Expect(err).ShouldNot(HaveOccurred())
cmd.Wait()
Expect(strings.Contains(string(output), `[info ] test: Listening on: \\.\pipe\fake_docker_engine`)).Should(BeTrue())
Expect(strings.Contains(string(output),`[info ] test: Socket forward established`)).Should(BeTrue())
})
})

View File

@@ -0,0 +1,130 @@
// +build windows
package e2e
import (
"bufio"
"context"
"fmt"
"io"
"net"
"net/http"
"strings"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
)
const fakeHostKey = `-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACAkXGLzDNnY5+xdAgnt8FlBIZtoFOZEdTUkNxkdSM05PgAAAJg9WMAvPVjA
LwAAAAtzc2gtZWQyNTUxOQAAACAkXGLzDNnY5+xdAgnt8FlBIZtoFOZEdTUkNxkdSM05Pg
AAAEAFvLprhpMPdNsxSwo1Cs5VP5joCh9XLicRqKE0JJzdxCRcYvMM2djn7F0CCe3wWUEh
m2gU5kR1NSQ3GR1IzTk+AAAAEmphc29uQFRyaXBlbC5sb2NhbAECAw==
-----END OPENSSH PRIVATE KEY-----`
type streamLocalDirect struct {
SocketPath string
Reserved0 string
Reserved1 uint32
}
var cancel context.CancelFunc
func startMockServer() {
sshConfig := &ssh.ServerConfig{
NoClientAuth: true,
}
key, err := ssh.ParsePrivateKey([]byte(fakeHostKey))
if err != nil {
logrus.Errorf("Could not parse key: %s", err)
}
sshConfig.AddHostKey(key)
listener, err := net.Listen("tcp", ":2134")
if err != nil {
panic(err)
}
var ctx context.Context
ctx, cancel = context.WithCancel(context.Background())
go func() {
loop:
for {
select {
case <-ctx.Done():
break loop
default:
// proceed
}
conn, err := listener.Accept()
if err != nil {
panic(err)
}
// From a standard TCP connection to an encrypted SSH connection
_, chans, reqs, err := ssh.NewServerConn(conn, sshConfig)
if err != nil {
panic(err)
}
go handleRequests(reqs)
// Accept all channels
go handleChannels(chans)
}
listener.Close()
}()
}
func stopMockServer() {
cancel()
}
func handleRequests(reqs <-chan *ssh.Request) {
for _ = range reqs {
}
}
func handleChannels(chans <-chan ssh.NewChannel) {
directMsg := streamLocalDirect{}
for newChannel := range chans {
if t := newChannel.ChannelType(); t != "direct-streamlocal@openssh.com" {
newChannel.Reject(ssh.UnknownChannelType, fmt.Sprintf("unknown channel type: %s", t))
continue
}
if err := ssh.Unmarshal(newChannel.ExtraData(), &directMsg); err != nil {
logrus.Errorf("could not direct-streamlocal data: %s", err)
newChannel.Reject(ssh.Prohibited, "invalid format")
return
}
channel, _, err := newChannel.Accept()
if err != nil {
logrus.Errorf("could not accept channel: %s", err)
continue
}
req, err := http.ReadRequest(bufio.NewReader(channel))
if err != nil {
logrus.Errorf("could not process http request: %s", err)
}
resp := http.Response{}
resp.Close = true
switch req.RequestURI {
case "/ping":
resp.StatusCode = 200
resp.ContentLength = 4
resp.Body = io.NopCloser(strings.NewReader("pong"))
default:
resp.StatusCode = 404
resp.ContentLength = 0
}
resp.Write(channel)
channel.CloseWrite()
}
}

View File

@@ -0,0 +1,124 @@
// +build windows
package e2e
import (
"flag"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"syscall"
"testing"
"time"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
const (
WM_QUIT = 0x12
)
var (
tmpDir string
binDir string
keyFile string
winSshProxy string
tidFile string
)
func TestSuite(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "win-sshproxy suite")
}
func init() {
flag.StringVar(&tmpDir, "tmpDir", "../tmp", "temporary working directory")
flag.StringVar(&binDir, "bin", "../bin", "directory with compiled binaries")
_ = os.MkdirAll(tmpDir, 0755)
keyFile = filepath.Join(tmpDir, "id.key")
_ = ioutil.WriteFile(keyFile, []byte(fakeHostKey), 0600)
winSshProxy = filepath.Join(binDir, "win-sshproxy.exe")
tidFile = filepath.Join(tmpDir, "win-sshproxy.tid")
}
var _ = BeforeSuite(func() {
startMockServer()
})
var _ = AfterSuite(func() {
stopMockServer()
})
func startProxy() error {
os.Remove(tidFile)
cmd := exec.Command(winSshProxy, "-debug", "test", tmpDir, "npipe:////./pipe/fake_docker_engine", "ssh://localhost:2134/run/podman/podman.sock", keyFile)
return cmd.Start()
}
func readTid() (uint32, uint32, error) {
contents, err := ioutil.ReadFile(tidFile)
if err != nil {
return 0, 0, err
}
var pid, tid uint32
fmt.Sscanf(string(contents), "%d:%d", &pid, &tid)
return pid, tid, nil
}
func sendQuit(tid uint32) {
user32 := syscall.NewLazyDLL("user32.dll")
postMessage := user32.NewProc("PostThreadMessageW")
postMessage.Call(uintptr(tid), WM_QUIT, 0, 0)
}
func stopProxy(noKill bool) error {
pid, tid, err := readTid()
if err != nil {
return err
}
proc, err := os.FindProcess(int(pid))
if err != nil {
return err
}
sendQuit(tid)
state := waitTimeout(proc, 20*time.Second)
if state == nil || !state.Exited() {
if noKill {
return fmt.Errorf("proxy did not exit on request")
}
_ = proc.Kill()
state = waitTimeout(proc, 20*time.Second)
}
if state == nil || !state.Exited() {
return fmt.Errorf("Stop proxy failed: %d", pid)
}
_ = os.Remove(tidFile)
return nil
}
func waitTimeout(proc *os.Process, timeout time.Duration) *os.ProcessState {
return doTimeout(func(complete chan *os.ProcessState) {
state, _ := proc.Wait()
complete <- state
}, timeout)
}
func doTimeout(action func(complete chan *os.ProcessState), timeout time.Duration) *os.ProcessState {
complete := make(chan *os.ProcessState)
go action(complete)
select {
case <-time.After(timeout):
return nil
case state := <-complete:
return state
}
}