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

Test win-sshproxy with qemu VM #449

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
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 go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ require (
github.com/songgao/packets v0.0.0-20160404182456-549a10cd4091
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8
github.com/stretchr/testify v1.10.0
github.com/ulikunitz/xz v0.5.12
github.com/vishvananda/netlink v1.3.0
golang.org/x/crypto v0.29.0
golang.org/x/sync v0.9.0
Expand All @@ -43,7 +44,7 @@ require (
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
github.com/vishvananda/netns v0.0.4 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/mod v0.20.0 // indirect
golang.org/x/mod v0.20.0
golang.org/x/net v0.30.0 // indirect
golang.org/x/text v0.20.0 // indirect
golang.org/x/time v0.5.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
Expand Down
46 changes: 44 additions & 2 deletions test-utils/pull.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
package e2eutils

import (
"bufio"
"compress/gzip"
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"strings"

"github.com/sirupsen/logrus"
"github.com/ulikunitz/xz"
)

const XZCAT_TOOL = "xzcat"

// DownloadVMImage downloads a VM image from url to given path
// with download status
func DownloadVMImage(downloadURL string, localImagePath string) error {
Expand Down Expand Up @@ -82,11 +87,21 @@ func Decompress(localPath string) (string, error) {
return uncompressedPath, nil
}

// it uses the xz dependency so it can be used on any OS
func decompressXZ(src string, output io.Writer) error {
_, err := exec.LookPath(XZCAT_TOOL)
if err != nil {
return decompressXZWithReader(src, output)
}

return decompressXZWithExternalTool(src, output)
}

// Will error out if file without .xz already exists
// Maybe extracting then renameing is a good idea here..
// depends on xz: not pre-installed on mac, so it becomes a brew dependency
func decompressXZ(src string, output io.Writer) error {
cmd := exec.Command("xzcat", "-T0", "-k", src)
func decompressXZWithExternalTool(src string, output io.Writer) error {
cmd := exec.Command(XZCAT_TOOL, "-T0", "-k", src)
stdOut, err := cmd.StdoutPipe()
if err != nil {
return err
Expand All @@ -100,6 +115,33 @@ func decompressXZ(src string, output io.Writer) error {
return cmd.Run()
}

// it supports decompression without any external tool
// Known issue about slowness https://github.com/ulikunitz/xz/issues/23
func decompressXZWithReader(src string, output io.Writer) error {
file, err := os.Open(src)
if err != nil {
return err
}
defer file.Close()

// using bufio.NewReader speeds up the decompressions -> https://github.com/ulikunitz/xz/issues/23
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

even with the bufio reader, it is still a lot slower than the external tool? crc uses https://github.com/xi2/xz?tab=readme-ov-file for xz decompression, but I think it is also slower than native xz. And it hasn't seen any updates in years.

reader, err := xz.NewReader(bufio.NewReader(file))
if err != nil {
log.Fatalf("NewReader error %s", err)
}
for {
_, err := io.CopyN(output, reader, 10000)
if err != nil {
if err == io.EOF {
break
}
return err
}
}

return nil
}

func decompressGZ(src string, output io.Writer) error {
file, err := os.Open(src)
if err != nil {
Expand Down
46 changes: 46 additions & 0 deletions test-win-sshproxy/test-qemu/basic_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//go:build windows
// +build windows

package e2e_win_qemu

import (
"context"
"io"
"net"
"net/http"
"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("proxies over a windows pipe to call podman api", func() {
httpClient := &http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return winio.DialPipe(npipePodmanTestPath, &timeout)
},
},
}

Eventually(func(g Gomega) {
resp, err := httpClient.Get("http://localhost/v4.0.2/libpod/info")
g.Expect(err).ShouldNot(HaveOccurred())
defer resp.Body.Close()

g.Expect(resp.StatusCode).To(Equal(http.StatusOK))

reply := make([]byte, 8)
_, err = io.ReadAtLeast(resp.Body, reply, len(reply))

g.Expect(err).ShouldNot(HaveOccurred())
g.Expect(string(reply)).To(Equal("{\"host\":"))

}).Should(Succeed())
})
})
187 changes: 187 additions & 0 deletions test-win-sshproxy/test-qemu/suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
//go:build windows
// +build windows

package e2e_win_qemu

import (
"flag"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"testing"
"time"

"github.com/onsi/ginkgo"
"github.com/onsi/gomega"
log "github.com/sirupsen/logrus"

e2e_utils "github.com/containers/gvisor-tap-vsock/test-utils"
)

func TestSuite(t *testing.T) {
gomega.RegisterFailHandler(ginkgo.Fail)
ginkgo.RunSpecs(t, "win-sshproxy suite")
}

const (
qemuPort = 5554
ignitionUser = "test"
podmanSock = "/run/user/1001/podman/podman.sock"
podmanRootSock = "/run/podman/podman.sock"
npipePodmanTestPath = "\\\\.\\pipe\\test-win-sshproxy"
npipePodmanTest = "npipe:////./pipe/test-win-sshproxy"

// #nosec "test" (for manual usage)
ignitionPasswordHash = "$y$j9T$TqJWt3/mKJbH0sYi6B/LD1$QjVRuUgntjTHjAdAkqhkr4F73m.Be4jBXdAaKw98sPC"
)

var (
tmpDir string
binDir string
host *exec.Cmd
client *exec.Cmd
privateKeyFile string
publicKeyFile string
ignFile string
forwardSock string
forwardRootSock string
qconLog string
)

func init() {
flag.StringVar(&tmpDir, "tmpDir", "..\\..\\tmp", "temporary working directory")
flag.StringVar(&binDir, "bin", "..\\..\\bin", "directory with compiled binaries")
privateKeyFile = filepath.Join(tmpDir, "id_test")
publicKeyFile = privateKeyFile + ".pub"
ignFile = filepath.Join(tmpDir, "test.ign")
forwardSock = filepath.Join(tmpDir, "podman-remote.sock")
forwardRootSock = filepath.Join(tmpDir, "podman-root-remote.sock")
qconLog = filepath.Join(tmpDir, "qcon.log")
}

var _ = ginkgo.BeforeSuite(func() {
gomega.Expect(os.MkdirAll(filepath.Join(tmpDir, "disks"), os.ModePerm)).Should(gomega.Succeed())

downloader, err := e2e_utils.NewFcosDownloader(filepath.Join(tmpDir, "disks"))
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
qemuImage, err := downloader.DownloadImage("qemu", "qcow2.xz")
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())

publicKey, err := e2e_utils.CreateSSHKeys(publicKeyFile, privateKeyFile)
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())

err = e2e_utils.CreateIgnition(ignFile, publicKey, ignitionUser, ignitionPasswordHash)
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())

outer:
for panics := 0; ; panics++ {
template := `-m 2048 -nographic -serial file:%s -hda %s -fw_cfg name=opt/com.coreos/config,file=%s -nic user,hostfwd=tcp::%d-:22`

// #nosec
client = exec.Command(qemuExecutable(), strings.Split(fmt.Sprintf(template, qconLog, qemuImage, ignFile, qemuPort), " ")...)
client.Stderr = os.Stderr
client.Stdout = os.Stdout
gomega.Expect(client.Start()).Should(gomega.Succeed())
go func() {
if err := client.Wait(); err != nil {
log.Error(err)
}
}()

for {
_, err := sshExec("whoami")
if err == nil {
break outer
}

// Check for panic
didPanic, err := panicCheck(qconLog)
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())

if didPanic {
gomega.Expect(panics).ToNot(gomega.BeNumerically(">", 15), "No more than 15 panics allowed")
log.Info("Detected Kernel panic, retrying...")
_ = client.Process.Kill()
_ = os.Remove(qconLog)
continue outer
}

log.Infof("waiting for client to connect: %v", err)
time.Sleep(time.Second)
}
}

// #nosec
host = exec.Command(filepath.Join(binDir, "win-sshproxy.exe"), "test", tmpDir, npipePodmanTest,
fmt.Sprintf("ssh://%s@localhost:%d%s", ignitionUser, qemuPort, podmanSock), privateKeyFile)

host.Stderr = os.Stderr
host.Stdout = os.Stdout
gomega.Expect(host.Start()).Should(gomega.Succeed())
go func() {
if err := host.Wait(); err != nil {
log.Error(err)
}
}()

time.Sleep(5 * time.Second)
})

func qemuExecutable() string {
binary := fmt.Sprintf("qemu-system-%s.exe", e2e_utils.CoreosArch())
path, err := exec.LookPath(binary)
if err != nil {
return ""
}
return path
}

func sshExec(cmd ...string) ([]byte, error) {
return sshCommand(cmd...).Output()
}

func sshCommand(cmd ...string) *exec.Cmd {
sshCmd := exec.Command("ssh",
"-o", "UserKnownHostsFile=/dev/null",
"-o", "StrictHostKeyChecking=no",
"-o", "IdentitiesOnly=yes",
"-i", privateKeyFile,
"-p", strconv.Itoa(qemuPort),
fmt.Sprintf("%[email protected]", ignitionUser), "--", strings.Join(cmd, " ")) // #nosec G204
return sshCmd
}

func panicCheck(con string) (bool, error) {
file, err := os.Open(con)
if err != nil {
return false, err
}

_, _ = file.Seek(-500, io.SeekEnd)
// Ignore seek errors (not enough content yet)

contents := make([]byte, 500)
_, err = io.ReadAtLeast(file, contents, len(contents))
if err != nil && err != io.ErrUnexpectedEOF {
return false, err
}

return strings.Contains(string(contents), "end Kernel panic"), nil
}

var _ = ginkgo.AfterSuite(func() {
if host != nil {
if err := host.Process.Kill(); err != nil {
log.Error(err)
}
}
if client != nil {
if err := client.Process.Kill(); err != nil {
log.Error(err)
}
}
})