Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
43 changes: 41 additions & 2 deletions dial.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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) {
Expand Down
69 changes: 69 additions & 0 deletions listenconfig_native_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
138 changes: 138 additions & 0 deletions listenconfig_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
17 changes: 17 additions & 0 deletions tools/test-listenconfig.sh
Original file line number Diff line number Diff line change
@@ -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'