diff --git a/dial.go b/dial.go index edaddbb..262159c 100644 --- a/dial.go +++ b/dial.go @@ -222,8 +222,20 @@ type ListenConfig struct { // // The ctx argument is used while resolving the address on which to listen; // it does not affect the returned Listener. +// TINYGO: netdev.go has no lookup cancellation or socket control hook. +// Context is checked before and after lookup. Default socket options apply. func (lc *ListenConfig) Listen(ctx context.Context, network, address string) (Listener, error) { - return nil, errors.New("dial:ListenConfig:Listen not implemented") + if err := lc.checkListenConfig(ctx, true); err != nil { + return nil, &OpError{Op: "listen", Net: network, Err: err} + } + laddr, err := ResolveTCPAddr(network, address) + if err != nil { + return nil, err + } + if err := ctx.Err(); err != nil { + return nil, &OpError{Op: "listen", Net: network, Addr: laddr, Err: err} + } + return listenTCP(laddr) } // ListenPacket announces on the local network address. @@ -233,8 +245,35 @@ func (lc *ListenConfig) Listen(ctx context.Context, network, address string) (Li // // The ctx argument is used while resolving the address on which to listen; // it does not affect the returned PacketConn. +// TINYGO: netdev.go has no lookup cancellation or socket control hook. +// Context is checked before and after lookup. Default socket options apply. func (lc *ListenConfig) ListenPacket(ctx context.Context, network, address string) (PacketConn, error) { - return nil, errors.New("dial:ListenConfig:ListenPacket not implemented") + if err := lc.checkListenConfig(ctx, false); err != nil { + return nil, &OpError{Op: "listen", Net: network, Err: err} + } + switch network { + case "udp", "udp4": + default: + return nil, fmt.Errorf("Network %s not supported", network) + } + laddr, err := ResolveUDPAddr(network, address) + if err != nil { + return nil, err + } + if err := ctx.Err(); err != nil { + return nil, &OpError{Op: "listen", Net: network, Addr: laddr, Err: err} + } + return ListenUDP(network, laddr) +} + +func (lc *ListenConfig) checkListenConfig(ctx context.Context, stream bool) error { + if err := ctx.Err(); err != nil { + return err + } + if lc.Control != nil || stream && (lc.KeepAlive != 0 || lc.KeepAliveConfig.Enable) { + return fmt.Errorf("net: ListenConfig socket options: %w", errors.ErrUnsupported) + } + return nil } func parseNetwork(ctx context.Context, network string, needsProto bool) (afnet string, proto int, err error) { diff --git a/listenconfig_native_test.go b/listenconfig_native_test.go new file mode 100644 index 0000000..87e9773 --- /dev/null +++ b/listenconfig_native_test.go @@ -0,0 +1,69 @@ +//go:build linux && !baremetal && !tinygo.wasm + +package net + +import ( + "context" + "syscall" + "testing" + "time" +) + +func TestListenConfigTCPExchange(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + l, err := (&ListenConfig{}).Listen(ctx, "tcp4", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer l.Close() + cancel() + sa, err := syscall.Getsockname(l.(*listener).fd) + if err != nil { + t.Fatal(err) + } + fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, 0) + if err != nil { + t.Fatal(err) + } + defer syscall.Close(fd) + if err := syscall.Connect(fd, sa); err != nil { + t.Fatal(err) + } + c, err := l.Accept() + if err != nil { + t.Fatal(err) + } + defer c.Close() + c.SetReadDeadline(time.Now().Add(time.Second)) + if _, err := syscall.Write(fd, []byte("x")); err != nil { + t.Fatal(err) + } + b := make([]byte, 1) + if n, err := c.Read(b); n != 1 || err != nil || b[0] != 'x' { + t.Fatalf("read=%d %q error=%v", n, b, err) + } +} + +func TestListenConfigUDPReceive(t *testing.T) { + c, err := (&ListenConfig{}).ListenPacket(context.Background(), "udp4", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer c.Close() + c.SetReadDeadline(time.Now().Add(time.Second)) + fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, 0) + if err != nil { + t.Fatal(err) + } + defer syscall.Close(fd) + addr := c.LocalAddr().(*UDPAddr) + sa := &syscall.SockaddrInet4{Port: addr.Port, Addr: [4]byte{127, 0, 0, 1}} + if err := syscall.Sendto(fd, []byte("x"), 0, sa); err != nil { + t.Fatal(err) + } + b := make([]byte, 1) + if n, err := c.(*UDPConn).Read(b); n != 1 || err != nil || b[0] != 'x' { + t.Fatalf("read=%d %q error=%v", n, b, err) + } +} diff --git a/listenconfig_test.go b/listenconfig_test.go new file mode 100644 index 0000000..55c3d03 --- /dev/null +++ b/listenconfig_test.go @@ -0,0 +1,138 @@ +package net + +import ( + "context" + "errors" + "net/netip" + "syscall" + "testing" + "time" +) + +type listenConfigNetdev struct { + nopNetdev + lookup func() + sockets int + bound netip.AddrPort + listens int + closed int +} + +func (d *listenConfigNetdev) GetHostByName(string) (netip.Addr, error) { + if d.lookup != nil { + d.lookup() + } + return netip.MustParseAddr("127.0.0.1"), nil +} +func (d *listenConfigNetdev) Socket(int, int, int) (int, error) { + d.sockets++ + return 42, nil +} +func (d *listenConfigNetdev) Bind(_ int, addr netip.AddrPort) error { + d.bound = addr + return nil +} +func (d *listenConfigNetdev) Listen(int, int) error { + d.listens++ + return nil +} +func (d *listenConfigNetdev) Close(int) error { + d.closed++ + return nil +} + +func testListenConfigCall(lc *ListenConfig, ctx context.Context, network, address string) error { + if network == "udp4" { + c, err := lc.ListenPacket(ctx, network, address) + if err != nil { + return err + } + return c.Close() + } + l, err := lc.Listen(ctx, network, address) + if err != nil { + return err + } + return l.Close() +} + +func TestListenConfigNetdev(t *testing.T) { + previous := netdev + defer func() { netdev = previous }() + for _, network := range []string{"tcp4", "udp4"} { + t.Run(network, func(t *testing.T) { + d := &listenConfigNetdev{} + netdev = d + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + if err := testListenConfigCall(&ListenConfig{}, ctx, network, "test.invalid:12345"); err != nil { + t.Fatal(err) + } + if d.sockets != 1 || d.bound.String() != "127.0.0.1:12345" || d.closed != 1 { + t.Fatalf("socket=%d bound=%v close=%d", d.sockets, d.bound, d.closed) + } + wantListen := 0 + if network == "tcp4" { + wantListen = 1 + } + if d.listens != wantListen { + t.Fatalf("listen=%d want %d", d.listens, wantListen) + } + }) + } +} + +func TestListenConfigCancellation(t *testing.T) { + previous := netdev + defer func() { netdev = previous }() + for _, network := range []string{"tcp4", "udp4"} { + for _, duringLookup := range []bool{false, true} { + d := &listenConfigNetdev{} + netdev = d + ctx, cancel := context.WithCancel(context.Background()) + if duringLookup { + d.lookup = cancel + } else { + cancel() + } + err := testListenConfigCall(&ListenConfig{}, ctx, network, "test.invalid:12345") + cancel() + if !errors.Is(err, context.Canceled) { + t.Fatalf("%s duringLookup=%v error=%v", network, duringLookup, err) + } + if d.sockets != 0 { + t.Fatalf("%s opened socket after cancellation", network) + } + } + ctx, cancel := context.WithDeadline(context.Background(), time.Unix(1, 0)) + err := testListenConfigCall(&ListenConfig{}, ctx, network, "test.invalid:12345") + cancel() + if !errors.Is(err, context.DeadlineExceeded) { + t.Fatalf("%s error=%v", network, err) + } + } +} + +func TestListenConfigUnsupportedOptions(t *testing.T) { + previous := netdev + defer func() { netdev = previous }() + called := false + control := func(string, string, syscall.RawConn) error { called = true; return nil } + for _, network := range []string{"tcp4", "udp4"} { + d := &listenConfigNetdev{} + netdev = d + err := testListenConfigCall(&ListenConfig{Control: control}, context.Background(), network, "test.invalid:12345") + if !errors.Is(err, errors.ErrUnsupported) || called || d.sockets != 0 { + t.Fatalf("%s error=%v called=%v sockets=%d", network, err, called, d.sockets) + } + } + for _, lc := range []ListenConfig{{KeepAlive: time.Second}, {KeepAlive: -1}, {KeepAliveConfig: KeepAliveConfig{Enable: true}}} { + err := testListenConfigCall(&lc, context.Background(), "tcp4", "test.invalid:12345") + if !errors.Is(err, errors.ErrUnsupported) { + t.Fatalf("error=%v", err) + } + } + if err := testListenConfigCall(&ListenConfig{KeepAlive: time.Second}, context.Background(), "udp4", "test.invalid:12345"); err != nil { + t.Fatal(err) + } +} diff --git a/tools/test-listenconfig.sh b/tools/test-listenconfig.sh new file mode 100755 index 0000000..94cd9ea --- /dev/null +++ b/tools/test-listenconfig.sh @@ -0,0 +1,17 @@ +#!/bin/sh +set -eu + +tinygo_bin=${TINYGO_BIN:-tinygo} +release_root=$("$tinygo_bin" env TINYGOROOT) +net_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +test_root=$(mktemp -d "${TMPDIR:-/tmp}/net-listenconfig.XXXXXX") + +tar -C "$release_root" --exclude='./src/net' -cf - ./src | tar -C "$test_root" -xf - +mkdir "$test_root/src/net" +tar -C "$net_root" --exclude='./.git' -cf - . | tar -C "$test_root/src/net" -xf - +ln -s "$release_root/lib" "$test_root/lib" +ln -s "$release_root/targets" "$test_root/targets" + +TINYGOROOT="$test_root" "$tinygo_bin" test -c -o "$test_root/listenconfig.test" net +printf 'Test binary: %s\n' "$test_root/listenconfig.test" +printf 'Run on the target OS with an external timeout and -test.run TestListenConfig\n'