From a17de43ef12250cd9a0ffdd8ff2d05fb18fcf322 Mon Sep 17 00:00:00 2001 From: Chris O'Hara Date: Mon, 8 May 2023 17:08:20 +1000 Subject: net: implement wasip1 FileListener and FileConn Implements net.FileListener and net.FileConn for wasip1. net.FileListener can be used with a pre-opened socket. If the WASM module knows the file descriptor, a listener can be constructed with: l, err := net.FileListener(os.NewFile(fd, "")) If the WASM module does not know the file descriptor, but knows that at least one of the preopens is a socket, it can find the file descriptor and construct a listener like so: func findListener() (net.Listener, error) { // We start looking for pre-opened sockets at fd=3 because 0, 1, // and 2 are reserved for stdio. Pre-opened directories also // start at fd=3, so we skip fds that aren't sockets. Once we // reach EBADF we know there are no more pre-opens. for preopenFd := uintptr(3); ; preopenFd++ { l, err := net.FileListener(os.NewFile(preopenFd, "")) var se syscall.Errno switch errors.As(err, &se); se { case syscall.ENOTSOCK: continue case syscall.EBADF: err = nil } return l, err } } A similar strategy can be used with net.FileConn and pre-opened connection sockets. The wasmtime runtime supports pre-opening listener sockets: $ wasmtime --tcplisten 127.0.0.1:8080 module.wasm Change-Id: Iec6ae4ffa84b3753cce4f56a2817e150445db643 Reviewed-on: https://go-review.googlesource.com/c/go/+/493358 Reviewed-by: Matthew Dempsky Run-TryBot: Ian Lance Taylor Reviewed-by: Dmitri Shuralyov TryBot-Bypass: Dmitri Shuralyov Reviewed-by: Johan Brandhorst-Satzkorn Auto-Submit: Johan Brandhorst-Satzkorn Reviewed-by: Dmitri Shuralyov --- src/runtime/internal/wasitest/tcpecho_test.go | 92 +++++++++++++++++++++++ src/runtime/internal/wasitest/testdata/tcpecho.go | 74 ++++++++++++++++++ 2 files changed, 166 insertions(+) create mode 100644 src/runtime/internal/wasitest/tcpecho_test.go create mode 100644 src/runtime/internal/wasitest/testdata/tcpecho.go (limited to 'src/runtime') diff --git a/src/runtime/internal/wasitest/tcpecho_test.go b/src/runtime/internal/wasitest/tcpecho_test.go new file mode 100644 index 0000000000..506e6fe40a --- /dev/null +++ b/src/runtime/internal/wasitest/tcpecho_test.go @@ -0,0 +1,92 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package wasi_test + +import ( + "bytes" + "fmt" + "math/rand" + "net" + "os" + "os/exec" + "testing" + "time" +) + +func TestTCPEcho(t *testing.T) { + if target != "wasip1/wasm" { + t.Skip() + } + + // We're unable to pass port 0 here (let the OS choose a spare port). + // Although wasmtime accepts port 0, and testdata/main.go successfully + // listens, there's no way for this test case to query the chosen port + // so that it can connect to the WASM module. The WASM module itself + // cannot access any information about the socket due to limitations + // with WASI preview 1 networking, and wasmtime does not log the address + // when you preopen a socket. Instead, we probe for a free port here. + var host string + port := rand.Intn(10000) + 40000 + for attempts := 0; attempts < 10; attempts++ { + host = fmt.Sprintf("127.0.0.1:%d", port) + l, err := net.Listen("tcp", host) + if err == nil { + l.Close() + break + } + port++ + } + + subProcess := exec.Command("go", "run", "./testdata/tcpecho.go") + + subProcess.Env = append(os.Environ(), "GOOS=wasip1", "GOARCH=wasm") + + switch os.Getenv("GOWASIRUNTIME") { + case "wasmtime": + subProcess.Env = append(subProcess.Env, "GOWASIRUNTIMEARGS=--tcplisten="+host) + default: + t.Skip("WASI runtime does not support sockets") + } + + var b bytes.Buffer + subProcess.Stdout = &b + subProcess.Stderr = &b + + if err := subProcess.Start(); err != nil { + t.Log(b.String()) + t.Fatal(err) + } + defer subProcess.Process.Kill() + + var conn net.Conn + var err error + for attempts := 0; attempts < 5; attempts++ { + conn, err = net.Dial("tcp", host) + if err == nil { + break + } + time.Sleep(500 * time.Millisecond) + } + if err != nil { + t.Log(b.String()) + t.Fatal(err) + } + defer conn.Close() + + payload := []byte("foobar") + if _, err := conn.Write(payload); err != nil { + t.Fatal(err) + } + var buf [256]byte + n, err := conn.Read(buf[:]) + if err != nil { + t.Fatal(err) + } + if string(buf[:n]) != string(payload) { + t.Error("unexpected payload") + t.Logf("expect: %d bytes (%v)", len(payload), payload) + t.Logf("actual: %d bytes (%v)", n, buf[:n]) + } +} diff --git a/src/runtime/internal/wasitest/testdata/tcpecho.go b/src/runtime/internal/wasitest/testdata/tcpecho.go new file mode 100644 index 0000000000..819e352688 --- /dev/null +++ b/src/runtime/internal/wasitest/testdata/tcpecho.go @@ -0,0 +1,74 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "errors" + "net" + "os" + "syscall" +) + +func main() { + if err := run(); err != nil { + println(err) + os.Exit(1) + } +} + +func run() error { + l, err := findListener() + if err != nil { + return err + } + if l == nil { + return errors.New("no pre-opened sockets available") + } + defer l.Close() + + c, err := l.Accept() + if err != nil { + return err + } + return handleConn(c) +} + +func handleConn(c net.Conn) error { + defer c.Close() + + var buf [128]byte + n, err := c.Read(buf[:]) + if err != nil { + return err + } + if _, err := c.Write(buf[:n]); err != nil { + return err + } + if err := c.(*net.TCPConn).CloseWrite(); err != nil { + return err + } + return c.Close() +} + +func findListener() (net.Listener, error) { + // We start looking for pre-opened sockets at fd=3 because 0, 1, and 2 + // are reserved for stdio. Pre-opened directors also start at fd=3, so + // we skip fds that aren't sockets. Once we reach EBADF we know there + // are no more pre-opens. + for preopenFd := uintptr(3); ; preopenFd++ { + f := os.NewFile(preopenFd, "") + l, err := net.FileListener(f) + f.Close() + + var se syscall.Errno + switch errors.As(err, &se); se { + case syscall.ENOTSOCK: + continue + case syscall.EBADF: + err = nil + } + return l, err + } +} -- cgit v1.3