net: add UDP listen, resolver, DNSError, and TCP listener APIs for wasm (#63)

* 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.

* more complete PR for adding more net support (#64)

* format unixsock
This commit is contained in:
Pat Whittingslow
2026-07-06 15:24:35 -03:00
committed by GitHub
parent 1026408a38
commit d5da3ddeef
12 changed files with 770 additions and 1 deletions
+6
View File
@@ -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.
+152
View File
@@ -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)
}
+17
View File
@@ -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.
+107 -1
View File
@@ -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) {}