summaryrefslogtreecommitdiffstats
path: root/test
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 16:16:56 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 16:16:56 +0000
commit62e212dec2415aa605363d616c481c2a56e70737 (patch)
treeb5ace7061598be728afaf95a9a40054daf05ce19 /test
parentInitial commit. (diff)
downloadgolang-github-containers-gvisor-tap-vsocks-62e212dec2415aa605363d616c481c2a56e70737.tar.xz
golang-github-containers-gvisor-tap-vsocks-62e212dec2415aa605363d616c481c2a56e70737.zip
Adding upstream version 0.7.3+ds1.upstream/0.7.3+ds1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'test')
-rw-r--r--test/basic_test.go222
-rw-r--r--test/fcos.go80
-rw-r--r--test/fcos_amd64.go64
-rw-r--r--test/ignition.go162
-rw-r--r--test/ignition_schema.go248
-rwxr-xr-xtest/performance.sh54
-rw-r--r--test/port_forwarding_test.go269
-rw-r--r--test/pull.go81
-rw-r--r--test/suite_test.go242
-rwxr-xr-xtest/wsl.sh9
10 files changed, 1431 insertions, 0 deletions
diff --git a/test/basic_test.go b/test/basic_test.go
new file mode 100644
index 0000000..9d3faf0
--- /dev/null
+++ b/test/basic_test.go
@@ -0,0 +1,222 @@
+package e2e
+
+import (
+ "context"
+ "net"
+ "net/http"
+
+ gvproxyclient "github.com/containers/gvisor-tap-vsock/pkg/client"
+ "github.com/containers/gvisor-tap-vsock/pkg/types"
+ "github.com/onsi/ginkgo"
+ "github.com/onsi/gomega"
+)
+
+var _ = ginkgo.Describe("connectivity", func() {
+ ginkgo.It("should configure the interface", func() {
+ out, err := sshExec("ifconfig $(route | grep '^default' | grep -o '[^ ]*$')")
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+ gomega.Expect(string(out)).To(gomega.ContainSubstring("mtu 1500"))
+ gomega.Expect(string(out)).To(gomega.ContainSubstring("inet 192.168.127.2"))
+ gomega.Expect(string(out)).To(gomega.ContainSubstring("netmask 255.255.255.0"))
+ gomega.Expect(string(out)).To(gomega.ContainSubstring("broadcast 192.168.127.255"))
+ })
+
+ ginkgo.It("should configure the default route", func() {
+ out, err := sshExec("ip route show")
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+ gomega.Expect(string(out)).To(gomega.MatchRegexp(`default via 192\.168\.127\.1 dev (.*?) proto dhcp (src 192\.168\.127\.2 )?metric 100`))
+ })
+
+ ginkgo.It("should configure dns settings", func() {
+ out, err := sshExec("cat /etc/resolv.conf")
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+ gomega.Expect(string(out)).To(gomega.ContainSubstring("nameserver 192.168.127.1"))
+ })
+
+ ginkgo.It("should ping the tap device", func() {
+ out, err := sshExec("ping -c2 192.168.127.2")
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+ gomega.Expect(string(out)).To(gomega.ContainSubstring("2 packets transmitted, 2 received, 0% packet loss"))
+ })
+
+ ginkgo.It("should ping the gateway", func() {
+ out, err := sshExec("ping -c2 192.168.127.1")
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+ gomega.Expect(string(out)).To(gomega.ContainSubstring("2 packets transmitted, 2 received, 0% packet loss"))
+ })
+})
+
+var _ = ginkgo.Describe("dns", func() {
+ ginkgo.It("should resolve redhat.com", func() {
+ out, err := sshExec("nslookup redhat.com")
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+ gomega.Expect(string(out)).To(gomega.ContainSubstring("Address: 52.200.142.250"))
+ })
+
+ ginkgo.It("should resolve CNAME record for www.wikipedia.org", func() {
+ out, err := sshExec("nslookup -query=cname www.wikipedia.org")
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+ gomega.Expect(string(out)).To(gomega.ContainSubstring("www.wikipedia.org canonical name = dyna.wikimedia.org."))
+ })
+ ginkgo.It("should resolve MX record for wikipedia.org", func() {
+ out, err := sshExec("nslookup -query=mx wikipedia.org")
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+ gomega.Expect(string(out)).To(gomega.ContainSubstring("wikipedia.org mail exchanger = 10 mx1001.wikimedia.org."))
+ })
+
+ ginkgo.It("should resolve NS record for wikipedia.org", func() {
+ out, err := sshExec("nslookup -query=ns wikipedia.org")
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+ gomega.Expect(string(out)).To(gomega.ContainSubstring("wikipedia.org nameserver = ns0.wikimedia.org."))
+ })
+ ginkgo.It("should resolve LDAP SRV record for google.com", func() {
+ out, err := sshExec("nslookup -query=srv _ldap._tcp.google.com")
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+ gomega.Expect(string(out)).To(gomega.ContainSubstring(`_ldap._tcp.google.com service = 5 0 389 ldap.google.com.`))
+ })
+ ginkgo.It("should resolve TXT for wikipedia.org", func() {
+ out, err := sshExec("nslookup -query=txt wikipedia.org")
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+ gomega.Expect(string(out)).To(gomega.ContainSubstring(`"v=spf1 include:wikimedia.org ~all"`))
+ })
+
+ ginkgo.It("should resolve gateway.containers.internal", func() {
+ out, err := sshExec("nslookup gateway.containers.internal")
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+ gomega.Expect(string(out)).To(gomega.ContainSubstring("Address: 192.168.127.1"))
+ })
+
+ ginkgo.It("should resolve host.containers.internal", func() {
+ out, err := sshExec("nslookup host.containers.internal")
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+ gomega.Expect(string(out)).To(gomega.ContainSubstring("Address: 192.168.127.254"))
+ })
+
+ ginkgo.It("should resolve dynamically added dns entry test.dynamic.internal", func() {
+ client := gvproxyclient.New(&http.Client{
+ Transport: &http.Transport{
+ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
+ return net.Dial("unix", sock)
+ },
+ },
+ }, "http://base")
+ err := client.AddDNS(&types.Zone{
+ Name: "dynamic.internal.",
+ Records: []types.Record{
+ {
+ Name: "test",
+ IP: net.ParseIP("192.168.127.254"),
+ },
+ },
+ })
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+
+ out, err := sshExec("nslookup test.dynamic.internal")
+
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+ gomega.Expect(string(out)).To(gomega.ContainSubstring("Address: 192.168.127.254"))
+ })
+
+ ginkgo.It("should resolve recently added dns entry test.dynamic.internal", func() {
+ client := gvproxyclient.New(&http.Client{
+ Transport: &http.Transport{
+ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
+ return net.Dial("unix", sock)
+ },
+ },
+ }, "http://base")
+ err := client.AddDNS(&types.Zone{
+ Name: "dynamic.internal.",
+ Records: []types.Record{
+ {
+ Name: "test",
+ IP: net.ParseIP("192.168.127.254"),
+ },
+ },
+ })
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+
+ err = client.AddDNS(&types.Zone{
+ Name: "dynamic.internal.",
+ Records: []types.Record{
+ {
+ Name: "test",
+ IP: net.ParseIP("192.168.127.253"),
+ },
+ },
+ })
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+
+ out, err := sshExec("nslookup test.dynamic.internal")
+
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+ gomega.Expect(string(out)).To(gomega.ContainSubstring("Address: 192.168.127.253"))
+ })
+
+ ginkgo.It("should retain order of existing zone", func() {
+ client := gvproxyclient.New(&http.Client{
+ Transport: &http.Transport{
+ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
+ return net.Dial("unix", sock)
+ },
+ },
+ }, "http://base")
+ _ = client.AddDNS(&types.Zone{
+ Name: "dynamic.testing.",
+ DefaultIP: net.ParseIP("192.168.127.2"),
+ })
+ _ = client.AddDNS(&types.Zone{
+ Name: "testing.",
+ Records: []types.Record{
+ {
+ Name: "host",
+ IP: net.ParseIP("192.168.127.3"),
+ },
+ },
+ })
+ out, err := sshExec("nslookup test.dynamic.internal")
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+ gomega.Expect(string(out)).To(gomega.ContainSubstring("Address: 192.168.127.2"))
+
+ _ = client.AddDNS(&types.Zone{
+ Name: "testing.",
+ Records: []types.Record{
+ {
+ Name: "gateway",
+ IP: net.ParseIP("192.168.127.1"),
+ },
+ },
+ })
+ out, err = sshExec("nslookup *.dynamic.testing")
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+ gomega.Expect(string(out)).To(gomega.ContainSubstring("Address: 192.168.127.2"))
+
+ out, err = sshExec("nslookup gateway.testing")
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+ gomega.Expect(string(out)).To(gomega.ContainSubstring("Address: 192.168.127.1"))
+ })
+})
+
+var _ = ginkgo.Describe("command-line format", func() {
+ ginkgo.It("should convert Command to command line format", func() {
+ command := types.NewGvproxyCommand()
+ command.AddEndpoint("unix:///tmp/network.sock")
+ command.Debug = true
+ command.AddQemuSocket("tcp://0.0.0.0:1234")
+ command.PidFile = "~/gv-pidfile.txt"
+ command.LogFile = "~/gv.log"
+ command.AddForwardUser("demouser")
+
+ cmd := command.ToCmdline()
+ gomega.Expect(cmd).To(gomega.Equal([]string{
+ "-listen", "unix:///tmp/network.sock",
+ "-debug",
+ "-mtu", "1500",
+ "-ssh-port", "2222",
+ "-listen-qemu", "tcp://0.0.0.0:1234",
+ "-forward-user", "demouser",
+ "-pid-file", "~/gv-pidfile.txt",
+ "-log-file", "~/gv.log",
+ }))
+ })
+})
diff --git a/test/fcos.go b/test/fcos.go
new file mode 100644
index 0000000..e9cb9ac
--- /dev/null
+++ b/test/fcos.go
@@ -0,0 +1,80 @@
+package e2e
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/opencontainers/go-digest"
+)
+
+type FcosDownload struct {
+ DataDir string
+}
+
+type fcosDownloadInfo struct {
+ Location string
+ Sha256Sum string
+}
+
+func NewFcosDownloader(dataDir string) (*FcosDownload, error) {
+ return &FcosDownload{
+ DataDir: dataDir,
+ }, nil
+}
+
+func imageName(info *fcosDownloadInfo) string {
+ urlSplit := strings.Split(info.Location, "/")
+ return urlSplit[len(urlSplit)-1]
+}
+
+func (downloader *FcosDownload) DownloadImage() (string, error) {
+ info, err := getFCOSDownload()
+ if err != nil {
+ return "", err
+ }
+
+ compressedImage := filepath.Join(downloader.DataDir, imageName(info))
+ uncompressedImage := strings.TrimSuffix(filepath.Join(filepath.Dir(compressedImage), imageName(info)), ".xz")
+
+ // check if the latest image is already present
+ ok, err := downloader.updateAvailable(info, compressedImage)
+ if err != nil {
+ return "", err
+ }
+ if !ok {
+ if err := DownloadVMImage(info.Location, compressedImage); err != nil {
+ return "", err
+ }
+ }
+
+ if _, err := os.Stat(uncompressedImage); err == nil {
+ return uncompressedImage, nil
+ }
+ if err := Decompress(compressedImage, uncompressedImage); err != nil {
+ return "", err
+ }
+ return uncompressedImage, nil
+}
+
+func (downloader *FcosDownload) updateAvailable(info *fcosDownloadInfo, compressedImage string) (bool, error) {
+ // check the sha of the local image if it exists
+ // get the sha of the remote image
+ // == dont bother to pull
+ if _, err := os.Stat(compressedImage); os.IsNotExist(err) {
+ return false, nil
+ }
+ fd, err := os.Open(compressedImage)
+ if err != nil {
+ return false, err
+ }
+ defer fd.Close()
+ sum, err := digest.SHA256.FromReader(fd)
+ if err != nil {
+ return false, err
+ }
+ if sum.Encoded() == info.Sha256Sum {
+ return true, nil
+ }
+ return false, nil
+}
diff --git a/test/fcos_amd64.go b/test/fcos_amd64.go
new file mode 100644
index 0000000..3ffb906
--- /dev/null
+++ b/test/fcos_amd64.go
@@ -0,0 +1,64 @@
+package e2e
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+
+ "github.com/coreos/stream-metadata-go/fedoracoreos"
+ "github.com/coreos/stream-metadata-go/stream"
+ "github.com/sirupsen/logrus"
+)
+
+// This should get Exported and stay put as it will apply to all fcos downloads
+// getFCOS parses fedoraCoreOS's stream and returns the image download URL and the release version
+func getFCOSDownload() (*fcosDownloadInfo, error) {
+ streamurl := fedoracoreos.GetStreamURL(fedoracoreos.StreamNext)
+ resp, err := http.Get(streamurl.String())
+ if err != nil {
+ return nil, err
+ }
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, err
+ }
+ defer func() {
+ if err := resp.Body.Close(); err != nil {
+ logrus.Error(err)
+ }
+ }()
+
+ var fcosstable stream.Stream
+ if err := json.Unmarshal(body, &fcosstable); err != nil {
+ return nil, err
+ }
+ arch, ok := fcosstable.Architectures["x86_64"]
+ if !ok {
+ return nil, fmt.Errorf("unable to pull VM image: no targetArch in stream")
+ }
+ artifacts := arch.Artifacts
+ if artifacts == nil {
+ return nil, fmt.Errorf("unable to pull VM image: no artifact in stream")
+ }
+ qemu, ok := artifacts["qemu"]
+ if !ok {
+ return nil, fmt.Errorf("unable to pull VM image: no qemu artifact in stream")
+ }
+ formats := qemu.Formats
+ if formats == nil {
+ return nil, fmt.Errorf("unable to pull VM image: no formats in stream")
+ }
+ qcow, ok := formats["qcow2.xz"]
+ if !ok {
+ return nil, fmt.Errorf("unable to pull VM image: no qcow2.xz format in stream")
+ }
+ disk := qcow.Disk
+ if disk == nil {
+ return nil, fmt.Errorf("unable to pull VM image: no disk in stream")
+ }
+ return &fcosDownloadInfo{
+ Location: disk.Location,
+ Sha256Sum: disk.Sha256,
+ }, nil
+}
diff --git a/test/ignition.go b/test/ignition.go
new file mode 100644
index 0000000..a9dcbff
--- /dev/null
+++ b/test/ignition.go
@@ -0,0 +1,162 @@
+package e2e
+
+import (
+ "encoding/json"
+ "net/url"
+ "os"
+)
+
+var (
+ mode = 0644
+ dirMode = 0744
+ root = "root"
+ test = "test"
+ yes = true
+ no = false
+)
+
+func CreateIgnition(ignitionFile string, publicKey string, user string, password string) error {
+
+ linger := `[Unit]
+Description=Activate podman socket
+Wants=podman.socket
+[Service]
+ExecStart=/usr/bin/sleep infinity
+`
+
+ systemd := Systemd{
+ Units: []Unit{
+ {
+ Name: "systemd-resolved.service",
+ Enabled: &no,
+ Mask: &yes,
+ },
+ {
+ Name: "podman.socket",
+ Enabled: &yes,
+ },
+ },
+ }
+
+ passwd := Passwd{
+ Users: []PasswdUser{
+ {
+ Name: user,
+ PasswordHash: &password,
+ SSHAuthorizedKeys: []SSHAuthorizedKey{
+ SSHAuthorizedKey(publicKey),
+ },
+ Groups: []Group{
+ "wheel",
+ "sudo",
+ },
+ },
+ {
+ Name: "root",
+ PasswordHash: &password,
+ SSHAuthorizedKeys: []SSHAuthorizedKey{
+ SSHAuthorizedKey(publicKey),
+ },
+ },
+ },
+ }
+
+ storage := Storage{
+ // Replaces resolv.conf with an empty file that will be overwritten by NetworkManager
+ Files: []File{
+ {
+ Node: Node{
+ Group: NodeGroup{Name: &root},
+ Path: "/etc/resolv.conf",
+ User: NodeUser{Name: &root},
+ Overwrite: &yes,
+ },
+ FileEmbedded1: FileEmbedded1{
+ Contents: Resource{
+ Source: encodeData(""),
+ },
+ Mode: &mode,
+ },
+ },
+ {
+ Node: Node{
+ Group: NodeGroup{Name: &test},
+ Path: "/home/" + test + "/.config/systemd/user/linger-podman.service",
+ User: NodeUser{Name: &test},
+ Overwrite: &yes,
+ },
+ FileEmbedded1: FileEmbedded1{
+ Contents: Resource{
+ Source: encodeData(linger),
+ },
+ Mode: &mode,
+ },
+ },
+ {
+ Node: Node{
+ Group: NodeGroup{Name: &test},
+ Path: "/var/lib/systemd/linger/" + test,
+ User: NodeUser{Name: &test},
+ Overwrite: &yes,
+ },
+ FileEmbedded1: FileEmbedded1{
+ Contents: Resource{
+ Source: encodeData(""),
+ },
+ Mode: &mode,
+ },
+ },
+ },
+ Directories: []Directory{
+ dir("/home/" + test + "/.config"),
+ dir("/home/" + test + "/.config/containers"),
+ dir("/home/" + test + "/.config/systemd"),
+ dir("/home/" + test + "/.config/systemd/user"),
+ dir("/home/" + test + "/.config/systemd/user/default.target.wants"),
+ },
+ Links: []Link{
+ {
+ Node: Node{
+ Group: NodeGroup{Name: &test},
+ Path: "/home/" + test + "/.config/systemd/user/default.target.wants/linger-podman.service",
+ User: NodeUser{Name: &test},
+ },
+ LinkEmbedded1: LinkEmbedded1{
+ Hard: &no,
+ Target: "/home/" + test + "/.config/systemd/user/linger-podman.service",
+ },
+ },
+ },
+ }
+
+ config := Config{
+ Ignition: Ignition{Version: "3.2.0"},
+ Systemd: systemd,
+ Passwd: passwd,
+ Storage: storage,
+ }
+
+ contents, err := json.Marshal(config)
+ if err != nil {
+ return err
+ }
+
+ // #nosec
+ return os.WriteFile(ignitionFile, contents, 0644)
+}
+
+func dir(path string) Directory {
+ return Directory{
+ Node: Node{
+ Group: NodeGroup{Name: &test},
+ Path: path,
+ User: NodeUser{Name: &test},
+ },
+ DirectoryEmbedded1: DirectoryEmbedded1{Mode: &dirMode},
+ }
+}
+
+func encodeData(data string) *string {
+ str := "data:," + url.PathEscape(data)
+ return &str
+}
diff --git a/test/ignition_schema.go b/test/ignition_schema.go
new file mode 100644
index 0000000..5fb9d75
--- /dev/null
+++ b/test/ignition_schema.go
@@ -0,0 +1,248 @@
+package e2e
+
+// Taken from https://github.com/coreos/ignition/blob/master/config/v3_2/types/schema.go
+
+// generated by "schematyper --package=types config/v3_2/schema/ignition.json -o config/v3_2/types/schema.go --root-type=Config" -- DO NOT EDIT
+
+type Clevis struct {
+ Custom *Custom `json:"custom,omitempty"`
+ Tang []Tang `json:"tang,omitempty"`
+ Threshold *int `json:"threshold,omitempty"`
+ Tpm2 *bool `json:"tpm2,omitempty"`
+}
+
+type Config struct {
+ Ignition Ignition `json:"ignition"`
+ Passwd Passwd `json:"passwd,omitempty"`
+ Storage Storage `json:"storage,omitempty"`
+ Systemd Systemd `json:"systemd,omitempty"`
+}
+
+type Custom struct {
+ Config string `json:"config"`
+ NeedsNetwork *bool `json:"needsNetwork,omitempty"`
+ Pin string `json:"pin"`
+}
+
+type Device string
+
+type Directory struct {
+ Node
+ DirectoryEmbedded1
+}
+
+type DirectoryEmbedded1 struct {
+ Mode *int `json:"mode,omitempty"`
+}
+
+type Disk struct {
+ Device string `json:"device"`
+ Partitions []Partition `json:"partitions,omitempty"`
+ WipeTable *bool `json:"wipeTable,omitempty"`
+}
+
+type Dropin struct {
+ Contents *string `json:"contents,omitempty"`
+ Name string `json:"name"`
+}
+
+type File struct {
+ Node
+ FileEmbedded1
+}
+
+type FileEmbedded1 struct {
+ Append []Resource `json:"append,omitempty"`
+ Contents Resource `json:"contents,omitempty"`
+ Mode *int `json:"mode,omitempty"`
+}
+
+type Filesystem struct {
+ Device string `json:"device"`
+ Format *string `json:"format,omitempty"`
+ Label *string `json:"label,omitempty"`
+ MountOptions []MountOption `json:"mountOptions,omitempty"`
+ Options []FilesystemOption `json:"options,omitempty"`
+ Path *string `json:"path,omitempty"`
+ UUID *string `json:"uuid,omitempty"`
+ WipeFilesystem *bool `json:"wipeFilesystem,omitempty"`
+}
+
+type FilesystemOption string
+
+type Group string
+
+type HTTPHeader struct {
+ Name string `json:"name"`
+ Value *string `json:"value,omitempty"`
+}
+
+type HTTPHeaders []HTTPHeader
+
+type Ignition struct {
+ Config IgnitionConfig `json:"config,omitempty"`
+ Proxy Proxy `json:"proxy,omitempty"`
+ Security Security `json:"security,omitempty"`
+ Timeouts Timeouts `json:"timeouts,omitempty"`
+ Version string `json:"version,omitempty"`
+}
+
+type IgnitionConfig struct {
+ Merge []Resource `json:"merge,omitempty"`
+ Replace Resource `json:"replace,omitempty"`
+}
+
+type Link struct {
+ Node
+ LinkEmbedded1
+}
+
+type LinkEmbedded1 struct {
+ Hard *bool `json:"hard,omitempty"`
+ Target string `json:"target"`
+}
+
+type Luks struct {
+ Clevis *Clevis `json:"clevis,omitempty"`
+ Device *string `json:"device,omitempty"`
+ KeyFile Resource `json:"keyFile,omitempty"`
+ Label *string `json:"label,omitempty"`
+ Name string `json:"name"`
+ Options []LuksOption `json:"options,omitempty"`
+ UUID *string `json:"uuid,omitempty"`
+ WipeVolume *bool `json:"wipeVolume,omitempty"`
+}
+
+type LuksOption string
+
+type MountOption string
+
+type NoProxyItem string
+
+type Node struct {
+ Group NodeGroup `json:"group,omitempty"`
+ Overwrite *bool `json:"overwrite,omitempty"`
+ Path string `json:"path"`
+ User NodeUser `json:"user,omitempty"`
+}
+
+type NodeGroup struct {
+ ID *int `json:"id,omitempty"`
+ Name *string `json:"name,omitempty"`
+}
+
+type NodeUser struct {
+ ID *int `json:"id,omitempty"`
+ Name *string `json:"name,omitempty"`
+}
+
+type Partition struct {
+ GUID *string `json:"guid,omitempty"`
+ Label *string `json:"label,omitempty"`
+ Number int `json:"number,omitempty"`
+ Resize *bool `json:"resize,omitempty"`
+ ShouldExist *bool `json:"shouldExist,omitempty"`
+ SizeMiB *int `json:"sizeMiB,omitempty"`
+ StartMiB *int `json:"startMiB,omitempty"`
+ TypeGUID *string `json:"typeGuid,omitempty"`
+ WipePartitionEntry *bool `json:"wipePartitionEntry,omitempty"`
+}
+
+type Passwd struct {
+ Groups []PasswdGroup `json:"groups,omitempty"`
+ Users []PasswdUser `json:"users,omitempty"`
+}
+
+type PasswdGroup struct {
+ Gid *int `json:"gid,omitempty"`
+ Name string `json:"name"`
+ PasswordHash *string `json:"passwordHash,omitempty"`
+ ShouldExist *bool `json:"shouldExist,omitempty"`
+ System *bool `json:"system,omitempty"`
+}
+
+type PasswdUser struct {
+ Gecos *string `json:"gecos,omitempty"`
+ Groups []Group `json:"groups,omitempty"`
+ HomeDir *string `json:"homeDir,omitempty"`
+ Name string `json:"name"`
+ NoCreateHome *bool `json:"noCreateHome,omitempty"`
+ NoLogInit *bool `json:"noLogInit,omitempty"`
+ NoUserGroup *bool `json:"noUserGroup,omitempty"`
+ PasswordHash *string `json:"passwordHash,omitempty"`
+ PrimaryGroup *string `json:"primaryGroup,omitempty"`
+ SSHAuthorizedKeys []SSHAuthorizedKey `json:"sshAuthorizedKeys,omitempty"`
+ Shell *string `json:"shell,omitempty"`
+ ShouldExist *bool `json:"shouldExist,omitempty"`
+ System *bool `json:"system,omitempty"`
+ UID *int `json:"uid,omitempty"`
+}
+
+type Proxy struct {
+ HTTPProxy *string `json:"httpProxy,omitempty"`
+ HTTPSProxy *string `json:"httpsProxy,omitempty"`
+ NoProxy []NoProxyItem `json:"noProxy,omitempty"`
+}
+
+type Raid struct {
+ Devices []Device `json:"devices"`
+ Level string `json:"level"`
+ Name string `json:"name"`
+ Options []RaidOption `json:"options,omitempty"`
+ Spares *int `json:"spares,omitempty"`
+}
+
+type RaidOption string
+
+type Resource struct {
+ Compression *string `json:"compression,omitempty"`
+ HTTPHeaders HTTPHeaders `json:"httpHeaders,omitempty"`
+ Source *string `json:"source,omitempty"`
+ Verification Verification `json:"verification,omitempty"`
+}
+
+type SSHAuthorizedKey string
+
+type Security struct {
+ TLS TLS `json:"tls,omitempty"`
+}
+
+type Storage struct {
+ Directories []Directory `json:"directories,omitempty"`
+ Disks []Disk `json:"disks,omitempty"`
+ Files []File `json:"files,omitempty"`
+ Filesystems []Filesystem `json:"filesystems,omitempty"`
+ Links []Link `json:"links,omitempty"`
+ Luks []Luks `json:"luks,omitempty"`
+ Raid []Raid `json:"raid,omitempty"`
+}
+
+type Systemd struct {
+ Units []Unit `json:"units,omitempty"`
+}
+
+type TLS struct {
+ CertificateAuthorities []Resource `json:"certificateAuthorities,omitempty"`
+}
+
+type Tang struct {
+ Thumbprint *string `json:"thumbprint,omitempty"`
+ URL string `json:"url,omitempty"`
+}
+
+type Timeouts struct {
+ HTTPResponseHeaders *int `json:"httpResponseHeaders,omitempty"`
+ HTTPTotal *int `json:"httpTotal,omitempty"`
+}
+
+type Unit struct {
+ Contents *string `json:"contents,omitempty"`
+ Dropins []Dropin `json:"dropins,omitempty"`
+ Enabled *bool `json:"enabled,omitempty"`
+ Mask *bool `json:"mask,omitempty"`
+ Name string `json:"name"`
+}
+
+type Verification struct {
+ Hash *string `json:"hash,omitempty"`
+} \ No newline at end of file
diff --git a/test/performance.sh b/test/performance.sh
new file mode 100755
index 0000000..4f4c413
--- /dev/null
+++ b/test/performance.sh
@@ -0,0 +1,54 @@
+#!/usr/bin/env bash
+
+set -x
+
+VM=$1
+GVPROXY_SOCKET=$2
+
+echo "Testing Internet access with a server running on the host"
+
+nohup iperf3 -s > /dev/null 2>&1 &
+serverPID=$!
+
+ssh $VM curl https://iperf.fr/download/ubuntu/libiperf.so.0_3.1.3 -o libiperf.so.0
+ssh $VM curl https://iperf.fr/download/ubuntu/iperf3_3.1.3 -o iperf3
+ssh $VM chmod +x iperf3
+
+echo "TCP: sending data"
+ssh $VM LD_LIBRARY_PATH=. ./iperf3 -c host.crc.testing
+echo "TCP: receiving data"
+ssh $VM LD_LIBRARY_PATH=. ./iperf3 -c host.crc.testing -R
+
+echo "UDP: sending data"
+ssh $VM LD_LIBRARY_PATH=. ./iperf3 -c host.crc.testing -u
+echo "UDP: receiving data"
+ssh $VM LD_LIBRARY_PATH=. ./iperf3 -c host.crc.testing -R -u
+
+kill $serverPID
+
+echo "Testing forwarder with a server running in the VM"
+
+curl --unix-socket $GVPROXY_SOCKET http:/unix/services/forwarder/expose -X POST \
+ -d'{"local":":5201", "protocol": "udp", "remote": "192.168.127.2:5201"}'
+curl --unix-socket $GVPROXY_SOCKET http:/unix/services/forwarder/expose -X POST \
+ -d'{"local":":5201", "protocol": "tcp", "remote": "192.168.127.2:5201"}'
+
+ssh $VM LD_LIBRARY_PATH=. ./iperf3 -s > /dev/null 2>&1 &
+sleep 1
+
+echo "TCP: sending data"
+iperf3 -c 127.0.0.1
+echo "TCP: receiving data"
+iperf3 -c 127.0.0.1 -R
+
+echo "UDP: sending data"
+iperf3 -c 127.0.0.1 -u -l 9216
+echo "UDP: receiving data"
+iperf3 -c 127.0.0.1 -R -u -l 9216
+
+ssh $VM pkill iperf3
+
+curl --unix-socket $GVPROXY_SOCKET http:/unix/services/forwarder/unexpose -X POST \
+ -d'{"local":":5201", "protocol": "udp"}'
+curl --unix-socket $GVPROXY_SOCKET http:/unix/services/forwarder/unexpose -X POST \
+ -d'{"local":":5201", "protocol": "tcp"}'
diff --git a/test/port_forwarding_test.go b/test/port_forwarding_test.go
new file mode 100644
index 0000000..c38e4f6
--- /dev/null
+++ b/test/port_forwarding_test.go
@@ -0,0 +1,269 @@
+package e2e
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "net"
+ "net/http"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "time"
+
+ gvproxyclient "github.com/containers/gvisor-tap-vsock/pkg/client"
+ "github.com/containers/gvisor-tap-vsock/pkg/transport"
+ "github.com/containers/gvisor-tap-vsock/pkg/types"
+ "github.com/onsi/ginkgo"
+ "github.com/onsi/gomega"
+ log "github.com/sirupsen/logrus"
+)
+
+var _ = ginkgo.Describe("port forwarding", func() {
+ client := gvproxyclient.New(&http.Client{
+ Transport: &http.Transport{
+ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
+ return net.Dial("unix", sock)
+ },
+ },
+ }, "http://base")
+
+ ginkgo.It("should reach a http server on the host", func() {
+ ln, err := net.Listen("tcp", "127.0.0.1:9090")
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+ defer ln.Close()
+
+ mux := http.NewServeMux()
+ mux.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
+ _, _ = writer.Write([]byte("Hello from the host"))
+ })
+ go func() {
+ s := &http.Server{
+ Handler: mux,
+ ReadTimeout: 10 * time.Second,
+ WriteTimeout: 10 * time.Second,
+ }
+ err := s.Serve(ln)
+ if err != nil {
+ log.Error(err)
+ }
+ }()
+
+ out, err := sshExec("curl http://host.containers.internal:9090")
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+ gomega.Expect(string(out)).To(gomega.ContainSubstring("Hello from the host"))
+
+ out, err = sshExec("curl http://host.docker.internal:9090")
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+ gomega.Expect(string(out)).To(gomega.ContainSubstring("Hello from the host"))
+ })
+
+ ginkgo.It("should reach a http server in the VM using dynamic port forwarding", func() {
+ _, err := net.Dial("tcp", "127.0.0.1:9090")
+ gomega.Expect(err).Should(gomega.HaveOccurred())
+ gomega.Expect(err.Error()).To(gomega.HaveSuffix("connection refused"))
+
+ gomega.Expect(client.Expose(&types.ExposeRequest{
+ Local: "127.0.0.1:9090",
+ Remote: "192.168.127.2:8080",
+ })).Should(gomega.Succeed())
+
+ gomega.Eventually(func(g gomega.Gomega) {
+ resp, err := http.Get("http://127.0.0.1:9090")
+ g.Expect(err).ShouldNot(gomega.HaveOccurred())
+ g.Expect(resp.StatusCode).To(gomega.Equal(http.StatusOK))
+ }).Should(gomega.Succeed())
+
+ gomega.Expect(client.Unexpose(&types.UnexposeRequest{
+ Local: "127.0.0.1:9090",
+ })).Should(gomega.Succeed())
+
+ gomega.Eventually(func(g gomega.Gomega) {
+ _, err = net.Dial("tcp", "127.0.0.1:9090")
+ g.Expect(err).Should(gomega.HaveOccurred())
+ g.Expect(err.Error()).To(gomega.HaveSuffix("connection refused"))
+ }).Should(gomega.Succeed())
+ })
+
+ ginkgo.It("should reach a dns server in the VM using dynamic port forwarding", func() {
+ gomega.Expect(client.Expose(&types.ExposeRequest{
+ Local: ":1053",
+ Remote: "192.168.127.2:53",
+ Protocol: "udp",
+ })).Should(gomega.Succeed())
+
+ gomega.Eventually(func(g gomega.Gomega) {
+ cmd := exec.Command("nslookup", "-timeout=1", "-port=1053", "foobar", "127.0.0.1")
+ out, err := cmd.CombinedOutput()
+ g.Expect(err).ShouldNot(gomega.HaveOccurred())
+ g.Expect(string(out)).To(gomega.ContainSubstring("Address: 1.2.3.4"))
+ }).Should(gomega.Succeed())
+
+ gomega.Expect(client.Unexpose(&types.UnexposeRequest{
+ Local: ":1053",
+ Protocol: "udp",
+ })).Should(gomega.Succeed())
+ })
+
+ ginkgo.It("should reach a http server in the VM using the tunneling of the daemon", func() {
+ httpClient := &http.Client{
+ Transport: &http.Transport{
+ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
+ conn, err := net.Dial("unix", sock)
+ if err != nil {
+ return nil, err
+ }
+ return conn, transport.Tunnel(conn, "192.168.127.2", 8080)
+ },
+ },
+ }
+
+ gomega.Eventually(func(g gomega.Gomega) {
+ resp, err := httpClient.Get("http://placeholder/")
+ g.Expect(err).ShouldNot(gomega.HaveOccurred())
+ g.Expect(resp.StatusCode).To(gomega.Equal(http.StatusOK))
+ }).Should(gomega.Succeed())
+ })
+
+ ginkgo.It("should reach a http server in the VM using dynamic port forwarding configured within the VM", func() {
+ _, err := net.Dial("tcp", "127.0.0.1:9090")
+ gomega.Expect(err).Should(gomega.HaveOccurred())
+ gomega.Expect(err.Error()).To(gomega.HaveSuffix("connection refused"))
+
+ _, err = sshExec(`curl http://gateway.containers.internal/services/forwarder/expose -X POST -d'{"local":":9090", "remote":":8080"}'`)
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+
+ gomega.Eventually(func(g gomega.Gomega) {
+ resp, err := http.Get("http://127.0.0.1:9090")
+ g.Expect(err).ShouldNot(gomega.HaveOccurred())
+ g.Expect(resp.StatusCode).To(gomega.Equal(http.StatusOK))
+ }).Should(gomega.Succeed())
+
+ _, err = sshExec(`curl http://gateway.containers.internal/services/forwarder/unexpose -X POST -d'{"local":":9090"}'`)
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+
+ gomega.Eventually(func(g gomega.Gomega) {
+ _, err = net.Dial("tcp", "127.0.0.1:9090")
+ g.Expect(err).Should(gomega.HaveOccurred())
+ g.Expect(err.Error()).To(gomega.HaveSuffix("connection refused"))
+ }).Should(gomega.Succeed())
+ })
+
+ ginkgo.It("should reach rootless podman API using unix socket forwarding over ssh", func() {
+ httpClient := &http.Client{
+ Transport: &http.Transport{
+ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
+ return net.Dial("unix", forwardSock)
+ },
+ },
+ }
+
+ gomega.Eventually(func(g gomega.Gomega) {
+ resp, err := httpClient.Get("http://host/_ping")
+ g.Expect(err).ShouldNot(gomega.HaveOccurred())
+ g.Expect(resp.StatusCode).To(gomega.Equal(http.StatusOK))
+ g.Expect(resp.ContentLength).To(gomega.Equal(int64(2)))
+
+ reply := make([]byte, resp.ContentLength)
+ _, err = io.ReadAtLeast(resp.Body, reply, len(reply))
+
+ g.Expect(err).ShouldNot(gomega.HaveOccurred())
+ g.Expect(string(reply)).To(gomega.Equal("OK"))
+ }).Should(gomega.Succeed())
+ })
+
+ ginkgo.It("should reach rootful podman API using unix socket forwarding over ssh", func() {
+ httpClient := &http.Client{
+ Transport: &http.Transport{
+ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
+ return net.Dial("unix", forwardRootSock)
+ },
+ },
+ }
+
+ gomega.Eventually(func(g gomega.Gomega) {
+ resp, err := httpClient.Get("http://host/_ping")
+ g.Expect(err).ShouldNot(gomega.HaveOccurred())
+ g.Expect(resp.StatusCode).To(gomega.Equal(http.StatusOK))
+ g.Expect(resp.ContentLength).To(gomega.Equal(int64(2)))
+
+ reply := make([]byte, resp.ContentLength)
+ _, err = io.ReadAtLeast(resp.Body, reply, len(reply))
+
+ g.Expect(err).ShouldNot(gomega.HaveOccurred())
+ g.Expect(string(reply)).To(gomega.Equal("OK"))
+ }).Should(gomega.Succeed())
+ })
+
+ ginkgo.It("should expose and reach an http service using unix to tcp forwarding", func() {
+ if runtime.GOOS == "windows" {
+ ginkgo.Skip("AF_UNIX not supported on Windows")
+ }
+
+ unix2tcpfwdsock, _ := filepath.Abs(filepath.Join(tmpDir, "podman-unix-to-unix-forwarding.sock"))
+
+ out, err := sshExec(`curl http://gateway.containers.internal/services/forwarder/expose -X POST -d'{"protocol":"unix","local":"` + unix2tcpfwdsock + `","remote":"tcp://192.168.127.2:8080"}'`)
+ gomega.Expect(string(out)).Should(gomega.Equal(""))
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+
+ gomega.Eventually(func(g gomega.Gomega) {
+ sockfile, err := os.Stat(unix2tcpfwdsock)
+ g.Expect(err).ShouldNot(gomega.HaveOccurred())
+ g.Expect(sockfile.Mode().Type().String()).To(gomega.Equal(os.ModeSocket.String()))
+ }).Should(gomega.Succeed())
+
+ httpClient := &http.Client{
+ Transport: &http.Transport{
+ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
+ return net.Dial("unix", unix2tcpfwdsock)
+ },
+ },
+ }
+
+ gomega.Eventually(func(g gomega.Gomega) {
+ resp, err := httpClient.Get("http://placeholder/")
+ g.Expect(err).ShouldNot(gomega.HaveOccurred())
+ g.Expect(resp.StatusCode).To(gomega.Equal(http.StatusOK))
+ }).Should(gomega.Succeed())
+ })
+
+ ginkgo.It("should expose and reach rootless podman API using unix to unix forwarding over ssh", func() {
+ if runtime.GOOS == "windows" {
+ ginkgo.Skip("AF_UNIX not supported on Windows")
+ }
+
+ unix2unixfwdsock, _ := filepath.Abs(filepath.Join(tmpDir, "podman-unix-to-unix-forwarding.sock"))
+
+ remoteuri := fmt.Sprintf(`ssh-tunnel://root@%s:%d%s?key=%s`, "192.168.127.2", 22, podmanSock, privateKeyFile)
+ _, err := sshExec(`curl http://192.168.127.1/services/forwarder/expose -X POST -d'{"protocol":"unix","local":"` + unix2unixfwdsock + `","remote":"` + remoteuri + `"}'`)
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+
+ gomega.Eventually(func(g gomega.Gomega) {
+ sockfile, err := os.Stat(unix2unixfwdsock)
+ g.Expect(err).ShouldNot(gomega.HaveOccurred())
+ g.Expect(sockfile.Mode().Type().String()).To(gomega.Equal(os.ModeSocket.String()))
+ }).Should(gomega.Succeed())
+
+ httpClient := &http.Client{
+ Transport: &http.Transport{
+ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
+ return net.Dial("unix", unix2unixfwdsock)
+ },
+ },
+ }
+
+ gomega.Eventually(func(g gomega.Gomega) {
+ resp, err := httpClient.Get("http://host/_ping")
+ g.Expect(err).ShouldNot(gomega.HaveOccurred())
+ g.Expect(resp.StatusCode).To(gomega.Equal(http.StatusOK))
+ g.Expect(resp.ContentLength).To(gomega.Equal(int64(2)))
+
+ reply := make([]byte, resp.ContentLength)
+ _, err = io.ReadAtLeast(resp.Body, reply, len(reply))
+
+ g.Expect(err).ShouldNot(gomega.HaveOccurred())
+ g.Expect(string(reply)).To(gomega.Equal("OK"))
+ }).Should(gomega.Succeed())
+ })
+})
diff --git a/test/pull.go b/test/pull.go
new file mode 100644
index 0000000..f1fa236
--- /dev/null
+++ b/test/pull.go
@@ -0,0 +1,81 @@
+package e2e
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+ "os/exec"
+ "strings"
+
+ "github.com/sirupsen/logrus"
+)
+
+// DownloadVMImage downloads a VM image from url to given path
+// with download status
+func DownloadVMImage(downloadURL string, localImagePath string) error {
+ fmt.Println("Downloading VM image: " + downloadURL)
+
+ out, err := os.Create(localImagePath)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if err := out.Close(); err != nil {
+ logrus.Error(err)
+ }
+ }()
+
+ // #nosec
+ resp, err := http.Get(downloadURL)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if err := resp.Body.Close(); err != nil {
+ logrus.Error(err)
+ }
+ }()
+
+ if resp.StatusCode != http.StatusOK {
+ return fmt.Errorf("error downloading VM image %s: %s", downloadURL, resp.Status)
+ }
+
+ if _, err := io.Copy(out, resp.Body); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func Decompress(localPath, uncompressedPath string) error {
+ uncompressedFileWriter, err := os.OpenFile(uncompressedPath, os.O_CREATE|os.O_RDWR, 0600)
+ if err != nil {
+ return err
+ }
+
+ if !strings.HasSuffix(localPath, ".xz") {
+ return fmt.Errorf("unsupported compression for %s", localPath)
+ }
+
+ fmt.Printf("Extracting %s\n", localPath)
+ return decompressXZ(localPath, uncompressedFileWriter)
+}
+
+// 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)
+ stdOut, err := cmd.StdoutPipe()
+ if err != nil {
+ return err
+ }
+ cmd.Stderr = os.Stderr
+ go func() {
+ if _, err := io.Copy(output, stdOut); err != nil {
+ logrus.Error(err)
+ }
+ }()
+ return cmd.Run()
+}
diff --git a/test/suite_test.go b/test/suite_test.go
new file mode 100644
index 0000000..5c08d5f
--- /dev/null
+++ b/test/suite_test.go
@@ -0,0 +1,242 @@
+package e2e
+
+import (
+ "flag"
+ "fmt"
+ "io"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "strconv"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/onsi/ginkgo"
+ "github.com/onsi/gomega"
+ "github.com/pkg/errors"
+ log "github.com/sirupsen/logrus"
+)
+
+func TestSuite(t *testing.T) {
+ gomega.RegisterFailHandler(ginkgo.Fail)
+ ginkgo.RunSpecs(t, "gvisor-tap-vsock suite")
+}
+
+const (
+ sock = "/tmp/gvproxy-api.sock"
+ qemuPort = 5555
+ sshPort = 2222
+ ignitionUser = "test"
+ qconLog = "qcon.log"
+ podmanSock = "/run/user/1001/podman/podman.sock"
+ podmanRootSock = "/run/podman/podman.sock"
+
+ // #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
+)
+
+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")
+
+}
+
+var _ = ginkgo.BeforeSuite(func() {
+ gomega.Expect(os.MkdirAll(filepath.Join(tmpDir, "disks"), os.ModePerm)).Should(gomega.Succeed())
+
+ downloader, err := NewFcosDownloader(filepath.Join(tmpDir, "disks"))
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+ qemuImage, err := downloader.DownloadImage()
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+
+ publicKey, err := createSSHKeys()
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+
+ err = CreateIgnition(ignFile, publicKey, ignitionUser, ignitionPasswordHash)
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+
+outer:
+ for panics := 0; ; panics++ {
+ _ = os.Remove(sock)
+
+ // #nosec
+ host = exec.Command(filepath.Join(binDir, "gvproxy"), fmt.Sprintf("--listen=unix://%s", sock), fmt.Sprintf("--listen-qemu=tcp://127.0.0.1:%d", qemuPort),
+ fmt.Sprintf("--forward-sock=%s", forwardSock), fmt.Sprintf("--forward-dest=%s", podmanSock), fmt.Sprintf("--forward-user=%s", ignitionUser),
+ fmt.Sprintf("--forward-identity=%s", privateKeyFile),
+ fmt.Sprintf("--forward-sock=%s", forwardRootSock), fmt.Sprintf("--forward-dest=%s", podmanRootSock), fmt.Sprintf("--forward-user=%s", "root"),
+ fmt.Sprintf("--forward-identity=%s", 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)
+ }
+ }()
+
+ for {
+ _, err := os.Stat(sock)
+ if os.IsNotExist(err) {
+ log.Info("waiting for socket")
+ time.Sleep(100 * time.Millisecond)
+ continue
+ }
+ break
+ }
+
+ template := `%s -m 2048 -nographic -serial file:%s -snapshot -drive if=virtio,file=%s -fw_cfg name=opt/com.coreos/config,file=%s -netdev socket,id=vlan,connect=127.0.0.1:%d -device virtio-net-pci,netdev=vlan,mac=5a:94:ef:e4:0c:ee`
+ // #nosec
+ client = exec.Command(qemuExecutable(), strings.Split(fmt.Sprintf(template, qemuArgs(), 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()
+ _ = host.Process.Kill()
+ _ = os.Remove(qconLog)
+ continue outer
+ }
+
+ log.Infof("waiting for client to connect: %v", err)
+ time.Sleep(time.Second)
+ }
+ }
+
+ err = scp(filepath.Join(binDir, "test-companion"), "/tmp/test-companion")
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+
+ // start an embedded DNS and http server in the VM. Wait a bit for the server to start.
+ cmd := sshCommand("sudo /tmp/test-companion")
+ gomega.Expect(cmd.Start()).ShouldNot(gomega.HaveOccurred())
+ time.Sleep(5 * time.Second)
+})
+
+func qemuExecutable() string {
+ if runtime.GOOS == "darwin" {
+ return "qemu-system-x86_64"
+ }
+ return "qemu-kvm"
+}
+
+func qemuArgs() string {
+ if runtime.GOOS == "darwin" {
+ return "-machine q35,accel=hvf:tcg -smp 4 -cpu host"
+ }
+ return "-cpu host"
+}
+
+func createSSHKeys() (string, error) {
+ _ = os.Remove(publicKeyFile)
+ _ = os.Remove(privateKeyFile)
+ err := exec.Command("ssh-keygen", "-N", "", "-t", "ed25519", "-f", privateKeyFile).Run()
+ if err != nil {
+ return "", errors.Wrap(err, "Could not generate ssh keys")
+ }
+
+ return readPublicKey()
+}
+
+func readPublicKey() (string, error) {
+ publicKey, err := os.ReadFile(publicKeyFile)
+ if err != nil {
+ return "", nil
+ }
+
+ return strings.TrimSpace(string(publicKey)), nil
+}
+
+func scp(src, dst string) error {
+ sshCmd := exec.Command("scp",
+ "-o", "UserKnownHostsFile=/dev/null",
+ "-o", "StrictHostKeyChecking=no",
+ "-i", privateKeyFile,
+ "-P", strconv.Itoa(sshPort),
+ src,
+ fmt.Sprintf("%s@127.0.0.1:%s", ignitionUser, dst)) // #nosec G204
+ sshCmd.Stderr = os.Stderr
+ sshCmd.Stdout = os.Stdout
+ return sshCmd.Run()
+}
+
+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",
+ "-i", privateKeyFile,
+ "-p", strconv.Itoa(sshPort),
+ fmt.Sprintf("%s@127.0.0.1", 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)
+ }
+ }
+})
diff --git a/test/wsl.sh b/test/wsl.sh
new file mode 100755
index 0000000..a28daf5
--- /dev/null
+++ b/test/wsl.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+set -x
+
+./bin/gvforwarder \
+ -url="stdio:$(pwd)/bin/gvproxy-windows.exe?listen-stdio=accept&debug=true" \
+ -iface="eth1" \
+ -stop-if-exist="" \
+ -debug