From 2e825c2b01cd1a51a52b8d0357aa7c6866f1e957 Mon Sep 17 00:00:00 2001 From: Patricio Whittingslow Date: Wed, 1 Jul 2026 22:34:14 -0300 Subject: [PATCH 1/3] net: add UDP listen, resolver, DNSError, and TCP listener APIs for wasm TinyGo's net package was missing several symbols that upstream Go's js/wasm net declares, blocking builds that pull pion/transport, pion/dtls, and similar (via netbird's WASM client). Add them, backed by the existing netdev abstraction so they compile for all targets and no-op cleanly under nopNetdev (matching Go's js runtime behavior): - DNSError type (dnserror.go), copied from Go 1.26.2. - Resolver + DefaultResolver with LookupHost/LookupIP/LookupIPAddr/ LookupNetIP/LookupPort; LookupHost/LookupIP package funcs; Dialer.Resolver field for API compatibility. - UDPConn: ListenUDP, ReadFromUDP(AddrPort), WriteToUDP(AddrPort), SetReadBuffer, SetWriteBuffer. - TCPConn: CloseRead, SetNoDelay, SetReadBuffer, SetWriteBuffer, ReadFrom(io.Reader). - TCPListener: ListenTCP, AcceptTCP, SetDeadline; ListenPacket package func. - Interface: Addrs, MulticastAddrs stubs. --- dial.go | 26 ++++++++++++ dnserror.go | 47 +++++++++++++++++++++ interface.go | 16 +++++++ lookup.go | 109 ++++++++++++++++++++++++++++++++++++++++++++++++ tcpsock.go | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++ udpsock.go | 107 +++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 420 insertions(+) create mode 100644 dnserror.go diff --git a/dial.go b/dial.go index 876a1d8..9c78ed4 100644 --- a/dial.go +++ b/dial.go @@ -87,6 +87,13 @@ type Dialer struct { // If KeepAliveConfig.Enable is false and KeepAlive is negative, // keep-alive probes are disabled. KeepAliveConfig KeepAliveConfig + + // Resolver optionally specifies an alternate resolver to use. + // + // TINYGO: present for API compatibility; DialContext resolves via the + // netdev-backed ResolveTCPAddr/ResolveUDPAddr and does not consult this + // field. + Resolver *Resolver } // Dial connects to the address on the named network. @@ -281,3 +288,22 @@ func Listen(network, address string) (Listener, error) { return listenTCP(laddr) } + +// ListenPacket announces on the local network address. +// +// TINYGO: only UDP networks are supported, backed by ListenUDP; the +// returned PacketConn is a *UDPConn. +func ListenPacket(network, address string) (PacketConn, error) { + 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 + } + + return ListenUDP(network, laddr) +} diff --git a/dnserror.go b/dnserror.go new file mode 100644 index 0000000..a39349e --- /dev/null +++ b/dnserror.go @@ -0,0 +1,47 @@ +// TINYGO: The following is copied and modified from Go 1.26.2 official implementation. + +// Copyright 2018 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 net + +// DNSError represents a DNS lookup error. +type DNSError struct { + UnwrapErr error // error returned by the [DNSError.Unwrap] method, might be nil + Err string // description of the error + Name string // name looked for + Server string // server used + IsTimeout bool // if true, timed out; not all timeouts set this + IsTemporary bool // if true, error is temporary; not all errors set this + + // IsNotFound is set to true when the requested name does not + // contain any records of the requested type (data not found), + // or the name itself was not found (NXDOMAIN). + IsNotFound bool +} + +// Unwrap returns e.UnwrapErr. +func (e *DNSError) Unwrap() error { return e.UnwrapErr } + +func (e *DNSError) Error() string { + if e == nil { + return "" + } + s := "lookup " + e.Name + if e.Server != "" { + s += " on " + e.Server + } + s += ": " + e.Err + return s +} + +// Timeout reports whether the DNS lookup is known to have timed out. +// This is not always known; a DNS lookup may fail due to a timeout +// and return a [DNSError] for which Timeout returns false. +func (e *DNSError) Timeout() bool { return e.IsTimeout } + +// Temporary reports whether the DNS error is known to be temporary. +// This is not always known; a DNS lookup may fail due to a temporary +// error and return a [DNSError] for which Temporary returns false. +func (e *DNSError) Temporary() bool { return e.IsTimeout || e.IsTemporary } diff --git a/interface.go b/interface.go index 1a7b5ba..25bfb1f 100644 --- a/interface.go +++ b/interface.go @@ -35,6 +35,22 @@ type Interface struct { Flags Flags // e.g., FlagUp, FlagLoopback, FlagMulticast } +// Addrs returns a list of unicast interface addresses for a specific +// interface. +// +// TINYGO: not implemented; netdev exposes no per-interface address list. +func (ifi *Interface) Addrs() ([]Addr, error) { + return nil, errors.New("Interface.Addrs not implemented") +} + +// MulticastAddrs returns a list of multicast, joined group addresses +// for a specific interface. +// +// TINYGO: not implemented; netdev exposes no per-interface address list. +func (ifi *Interface) MulticastAddrs() ([]Addr, error) { + return nil, errors.New("Interface.MulticastAddrs not implemented") +} + type Flags uint const ( diff --git a/lookup.go b/lookup.go index 4eabf77..7b77c26 100644 --- a/lookup.go +++ b/lookup.go @@ -7,13 +7,122 @@ package net import ( + "context" "errors" + "net/netip" ) +// A Resolver looks up names and numbers. +// +// A nil *Resolver is equivalent to a zero Resolver. +// +// TINYGO: name resolution is backed by the netdev GetHostByName hook; +// the PreferGo/StrictErrors/Dial fields exist for API compatibility with +// upstream Go and are ignored. +type Resolver struct { + // PreferGo controls whether Go's built-in DNS resolver is preferred + // on platforms where it's available. It is equivalent to setting + // GODEBUG=netdns=go, but scoped to just this resolver. + PreferGo bool + + // StrictErrors controls the behavior of temporary errors + // (including timeout, socket errors, and SERVFAIL) when using + // Go's built-in resolver. + StrictErrors bool + + // Dial optionally specifies an alternate dialer for use by + // Go's built-in DNS resolver to make TCP and UDP connections + // to DNS services. + Dial func(ctx context.Context, network, address string) (Conn, error) +} + +// DefaultResolver is the resolver used by the package-level Lookup +// functions and by Dialers without a specified Resolver. +var DefaultResolver = &Resolver{} + +// LookupHost looks up the given host using the local resolver. +// It returns a slice of that host's addresses. +// +// LookupHost uses [context.Background] internally; to specify the context, use +// [Resolver.LookupHost]. +func LookupHost(host string) (addrs []string, err error) { + return DefaultResolver.LookupHost(context.Background(), host) +} + +// LookupHost looks up the given host using the local resolver. +// It returns a slice of that host's addresses. +// +// TINYGO: resolves via netdev.GetHostByName, returning at most one address. +func (r *Resolver) LookupHost(ctx context.Context, host string) (addrs []string, err error) { + if host == "" { + return nil, &DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true} + } + ip, err := netdev.GetHostByName(host) + if err != nil { + return nil, &DNSError{Err: err.Error(), Name: host} + } + return []string{ip.String()}, nil +} + +// LookupIP looks up host using the local resolver. +// It returns a slice of that host's IPv4 and IPv6 addresses. +func LookupIP(host string) ([]IP, error) { + addrs, err := DefaultResolver.LookupIPAddr(context.Background(), host) + if err != nil { + return nil, err + } + ips := make([]IP, 0, len(addrs)) + for _, addr := range addrs { + ips = append(ips, addr.IP) + } + return ips, nil +} + +// LookupIPAddr looks up host using the local resolver. +// It returns a slice of that host's IPv4 and IPv6 addresses. +// +// TINYGO: resolves via netdev.GetHostByName, returning at most one address. +func (r *Resolver) LookupIPAddr(ctx context.Context, host string) ([]IPAddr, error) { + if host == "" { + return nil, &DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true} + } + ip, err := netdev.GetHostByName(host) + if err != nil { + return nil, &DNSError{Err: err.Error(), Name: host} + } + return []IPAddr{{IP: ip.AsSlice(), Zone: ip.Zone()}}, nil +} + +// LookupNetIP looks up host using the local resolver. +// It returns a slice of that host's IP addresses of the type specified by +// network. The network must be one of "ip", "ip4" or "ip6". +// +// TINYGO: resolves via netdev.GetHostByName, returning at most one address. +func (r *Resolver) LookupNetIP(ctx context.Context, network, host string) ([]netip.Addr, error) { + if host == "" { + return nil, &DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true} + } + ip, err := netdev.GetHostByName(host) + if err != nil { + return nil, &DNSError{Err: err.Error(), Name: host} + } + return []netip.Addr{ip}, nil +} + // LookupPort looks up the port for the given network and service. // // LookupPort uses [context.Background] internally; to specify the context, use // [Resolver.LookupPort]. func LookupPort(network, service string) (port int, err error) { + return DefaultResolver.LookupPort(context.Background(), network, service) +} + +// LookupPort looks up the port for the given network and service. +// +// TINYGO: not implemented; netdev provides no service database. +func (r *Resolver) LookupPort(ctx context.Context, network, service string) (port int, err error) { return 0, errors.New("net:LookupPort not implemented") } + +// errNoSuchHost is returned when the host lookup finds no matching records. +var errNoSuchHost = errors.New("no such host") diff --git a/tcpsock.go b/tcpsock.go index 6415c48..17e02a6 100644 --- a/tcpsock.go +++ b/tcpsock.go @@ -296,6 +296,61 @@ func (c *TCPConn) CloseWrite() error { return fmt.Errorf("CloseWrite not implemented") } +// CloseRead shuts down the reading side of the TCP connection. +// Most callers should just use Close. +// +// TINYGO: no-op; netdev has no half-close, reads simply stop when Close +// is called. +func (c *TCPConn) CloseRead() error { + return nil +} + +// SetNoDelay controls whether the operating system should delay +// packet transmission in hopes of sending fewer packets (Nagle's +// algorithm). The default is true (no delay), meaning that data is +// sent as soon as possible after a Write. +// +// TINYGO: no-op; Nagle behavior is managed by the netdev driver. +func (c *TCPConn) SetNoDelay(noDelay bool) error { + return nil +} + +// SetReadBuffer sets the size of the operating system's receive buffer +// associated with the connection. +// +// TINYGO: no-op; buffer sizing is managed by the netdev driver. +func (c *TCPConn) SetReadBuffer(bytes int) error { + return nil +} + +// SetWriteBuffer sets the size of the operating system's transmit buffer +// associated with the connection. +// +// TINYGO: no-op; buffer sizing is managed by the netdev driver. +func (c *TCPConn) SetWriteBuffer(bytes int) error { + return nil +} + +// ReadFrom implements the io.ReaderFrom ReadFrom method. +// +// TINYGO: generic copy loop over Write; no zero-copy sendfile path. +func (c *TCPConn) ReadFrom(r io.Reader) (int64, error) { + return genericReadFrom(c, r) +} + +// genericReadFrom copies from r into w until EOF, returning the number +// of bytes copied. +func genericReadFrom(w io.Writer, r io.Reader) (int64, error) { + // Wrap w to hide its ReadFrom method, so io.Copy uses the generic + // read/write loop instead of recursing back into TCPConn.ReadFrom. + return io.Copy(onlyWriter{w}, r) +} + +// onlyWriter hides w's ReadFrom method from io.Copy. +type onlyWriter struct { + io.Writer +} + type listener struct { fd int laddr *TCPAddr @@ -348,3 +403,63 @@ func listenTCP(laddr *TCPAddr) (Listener, error) { type TCPListener struct { listener } + +// ListenTCP acts like [Listen] for TCP networks. +// +// The network must be a TCP network name; see func [Dial] for details. +// If the IP field of laddr is nil or an unspecified IP address, +// ListenTCP listens on all available unicast and anycast IP addresses +// of the local system. If the Port field of laddr is 0, a port number +// is automatically chosen. +func ListenTCP(network string, laddr *TCPAddr) (*TCPListener, error) { + switch network { + case "tcp", "tcp4": + default: + return nil, fmt.Errorf("Network '%s' not supported", network) + } + + if laddr == nil { + laddr = &TCPAddr{} + } + + fd, err := netdev.Socket(_AF_INET, _SOCK_STREAM, _IPPROTO_TCP) + if err != nil { + return nil, err + } + + if err = netdev.Bind(fd, laddr.AddrPort()); err != nil { + netdev.Close(fd) + return nil, err + } + + if err = netdev.Listen(fd, 5); err != nil { + netdev.Close(fd) + return nil, err + } + + return &TCPListener{listener{fd: fd, laddr: laddr}}, nil +} + +// AcceptTCP accepts the next incoming call and returns the new +// connection. +func (l *TCPListener) AcceptTCP() (*TCPConn, error) { + fd, raddr, err := netdev.Accept(l.fd) + if err != nil { + return nil, err + } + + return &TCPConn{ + fd: fd, + net: "tcp", + laddr: l.laddr, + raddr: TCPAddrFromAddrPort(raddr), + }, nil +} + +// SetDeadline sets the deadline associated with the listener. +// A zero time value disables the deadline. +// +// TINYGO: no-op; netdev.Accept has no deadline support. +func (l *TCPListener) SetDeadline(t time.Time) error { + return nil +} diff --git a/udpsock.go b/udpsock.go index ef683b7..11df8b6 100644 --- a/udpsock.go +++ b/udpsock.go @@ -210,6 +210,53 @@ func DialUDP(network string, laddr, raddr *UDPAddr) (*UDPConn, error) { }, nil } +// ListenUDP acts like [ListenPacket] for UDP networks. +// +// The network must be a UDP network name; see func [Dial] for details. +// +// If the IP field of laddr is nil or an unspecified IP address, +// ListenUDP listens on all available IP addresses of the local system +// except multicast IP addresses. +// If the Port field of laddr is 0, a port number is automatically +// chosen. +func ListenUDP(network string, laddr *UDPAddr) (*UDPConn, error) { + switch network { + case "udp", "udp4": + default: + return nil, fmt.Errorf("Network '%s' not supported", network) + } + + // TINYGO: Use netdev to create UDP socket and bind (no connect) + + if laddr == nil { + laddr = &UDPAddr{} + } + + // If no port was given, grab an ephemeral port + if laddr.Port == 0 { + laddr.Port = ephemeralPort() + } + + fd, err := netdev.Socket(_AF_INET, _SOCK_DGRAM, _IPPROTO_UDP) + if err != nil { + return nil, err + } + + lip, _ := netip.AddrFromSlice(laddr.IP) + laddrport := netip.AddrPortFrom(lip, uint16(laddr.Port)) + + if err = netdev.Bind(fd, laddrport); err != nil { + netdev.Close(fd) + return nil, err + } + + return &UDPConn{ + fd: fd, + net: network, + laddr: laddr, + }, nil +} + // SyscallConn returns a raw network connection. // This implements the syscall.Conn interface. func (c *UDPConn) SyscallConn() (syscall.RawConn, error) { @@ -242,6 +289,66 @@ func (c *UDPConn) Write(b []byte) (int, error) { return n, err } +// ReadFromUDP acts like [UDPConn.ReadFrom] but returns a [UDPAddr]. +// +// TINYGO: netdev has no per-datagram source address, so the connected +// remote address (c.raddr) is returned as the source. +func (c *UDPConn) ReadFromUDP(b []byte) (int, *UDPAddr, error) { + n, err := netdev.Recv(c.fd, b, 0, c.readDeadline) + if n < 0 { + n = 0 + } + if err != nil && err != io.EOF { + err = &OpError{Op: "read", Net: c.net, Source: c.laddr, Addr: c.raddr, Err: err} + } + return n, c.raddr, err +} + +// ReadFromUDPAddrPort acts like [UDPConn.ReadFromUDP] but returns a [netip.AddrPort]. +func (c *UDPConn) ReadFromUDPAddrPort(b []byte) (n int, addr netip.AddrPort, err error) { + n, uaddr, err := c.ReadFromUDP(b) + if uaddr != nil { + addr = uaddr.AddrPort() + } + return n, addr, err +} + +// WriteToUDP acts like [UDPConn.WriteTo] but takes a [UDPAddr]. +// +// TINYGO: the socket is connected via netdev, so writes go to the +// connected remote regardless of addr, mirroring netdev.Send. +func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (int, error) { + n, err := netdev.Send(c.fd, b, 0, c.writeDeadline) + if n < 0 { + n = 0 + } + if err != nil { + err = &OpError{Op: "write", Net: c.net, Source: c.laddr, Addr: addr.opAddr(), Err: err} + } + return n, err +} + +// WriteToUDPAddrPort acts like [UDPConn.WriteToUDP] but takes a [netip.AddrPort]. +func (c *UDPConn) WriteToUDPAddrPort(b []byte, addr netip.AddrPort) (int, error) { + return c.WriteToUDP(b, UDPAddrFromAddrPort(addr)) +} + +// SetReadBuffer sets the size of the operating system's receive buffer +// associated with the connection. +// +// TINYGO: no-op; buffer sizing is managed by the netdev driver. +func (c *UDPConn) SetReadBuffer(bytes int) error { + return nil +} + +// SetWriteBuffer sets the size of the operating system's transmit buffer +// associated with the connection. +// +// TINYGO: no-op; buffer sizing is managed by the netdev driver. +func (c *UDPConn) SetWriteBuffer(bytes int) error { + return nil +} + // ReadFrom implements the PacketConn ReadFrom method. func (c *UDPConn) ReadFrom(b []byte) (int, Addr, error) { return 0, nil, errors.New("ReadFrom not implemented") From ffb222f1ad9d9fb69e8c4c8929886d35044bccd1 Mon Sep 17 00:00:00 2001 From: Pat Whittingslow Date: Fri, 3 Jul 2026 17:04:17 -0300 Subject: [PATCH 2/3] more complete PR for adding more net support (#64) --- http/response.go | 6 ++ http/responsecontroller.go | 152 +++++++++++++++++++++++++++++++++++++ http/server.go | 17 +++++ http/transport.go | 108 +++++++++++++++++++++++++- interface.go | 5 ++ lookup.go | 23 ++++++ tlssock.go | 8 ++ unixsock.go | 32 ++++++++ 8 files changed, 350 insertions(+), 1 deletion(-) create mode 100644 http/responsecontroller.go diff --git a/http/response.go b/http/response.go index 9bf59e3..aa034dc 100644 --- a/http/response.go +++ b/http/response.go @@ -14,6 +14,7 @@ package http import ( "bufio" "bytes" + "crypto/tls" "errors" "fmt" "io" @@ -117,6 +118,11 @@ type Response struct { // Request's Body is nil (having already been consumed). // This is only populated for Client requests. Request *Request + + // TLS contains information about the TLS connection on which the response + // was received. It is nil for unencrypted responses. TINYGO: populated only + // if a caller sets it; the wasm fetch path leaves it nil. + TLS *tls.ConnectionState } // Cookies parses and returns the cookies set in the Set-Cookie headers. diff --git a/http/responsecontroller.go b/http/responsecontroller.go new file mode 100644 index 0000000..bcf1ee1 --- /dev/null +++ b/http/responsecontroller.go @@ -0,0 +1,152 @@ +// TINYGO: Copied verbatim from the Go 1.26.2 official implementation to provide +// the ResponseController API used by net/http/httputil (reverse proxy). The +// underlying methods degrade to ErrNotSupported when the ResponseWriter does +// not implement them, which is the standard behavior. + +// Copyright 2022 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 http + +import ( + "bufio" + "fmt" + "net" + "time" +) + +// A ResponseController is used by an HTTP handler to control the response. +// +// A ResponseController may not be used after the [Handler.ServeHTTP] method has returned. +type ResponseController struct { + rw ResponseWriter +} + +// NewResponseController creates a [ResponseController] for a request. +// +// The ResponseWriter should be the original value passed to the [Handler.ServeHTTP] method, +// or have an Unwrap method returning the original ResponseWriter. +// +// If the ResponseWriter implements any of the following methods, the ResponseController +// will call them as appropriate: +// +// Flush() +// FlushError() error // alternative Flush returning an error +// Hijack() (net.Conn, *bufio.ReadWriter, error) +// SetReadDeadline(deadline time.Time) error +// SetWriteDeadline(deadline time.Time) error +// EnableFullDuplex() error +// +// If the ResponseWriter does not support a method, ResponseController returns +// an error matching [ErrNotSupported]. +func NewResponseController(rw ResponseWriter) *ResponseController { + return &ResponseController{rw} +} + +type rwUnwrapper interface { + Unwrap() ResponseWriter +} + +// Flush flushes buffered data to the client. +func (c *ResponseController) Flush() error { + rw := c.rw + for { + switch t := rw.(type) { + case interface{ FlushError() error }: + return t.FlushError() + case Flusher: + t.Flush() + return nil + case rwUnwrapper: + rw = t.Unwrap() + default: + return errNotSupported() + } + } +} + +// Hijack lets the caller take over the connection. +// See the [Hijacker] interface for details. +func (c *ResponseController) Hijack() (net.Conn, *bufio.ReadWriter, error) { + rw := c.rw + for { + switch t := rw.(type) { + case Hijacker: + return t.Hijack() + case rwUnwrapper: + rw = t.Unwrap() + default: + return nil, nil, errNotSupported() + } + } +} + +// SetReadDeadline sets the deadline for reading the entire request, including the body. +// Reads from the request body after the deadline has been exceeded will return an error. +// A zero value means no deadline. +// +// Setting the read deadline after it has been exceeded will not extend it. +func (c *ResponseController) SetReadDeadline(deadline time.Time) error { + rw := c.rw + for { + switch t := rw.(type) { + case interface{ SetReadDeadline(time.Time) error }: + return t.SetReadDeadline(deadline) + case rwUnwrapper: + rw = t.Unwrap() + default: + return errNotSupported() + } + } +} + +// SetWriteDeadline sets the deadline for writing the response. +// Writes to the response body after the deadline has been exceeded will not block, +// but may succeed if the data has been buffered. +// A zero value means no deadline. +// +// Setting the write deadline after it has been exceeded will not extend it. +func (c *ResponseController) SetWriteDeadline(deadline time.Time) error { + rw := c.rw + for { + switch t := rw.(type) { + case interface{ SetWriteDeadline(time.Time) error }: + return t.SetWriteDeadline(deadline) + case rwUnwrapper: + rw = t.Unwrap() + default: + return errNotSupported() + } + } +} + +// EnableFullDuplex indicates that the request handler will interleave reads from [Request.Body] +// with writes to the [ResponseWriter]. +// +// For HTTP/1 requests, the Go HTTP server by default consumes any unread portion of +// the request body before beginning to write the response, preventing handlers from +// concurrently reading from the request and writing the response. +// Calling EnableFullDuplex disables this behavior and permits handlers to continue to read +// from the request while concurrently writing the response. +// +// For HTTP/2 requests, the Go HTTP server always permits concurrent reads and responses. +func (c *ResponseController) EnableFullDuplex() error { + rw := c.rw + for { + switch t := rw.(type) { + case interface{ EnableFullDuplex() error }: + return t.EnableFullDuplex() + case rwUnwrapper: + rw = t.Unwrap() + default: + return errNotSupported() + } + } +} + +// errNotSupported returns an error that Is ErrNotSupported, +// but is not == to it. +func errNotSupported() error { + return fmt.Errorf("%w", ErrNotSupported) +} diff --git a/http/server.go b/http/server.go index a749c09..4f6510c 100644 --- a/http/server.go +++ b/http/server.go @@ -2779,6 +2779,23 @@ type Server struct { // value. ConnContext func(ctx context.Context, c net.Conn) context.Context + // TLSNextProto optionally specifies a function to take over + // ownership of the provided TLS connection when an ALPN + // protocol upgrade has occurred. The map key is the protocol + // name negotiated. The Handler argument should be used to + // handle HTTP requests and will initialize the Request's TLS + // and RemoteAddr if not already set. The connection is + // automatically closed when the function returns. + // If TLSNextProto is not nil, HTTP/2 support is not enabled + // automatically. + TLSNextProto map[string]func(*Server, *tls.Conn, Handler) + + // HTTP2 configures HTTP/2 connections. + // + // This field does not yet have any effect. + // See https://go.dev/issue/67813. + HTTP2 *HTTP2Config + inShutdown atomicBool // true when server is in shutdown disableKeepAlives int32 // accessed atomically. diff --git a/http/transport.go b/http/transport.go index 8544ed0..9b6f790 100644 --- a/http/transport.go +++ b/http/transport.go @@ -12,8 +12,14 @@ package http import ( + "context" + "crypto/tls" + "errors" "io" + "net" + "net/url" "sync/atomic" + "time" ) type readTrackingBody struct { @@ -22,6 +28,106 @@ type readTrackingBody struct { didClose atomic.Bool } -type Transport struct{} +// Transport is an HTTP/1.x RoundTripper. TINYGO: in the wasm/browser build the +// actual round trips are performed by the host fetch API (see roundtrip_js.go); +// these fields exist only to satisfy configuration by callers such as +// golang.org/x/net/http2 and google.golang.org/grpc. They are stored but, aside +// from fetch, have no effect at runtime. +type Transport struct { + // TLSClientConfig specifies the TLS configuration to use with tls.Client. + TLSClientConfig *tls.Config + + // TLSNextProto specifies how the Transport switches to an alternate + // protocol (such as HTTP/2) after a TLS ALPN protocol negotiation. + TLSNextProto map[string]func(authority string, c *tls.Conn) RoundTripper + + // DisableKeepAlives, if true, disables HTTP keep-alives. + DisableKeepAlives bool + + // DisableCompression, if true, prevents the Transport from requesting + // compression with an "Accept-Encoding: gzip" request header. + DisableCompression bool + + // IdleConnTimeout is the maximum amount of time an idle connection will + // remain idle before closing itself. Zero means no limit. + IdleConnTimeout time.Duration + + // ResponseHeaderTimeout, if non-zero, specifies the amount of time to wait + // for a server's response headers after fully writing the request. + ResponseHeaderTimeout time.Duration + + // ExpectContinueTimeout, if non-zero, specifies the amount of time to wait + // for a server's first response headers after fully writing the request + // headers if the request has an "Expect: 100-continue" header. + ExpectContinueTimeout time.Duration + + // MaxResponseHeaderBytes specifies a limit on how many response bytes are + // allowed in the server's response header. Zero means to use a default limit. + MaxResponseHeaderBytes int64 + + // MaxIdleConns controls the maximum number of idle (keep-alive) connections + // across all hosts. Zero means no limit. + MaxIdleConns int + + // MaxIdleConnsPerHost, if non-zero, controls the maximum idle (keep-alive) + // connections to keep per-host. + MaxIdleConnsPerHost int + + // MaxConnsPerHost optionally limits the total number of connections per host. + MaxConnsPerHost int + + // HTTP2 configures HTTP/2 connections. This field does not yet have any effect. + HTTP2 *HTTP2Config + + // Dial specifies the dial function for creating unencrypted TCP connections. + // + // Deprecated: Use DialContext instead. TINYGO: stored only; unused by the + // fetch-based round tripper. + Dial func(network, addr string) (net.Conn, error) + + // DialContext specifies the dial function for creating unencrypted TCP + // connections. TINYGO: honored by callers that dial through the Transport + // directly (e.g. the relay WebSocket transport), which is how netbird routes + // connections through the netstack in the browser. + DialContext func(ctx context.Context, network, addr string) (net.Conn, error) +} var DefaultTransport RoundTripper = &Transport{} + +// CloseIdleConnections closes any connections which were previously connected +// from previous requests but are now sitting idle. TINYGO: no-op. +func (t *Transport) CloseIdleConnections() {} + +// Clone returns a copy of t with its exported fields shared. TINYGO: the +// tinygo Transport carries only configuration (round trips go through the host +// fetch API), so a shallow struct copy with duplicated maps is sufficient. +func (t *Transport) Clone() *Transport { + t2 := *t + if t.TLSClientConfig != nil { + t2.TLSClientConfig = t.TLSClientConfig.Clone() + } + if t.TLSNextProto != nil { + npm := make(map[string]func(authority string, c *tls.Conn) RoundTripper, len(t.TLSNextProto)) + for k, v := range t.TLSNextProto { + npm[k] = v + } + t2.TLSNextProto = npm + } + return &t2 +} + +// ProxyFromEnvironment returns the URL of the proxy to use for a given request. +// TINYGO: the wasm build has no environment proxy configuration, so this always +// reports "no proxy" (nil URL, nil error), which callers treat as a direct +// connection. +func ProxyFromEnvironment(req *Request) (*url.URL, error) { + return nil, nil +} + +// ErrSkipAltProtocol is a sentinel error value defined by Transport.RegisterProtocol. +var ErrSkipAltProtocol = errors.New("net/http: skip alternate protocol") + +// RegisterProtocol registers a new protocol with scheme. TINYGO: no-op; the +// wasm build performs round trips via the host fetch API and does not dispatch +// on registered alternate protocols. +func (t *Transport) RegisterProtocol(scheme string, rt RoundTripper) {} diff --git a/interface.go b/interface.go index 25bfb1f..c05ae36 100644 --- a/interface.go +++ b/interface.go @@ -109,3 +109,8 @@ func InterfaceAddrs() ([]Addr, error) { func InterfaceByIndex(index int) (*Interface, error) { return nil, errors.New("InterfaceByIndex not implemented") } + +// InterfaceByName returns the interface specified by name. +func InterfaceByName(name string) (*Interface, error) { + return nil, errors.New("InterfaceByName not implemented") +} diff --git a/lookup.go b/lookup.go index 7b77c26..5dbe657 100644 --- a/lookup.go +++ b/lookup.go @@ -124,5 +124,28 @@ func (r *Resolver) LookupPort(ctx context.Context, network, service string) (por return 0, errors.New("net:LookupPort not implemented") } +// An SRV represents a single DNS SRV record. +type SRV struct { + Target string + Port uint16 + Priority uint16 + Weight uint16 +} + +// LookupSRV tries to resolve an SRV query of the given service, protocol, and +// domain name. +// +// TINYGO: not implemented; netdev provides no SRV record lookup. +func (r *Resolver) LookupSRV(ctx context.Context, service, proto, name string) (string, []*SRV, error) { + return "", nil, errors.New("net:LookupSRV not implemented") +} + +// LookupTXT returns the DNS TXT records for the given domain name. +// +// TINYGO: not implemented; netdev provides no TXT record lookup. +func (r *Resolver) LookupTXT(ctx context.Context, name string) ([]string, error) { + return nil, errors.New("net:LookupTXT not implemented") +} + // errNoSuchHost is returned when the host lookup finds no matching records. var errNoSuchHost = errors.New("no such host") diff --git a/tlssock.go b/tlssock.go index c476f0d..f70875a 100644 --- a/tlssock.go +++ b/tlssock.go @@ -9,6 +9,7 @@ package net import ( + "context" "internal/itoa" "io" "net/netip" @@ -139,3 +140,10 @@ func (c *TLSConn) Handshake() error { panic("TLSConn.Handshake() not implemented") return nil } + +// HandshakeContext runs the client or server handshake protocol if it has not +// yet been run. TINYGO: TLS is offloaded to the network device; this exists to +// satisfy callers (e.g. pion/ice) that require the context-aware variant. +func (c *TLSConn) HandshakeContext(ctx context.Context) error { + return c.Handshake() +} diff --git a/unixsock.go b/unixsock.go index 5cd18f0..d9a24fb 100644 --- a/unixsock.go +++ b/unixsock.go @@ -6,6 +6,11 @@ package net +import ( + "errors" + "time" +) + // BUG(mikio): On JS, WASIP1 and Plan 9, methods and functions related // to UnixConn and UnixListener are not implemented. @@ -44,6 +49,33 @@ func (a *UnixAddr) opAddr() Addr { return a } +// UnixConn is an implementation of the Conn interface for connections to Unix +// domain sockets. +// +// TINYGO: Unix sockets are not implemented (the browser has no filesystem +// sockets). This type exists only to satisfy references and type assertions +// such as those in github.com/gliderlabs/ssh agent forwarding; all methods +// return an error. The corresponding code paths are never reached at runtime in +// the wasm build. +type UnixConn struct { + fd int + laddr *UnixAddr + raddr *UnixAddr +} + +var errUnixNotImplemented = errors.New("net: Unix sockets not implemented") + +func (c *UnixConn) Read(b []byte) (int, error) { return 0, errUnixNotImplemented } +func (c *UnixConn) Write(b []byte) (int, error) { return 0, errUnixNotImplemented } +func (c *UnixConn) Close() error { return errUnixNotImplemented } +func (c *UnixConn) CloseRead() error { return errUnixNotImplemented } +func (c *UnixConn) CloseWrite() error { return errUnixNotImplemented } +func (c *UnixConn) LocalAddr() Addr { return c.laddr } +func (c *UnixConn) RemoteAddr() Addr { return c.raddr } +func (c *UnixConn) SetDeadline(t time.Time) error { return errUnixNotImplemented } +func (c *UnixConn) SetReadDeadline(t time.Time) error { return errUnixNotImplemented } +func (c *UnixConn) SetWriteDeadline(t time.Time) error { return errUnixNotImplemented } + // ResolveUnixAddr returns an address of Unix domain socket end point. // // The network must be a Unix network name. From 2c757980bd9196991ff931f5ca90bc4ead75fdde Mon Sep 17 00:00:00 2001 From: Patricio Whittingslow Date: Mon, 6 Jul 2026 12:05:47 -0300 Subject: [PATCH 3/3] format unixsock --- unixsock.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/unixsock.go b/unixsock.go index d9a24fb..6abbe8d 100644 --- a/unixsock.go +++ b/unixsock.go @@ -65,13 +65,13 @@ type UnixConn struct { var errUnixNotImplemented = errors.New("net: Unix sockets not implemented") -func (c *UnixConn) Read(b []byte) (int, error) { return 0, errUnixNotImplemented } -func (c *UnixConn) Write(b []byte) (int, error) { return 0, errUnixNotImplemented } -func (c *UnixConn) Close() error { return errUnixNotImplemented } -func (c *UnixConn) CloseRead() error { return errUnixNotImplemented } -func (c *UnixConn) CloseWrite() error { return errUnixNotImplemented } -func (c *UnixConn) LocalAddr() Addr { return c.laddr } -func (c *UnixConn) RemoteAddr() Addr { return c.raddr } +func (c *UnixConn) Read(b []byte) (int, error) { return 0, errUnixNotImplemented } +func (c *UnixConn) Write(b []byte) (int, error) { return 0, errUnixNotImplemented } +func (c *UnixConn) Close() error { return errUnixNotImplemented } +func (c *UnixConn) CloseRead() error { return errUnixNotImplemented } +func (c *UnixConn) CloseWrite() error { return errUnixNotImplemented } +func (c *UnixConn) LocalAddr() Addr { return c.laddr } +func (c *UnixConn) RemoteAddr() Addr { return c.raddr } func (c *UnixConn) SetDeadline(t time.Time) error { return errUnixNotImplemented } func (c *UnixConn) SetReadDeadline(t time.Time) error { return errUnixNotImplemented } func (c *UnixConn) SetWriteDeadline(t time.Time) error { return errUnixNotImplemented }