From ea5f427fe98e6ef47213b9d6f8a00fde75e3fb87 Mon Sep 17 00:00:00 2001 From: Patricio Whittingslow Date: Wed, 1 Jul 2026 11:29:59 -0300 Subject: [PATCH] add lneto Signed-off-by: Patricio Whittingslow --- device/pools_test.go | 2 +- go.mod | 3 +- go.sum | 2 + tun/netstack/{tun.go => gvisor.go} | 347 +++----------------- tun/netstack/lneto.go | 487 +++++++++++++++++++++++++++++ tun/netstack/lneto_test.go | 302 ++++++++++++++++++ tun/netstack/lneto_tuncount.go | 30 ++ tun/netstack/lneto_tuncount_off.go | 7 + tun/netstack/net.go | 407 ++++++++++++++++++++++++ tun/netstack/net_debug.go | 31 ++ tun/netstack/net_debugoff.go | 5 + tun/netstack/net_gvisor.go | 15 + tun/netstack/net_lneto.go | 15 + 13 files changed, 1352 insertions(+), 301 deletions(-) rename tun/netstack/{tun.go => gvisor.go} (66%) create mode 100644 tun/netstack/lneto.go create mode 100644 tun/netstack/lneto_test.go create mode 100644 tun/netstack/lneto_tuncount.go create mode 100644 tun/netstack/lneto_tuncount_off.go create mode 100644 tun/netstack/net.go create mode 100644 tun/netstack/net_debug.go create mode 100644 tun/netstack/net_debugoff.go create mode 100644 tun/netstack/net_gvisor.go create mode 100644 tun/netstack/net_lneto.go diff --git a/device/pools_test.go b/device/pools_test.go index 8381d5a6f..3755e7b13 100644 --- a/device/pools_test.go +++ b/device/pools_test.go @@ -64,7 +64,7 @@ func TestWaitPool(t *testing.T) { } wg.Wait() if max.Load() != p.max { - t.Errorf("Actual maximum count (%d) != ideal maximum count (%d)", max, p.max) + t.Errorf("Actual maximum count (%d) != ideal maximum count (%d)", max.Load(), p.max) } } diff --git a/go.mod b/go.mod index 2a80e0001..0c62689f3 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,9 @@ module golang.zx2c4.com/wireguard -go 1.23.1 +go 1.24 require ( + github.com/soypat/lneto v0.2.0 golang.org/x/crypto v0.37.0 golang.org/x/net v0.39.0 golang.org/x/sys v0.32.0 diff --git a/go.sum b/go.sum index 61875c160..cf6bc4e61 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/soypat/lneto v0.2.0 h1:h+59QBUgWbpq/4LWOp31+6usVybN01mTDMcVW5U8OxM= +github.com/soypat/lneto v0.2.0/go.mod h1:Be5PjwoYukvHFiUXxpYi8+ppH2F/gw/vjGBvFdv+Ti8= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= diff --git a/tun/netstack/tun.go b/tun/netstack/gvisor.go similarity index 66% rename from tun/netstack/tun.go rename to tun/netstack/gvisor.go index a7aec9e82..50e6ff789 100644 --- a/tun/netstack/tun.go +++ b/tun/netstack/gvisor.go @@ -16,8 +16,6 @@ import ( "net" "net/netip" "os" - "regexp" - "strconv" "strings" "syscall" "time" @@ -50,9 +48,7 @@ type netTun struct { hasV4, hasV6 bool } -type Net netTun - -func CreateNetTUN(localAddresses, dnsServers []netip.Addr, mtu int) (tun.Device, *Net, error) { +func CreateNetTUNGvisor(localAddresses, dnsServers []netip.Addr, mtu int) (tun.Device, *Net, error) { opts := stack.Options{ NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol}, TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol, icmp.NewProtocol6, icmp.NewProtocol4}, @@ -105,9 +101,11 @@ func CreateNetTUN(localAddresses, dnsServers []netip.Addr, mtu int) (tun.Device, } dev.events <- tun.EventUp - return dev, (*Net)(dev), nil + return dev, &Net{stack: dev}, nil } +var _ Stack = (*netTun)(nil) + func (tun *netTun) Name() (string, error) { return "go", nil } @@ -131,6 +129,7 @@ func (tun *netTun) Read(buf [][]byte, sizes []int, offset int) (int, error) { return 0, err } sizes[0] = n + debugIPPacket(true, buf[0][offset:offset+n]) return 1, nil } @@ -140,6 +139,7 @@ func (tun *netTun) Write(buf [][]byte, offset int) (int, error) { if len(packet) == 0 { continue } + debugIPPacket(false, packet) pkb := stack.NewPacketBuffer(stack.PacketBufferOptions{Payload: buffer.MakeWithData(packet)}) switch packet[0] >> 4 { @@ -205,46 +205,34 @@ func convertToFullAddr(endpoint netip.AddrPort) (tcpip.FullAddress, tcpip.Networ }, protoNumber } -func (net *Net) DialContextTCPAddrPort(ctx context.Context, addr netip.AddrPort) (*gonet.TCPConn, error) { +func (tun *netTun) DialContextTCPAddrPort(ctx context.Context, addr netip.AddrPort) (TCPConn, error) { fa, pn := convertToFullAddr(addr) - return gonet.DialContextTCP(ctx, net.stack, fa, pn) -} - -func (net *Net) DialContextTCP(ctx context.Context, addr *net.TCPAddr) (*gonet.TCPConn, error) { - if addr == nil { - return net.DialContextTCPAddrPort(ctx, netip.AddrPort{}) + c, err := gonet.DialContextTCP(ctx, tun.stack, fa, pn) + if err != nil { + return nil, err } - ip, _ := netip.AddrFromSlice(addr.IP) - return net.DialContextTCPAddrPort(ctx, netip.AddrPortFrom(ip, uint16(addr.Port))) + return c, nil } -func (net *Net) DialTCPAddrPort(addr netip.AddrPort) (*gonet.TCPConn, error) { +func (tun *netTun) DialTCPAddrPort(addr netip.AddrPort) (TCPConn, error) { fa, pn := convertToFullAddr(addr) - return gonet.DialTCP(net.stack, fa, pn) -} - -func (net *Net) DialTCP(addr *net.TCPAddr) (*gonet.TCPConn, error) { - if addr == nil { - return net.DialTCPAddrPort(netip.AddrPort{}) + c, err := gonet.DialTCP(tun.stack, fa, pn) + if err != nil { + return nil, err } - ip, _ := netip.AddrFromSlice(addr.IP) - return net.DialTCPAddrPort(netip.AddrPortFrom(ip, uint16(addr.Port))) + return c, nil } -func (net *Net) ListenTCPAddrPort(addr netip.AddrPort) (*gonet.TCPListener, error) { +func (tun *netTun) ListenTCPAddrPort(addr netip.AddrPort) (TCPListener, error) { fa, pn := convertToFullAddr(addr) - return gonet.ListenTCP(net.stack, fa, pn) -} - -func (net *Net) ListenTCP(addr *net.TCPAddr) (*gonet.TCPListener, error) { - if addr == nil { - return net.ListenTCPAddrPort(netip.AddrPort{}) + l, err := gonet.ListenTCP(tun.stack, fa, pn) + if err != nil { + return nil, err } - ip, _ := netip.AddrFromSlice(addr.IP) - return net.ListenTCPAddrPort(netip.AddrPortFrom(ip, uint16(addr.Port))) + return l, nil } -func (net *Net) DialUDPAddrPort(laddr, raddr netip.AddrPort) (*gonet.UDPConn, error) { +func (tun *netTun) DialUDPAddrPort(laddr, raddr netip.AddrPort) (UDPConn, error) { var lfa, rfa *tcpip.FullAddress var pn tcpip.NetworkProtocolNumber if laddr.IsValid() || laddr.Port() > 0 { @@ -257,31 +245,19 @@ func (net *Net) DialUDPAddrPort(laddr, raddr netip.AddrPort) (*gonet.UDPConn, er addr, pn = convertToFullAddr(raddr) rfa = &addr } - return gonet.DialUDP(net.stack, lfa, rfa, pn) -} - -func (net *Net) ListenUDPAddrPort(laddr netip.AddrPort) (*gonet.UDPConn, error) { - return net.DialUDPAddrPort(laddr, netip.AddrPort{}) -} - -func (net *Net) DialUDP(laddr, raddr *net.UDPAddr) (*gonet.UDPConn, error) { - var la, ra netip.AddrPort - if laddr != nil { - ip, _ := netip.AddrFromSlice(laddr.IP) - la = netip.AddrPortFrom(ip, uint16(laddr.Port)) - } - if raddr != nil { - ip, _ := netip.AddrFromSlice(raddr.IP) - ra = netip.AddrPortFrom(ip, uint16(raddr.Port)) + c, err := gonet.DialUDP(tun.stack, lfa, rfa, pn) + if err != nil { + return nil, err } - return net.DialUDPAddrPort(la, ra) + return c, nil } -func (net *Net) ListenUDP(laddr *net.UDPAddr) (*gonet.UDPConn, error) { - return net.DialUDP(laddr, nil) +func (tun *netTun) ListenUDPAddrPort(laddr netip.AddrPort) (UDPConn, error) { + return tun.DialUDPAddrPort(laddr, netip.AddrPort{}) } -type PingConn struct { +// pingConn is the gvisor-backed [PingConn] implementation. +type pingConn struct { laddr PingAddr raddr PingAddr wq waiter.Queue @@ -289,30 +265,7 @@ type PingConn struct { deadline *time.Timer } -type PingAddr struct{ addr netip.Addr } - -func (ia PingAddr) String() string { - return ia.addr.String() -} - -func (ia PingAddr) Network() string { - if ia.addr.Is4() { - return "ping4" - } else if ia.addr.Is6() { - return "ping6" - } - return "ping" -} - -func (ia PingAddr) Addr() netip.Addr { - return ia.addr -} - -func PingAddrFromAddr(addr netip.Addr) *PingAddr { - return &PingAddr{addr} -} - -func (net *Net) DialPingAddr(laddr, raddr netip.Addr) (*PingConn, error) { +func (tun *netTun) DialPingAddr(laddr, raddr netip.Addr) (PingConn, error) { if !laddr.IsValid() && !raddr.IsValid() { return nil, errors.New("ping dial: invalid address") } @@ -333,13 +286,13 @@ func (net *Net) DialPingAddr(laddr, raddr netip.Addr) (*PingConn, error) { pn = ipv6.ProtocolNumber } - pc := &PingConn{ + pc := &pingConn{ laddr: PingAddr{laddr}, deadline: time.NewTimer(time.Hour << 10), } pc.deadline.Stop() - ep, tcpipErr := net.stack.NewEndpoint(tn, pn, &pc.wq) + ep, tcpipErr := tun.stack.NewEndpoint(tn, pn, &pc.wq) if tcpipErr != nil { return nil, fmt.Errorf("ping socket: endpoint: %s", tcpipErr) } @@ -363,48 +316,29 @@ func (net *Net) DialPingAddr(laddr, raddr netip.Addr) (*PingConn, error) { return pc, nil } -func (net *Net) ListenPingAddr(laddr netip.Addr) (*PingConn, error) { - return net.DialPingAddr(laddr, netip.Addr{}) +func (tun *netTun) ListenPingAddr(laddr netip.Addr) (PingConn, error) { + return tun.DialPingAddr(laddr, netip.Addr{}) } -func (net *Net) DialPing(laddr, raddr *PingAddr) (*PingConn, error) { - var la, ra netip.Addr - if laddr != nil { - la = laddr.addr - } - if raddr != nil { - ra = raddr.addr - } - return net.DialPingAddr(la, ra) -} - -func (net *Net) ListenPing(laddr *PingAddr) (*PingConn, error) { - var la netip.Addr - if laddr != nil { - la = laddr.addr - } - return net.ListenPingAddr(la) -} - -func (pc *PingConn) LocalAddr() net.Addr { +func (pc *pingConn) LocalAddr() net.Addr { return pc.laddr } -func (pc *PingConn) RemoteAddr() net.Addr { +func (pc *pingConn) RemoteAddr() net.Addr { return pc.raddr } -func (pc *PingConn) Close() error { +func (pc *pingConn) Close() error { pc.deadline.Reset(0) pc.ep.Close() return nil } -func (pc *PingConn) SetWriteDeadline(t time.Time) error { +func (pc *pingConn) SetWriteDeadline(t time.Time) error { return errors.New("not implemented") } -func (pc *PingConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { +func (pc *pingConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { var na netip.Addr switch v := addr.(type) { case *PingAddr: @@ -431,11 +365,11 @@ func (pc *PingConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { return int(n64), nil } -func (pc *PingConn) Write(p []byte) (n int, err error) { +func (pc *pingConn) Write(p []byte) (n int, err error) { return pc.WriteTo(p, &pc.raddr) } -func (pc *PingConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { +func (pc *pingConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { e, notifyCh := waiter.NewChannelEntry(waiter.EventIn) pc.wq.EventRegister(&e) defer pc.wq.EventUnregister(&e) @@ -459,83 +393,22 @@ func (pc *PingConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { return res.Count, &PingAddr{remoteAddr}, nil } -func (pc *PingConn) Read(p []byte) (n int, err error) { +func (pc *pingConn) Read(p []byte) (n int, err error) { n, _, err = pc.ReadFrom(p) return } -func (pc *PingConn) SetDeadline(t time.Time) error { +func (pc *pingConn) SetDeadline(t time.Time) error { // pc.SetWriteDeadline is unimplemented return pc.SetReadDeadline(t) } -func (pc *PingConn) SetReadDeadline(t time.Time) error { +func (pc *pingConn) SetReadDeadline(t time.Time) error { pc.deadline.Reset(time.Until(t)) return nil } -var ( - errNoSuchHost = errors.New("no such host") - errLameReferral = errors.New("lame referral") - errCannotUnmarshalDNSMessage = errors.New("cannot unmarshal DNS message") - errCannotMarshalDNSMessage = errors.New("cannot marshal DNS message") - errServerMisbehaving = errors.New("server misbehaving") - errInvalidDNSResponse = errors.New("invalid DNS response") - errNoAnswerFromDNSServer = errors.New("no answer from DNS server") - errServerTemporarilyMisbehaving = errors.New("server misbehaving") - errCanceled = errors.New("operation was canceled") - errTimeout = errors.New("i/o timeout") - errNumericPort = errors.New("port must be numeric") - errNoSuitableAddress = errors.New("no suitable address found") - errMissingAddress = errors.New("missing address") -) - -func (net *Net) LookupHost(host string) (addrs []string, err error) { - return net.LookupContextHost(context.Background(), host) -} - -func isDomainName(s string) bool { - l := len(s) - if l == 0 || l > 254 || l == 254 && s[l-1] != '.' { - return false - } - last := byte('.') - nonNumeric := false - partlen := 0 - for i := 0; i < len(s); i++ { - c := s[i] - switch { - default: - return false - case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_': - nonNumeric = true - partlen++ - case '0' <= c && c <= '9': - partlen++ - case c == '-': - if last == '.' { - return false - } - partlen++ - nonNumeric = true - case c == '.': - if last == '.' || last == '-' { - return false - } - if partlen > 63 || partlen == 0 { - return false - } - partlen = 0 - } - last = c - } - if last == '-' || partlen > 63 { - return false - } - return nonNumeric -} - func randU16() uint16 { var b [2]byte _, err := rand.Read(b[:]) @@ -650,7 +523,7 @@ func dnsStreamRoundTrip(c net.Conn, id uint16, query dnsmessage.Question, b []by return p, h, nil } -func (tnet *Net) exchange(ctx context.Context, server netip.Addr, q dnsmessage.Question, timeout time.Duration) (dnsmessage.Parser, dnsmessage.Header, error) { +func (tnet *netTun) exchange(ctx context.Context, server netip.Addr, q dnsmessage.Question, timeout time.Duration) (dnsmessage.Parser, dnsmessage.Header, error) { q.Class = dnsmessage.ClassINET id, udpReq, tcpReq, err := newRequest(q) if err != nil { @@ -743,7 +616,7 @@ func skipToAnswer(p *dnsmessage.Parser, qtype dnsmessage.Type) error { } } -func (tnet *Net) tryOneName(ctx context.Context, name string, qtype dnsmessage.Type) (dnsmessage.Parser, string, error) { +func (tnet *netTun) tryOneName(ctx context.Context, name string, qtype dnsmessage.Type) (dnsmessage.Parser, string, error) { var lastErr error n, err := dnsmessage.NewName(name) @@ -810,7 +683,7 @@ func (tnet *Net) tryOneName(ctx context.Context, name string, qtype dnsmessage.T return dnsmessage.Parser{}, "", lastErr } -func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]string, error) { +func (tnet *netTun) LookupContextHost(ctx context.Context, host string) ([]string, error) { if host == "" || (!tnet.hasV6 && !tnet.hasV4) { return nil, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true} } @@ -931,127 +804,3 @@ func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]string, } return saddrs, nil } - -func partialDeadline(now, deadline time.Time, addrsRemaining int) (time.Time, error) { - if deadline.IsZero() { - return deadline, nil - } - timeRemaining := deadline.Sub(now) - if timeRemaining <= 0 { - return time.Time{}, errTimeout - } - timeout := timeRemaining / time.Duration(addrsRemaining) - const saneMinimum = 2 * time.Second - if timeout < saneMinimum { - if timeRemaining < saneMinimum { - timeout = timeRemaining - } else { - timeout = saneMinimum - } - } - return now.Add(timeout), nil -} - -var protoSplitter = regexp.MustCompile(`^(tcp|udp|ping)(4|6)?$`) - -func (tnet *Net) DialContext(ctx context.Context, network, address string) (net.Conn, error) { - if ctx == nil { - panic("nil context") - } - var acceptV4, acceptV6 bool - matches := protoSplitter.FindStringSubmatch(network) - if matches == nil { - return nil, &net.OpError{Op: "dial", Err: net.UnknownNetworkError(network)} - } else if len(matches[2]) == 0 { - acceptV4 = true - acceptV6 = true - } else { - acceptV4 = matches[2][0] == '4' - acceptV6 = !acceptV4 - } - var host string - var port int - if matches[1] == "ping" { - host = address - } else { - var sport string - var err error - host, sport, err = net.SplitHostPort(address) - if err != nil { - return nil, &net.OpError{Op: "dial", Err: err} - } - port, err = strconv.Atoi(sport) - if err != nil || port < 0 || port > 65535 { - return nil, &net.OpError{Op: "dial", Err: errNumericPort} - } - } - allAddr, err := tnet.LookupContextHost(ctx, host) - if err != nil { - return nil, &net.OpError{Op: "dial", Err: err} - } - var addrs []netip.AddrPort - for _, addr := range allAddr { - ip, err := netip.ParseAddr(addr) - if err == nil && ((ip.Is4() && acceptV4) || (ip.Is6() && acceptV6)) { - addrs = append(addrs, netip.AddrPortFrom(ip, uint16(port))) - } - } - if len(addrs) == 0 && len(allAddr) != 0 { - return nil, &net.OpError{Op: "dial", Err: errNoSuitableAddress} - } - - var firstErr error - for i, addr := range addrs { - select { - case <-ctx.Done(): - err := ctx.Err() - if err == context.Canceled { - err = errCanceled - } else if err == context.DeadlineExceeded { - err = errTimeout - } - return nil, &net.OpError{Op: "dial", Err: err} - default: - } - - dialCtx := ctx - if deadline, hasDeadline := ctx.Deadline(); hasDeadline { - partialDeadline, err := partialDeadline(time.Now(), deadline, len(addrs)-i) - if err != nil { - if firstErr == nil { - firstErr = &net.OpError{Op: "dial", Err: err} - } - break - } - if partialDeadline.Before(deadline) { - var cancel context.CancelFunc - dialCtx, cancel = context.WithDeadline(ctx, partialDeadline) - defer cancel() - } - } - - var c net.Conn - switch matches[1] { - case "tcp": - c, err = tnet.DialContextTCPAddrPort(dialCtx, addr) - case "udp": - c, err = tnet.DialUDPAddrPort(netip.AddrPort{}, addr) - case "ping": - c, err = tnet.DialPingAddr(netip.Addr{}, addr.Addr()) - } - if err == nil { - return c, nil - } - if firstErr == nil { - firstErr = err - } - } - if firstErr == nil { - firstErr = &net.OpError{Op: "dial", Err: errMissingAddress} - } - return nil, firstErr -} - -func (tnet *Net) Dial(network, address string) (net.Conn, error) { - return tnet.DialContext(context.Background(), network, address) -} diff --git a/tun/netstack/lneto.go b/tun/netstack/lneto.go new file mode 100644 index 000000000..4024d9070 --- /dev/null +++ b/tun/netstack/lneto.go @@ -0,0 +1,487 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. + */ + +package netstack + +import ( + "context" + crand "crypto/rand" + "encoding/binary" + "errors" + "fmt" + "net" + "net/netip" + "os" + "runtime" + "strings" + "sync" + "syscall" + "time" + + "github.com/soypat/lneto" + "github.com/soypat/lneto/dns" + "github.com/soypat/lneto/x/xnet" + "golang.zx2c4.com/wireguard/tun" +) + +func CreateNetTUNLneto(localAddresses, dnsServers []netip.Addr, mtu int) (tun.Device, *Net, error) { + if mtu <= 0 { + mtu = 1500 + } + if mtu > 65535 { + return nil, nil, fmt.Errorf("CreateNetTUNLneto: mtu %d exceeds maximum 65535", mtu) + } + dev := &lnetoStack{ + events: make(chan tun.Event, 10), + closed: make(chan struct{}), + mtu: mtu, + dnsServers: dnsServers, + } + + var hwAddr [6]byte + if _, err := crand.Read(hwAddr[:]); err != nil { + return nil, nil, fmt.Errorf("CreateNetTUNLneto: rand MAC: %w", err) + } + hwAddr[0] &^= 0x01 // unicast + hwAddr[0] |= 0x02 // locally administered + + // Pick the first IPv4 and first IPv6 local address; track which families are + // configured (mirrors gVisor's hasV4/hasV6). + var staticAddr4 [4]byte + var staticAddr6 [16]byte + for _, addr := range localAddresses { + if addr.Is4() && !dev.hasV4 { + staticAddr4 = addr.As4() + dev.hasV4 = true + } else if addr.Is6() && !dev.hasV6 { + staticAddr6 = addr.As16() + dev.hasV6 = true + } + } + + // DNS server: prefer IPv4 (the stack's lookup path is currently IPv4-only), + // fall back to the first IPv6 server. + var dnsServer netip.Addr + for _, d := range dnsServers { + if d.Is4() { + dnsServer = d + break + } + } + if !dnsServer.IsValid() { + for _, d := range dnsServers { + if d.Is6() { + dnsServer = d + break + } + } + } + + var randSeed int64 + if err := binary.Read(crand.Reader, binary.LittleEndian, &randSeed); err != nil { + return nil, nil, fmt.Errorf("CreateNetTUNLneto: rand seed: %w", err) + } + + cfg := xnet.StackConfig{ + HardwareAddress: hwAddr, + StaticAddress4: staticAddr4, + MTU: uint16(mtu), + Hostname: "wg0", + RandSeed: randSeed, + PassivePeers: 0, // no ARP passive learning needed for TUN + ICMPQueueLimit: 4, + MaxActiveTCPPorts: 256, + MaxActiveUDPPorts: 256, + DNSServer: dnsServer, + } + if dev.hasV6 { + cfg.StaticAddress6 = staticAddr6 + cfg.IPv6Stack = xnet.DefaultStack6() + } + // NOTE: ICMP is intentionally NOT enabled here. On a TUN there is no link layer, + // so MAC resolution must be skipped: IPv4 ARP is gated off by leaving the subnet + // unset, and IPv6 NDP is gated off by leaving ICMPv6 unregistered. Enabling ICMP + // would make DialTCP6/DialUDP6 emit Neighbor Solicitations that are never answered + // on a TUN, breaking IPv6 dialing. Ping support (which needs ICMP) is a separate + // follow-up that must reconcile this. + if err := dev.sa.Reset(cfg); err != nil { + return nil, nil, fmt.Errorf("CreateNetTUNLneto: stack reset: %w", err) + } + + // Backoff selection. A native single-threaded runtime (GOMAXPROCS==1) only yields + // cooperatively, because sleeping the poll would starve egress — there is no other + // thread to drain it. js/wasm is also single-threaded but is the opposite case: it + // MUST sleep so the runtime hands control back to the browser event loop (a bare + // Gosched there starves all WebSocket/WireGuard I/O and freezes the tab). So js, + // like the multi-threaded case, keeps the exponential sleeping backoffs — applied + // to BOTH the stack poll and every per-connection RWBackoff. + baseStack := defaultStackBackoff + newTCPBackoff := func() lneto.BackoffStrategy { return defaultTCPBackoff } + if runtime.GOMAXPROCS(0) == 1 && runtime.GOOS != "js" { + baseStack = backoffYield + newTCPBackoff = func() lneto.BackoffStrategy { return backoffYield } + } + irq, backoff := interruptBackoff(baseStack) + dev.backoff = backoff + dev.backoffirq = irq + + dev.sgo = dev.sa.StackGo(backoff, xnet.StackGoConfig{ + ListenerPoolConfig: xnet.TCPPoolConfig{ + PoolSize: 256, + QueueSize: 8, + TxBufSize: 32 << 10, + RxBufSize: 32 << 10, + EstablishedTimeout: 30 * time.Second, + ClosingTimeout: 10 * time.Second, + NewBackoff: newTCPBackoff, // required: StackGo panics if nil. + }, + TCPDialTimeout: time.Second, + TCPDialRetries: 30, + }) + dev.events <- tun.EventUp + return dev, &Net{stack: dev}, nil +} + +// lnetoStack is a lneto-backed userspace network stack that implements both +// [tun.Device] (for WireGuard integration) and a networking API (Dial/Listen/DNS). +// +// Packet flow: +// - Ingress (WireGuard → stack): [lnetoStack.Write] calls [xnet.StackAsync.IngressIP], +// then pokes the backoff irq so a blocked [lnetoStack.Read] wakes immediately. +// - Egress (stack → WireGuard): [lnetoStack.Read] polls [xnet.StackAsync.EgressIP] +// directly, sleeping on an interruptible backoff between empty polls. +// +// Unlike gVisor's channel.Endpoint there is no native egress notification hook, so +// egress is driven by Read's poll loop. The backoff is interruptible (woken by +// [lnetoStack.interrupt]) and GOMAXPROCS-aware: on a single-threaded runtime it only +// yields cooperatively (sleeping would starve the poll), otherwise it sleeps with +// exponential backoff. This mirrors the proven go-net design. +// +// Lifecycle: created by [CreateNetTUNLneto], torn down by [lnetoStack.Close] which closes +// the closed channel (unblocking Read and any blocking socket op) and events. +type lnetoStack struct { + sa xnet.StackAsync + sgo xnet.StackGo // wraps sa; created once in CreateNetTUNLneto + + // events carries TUN state changes (e.g. EventUp) consumed by WireGuard's device loop. + events chan tun.Event + + // backoff is the interruptible stack-protocol backoff shared with blk/sgo and + // used by Read's egress poll. backoffirq wakes a sleeping backoff the moment new + // work arrives (ingress packet, socket call) via interrupt. + backoff lneto.BackoffStrategy + backoffirq chan<- event + + // closed is closed once by Close to unblock Read and signal shutdown. + closed chan struct{} + closeOnce sync.Once + + mtu int + dnsServers []netip.Addr + hasV4, hasV6 bool +} + +type event struct{} + +// interruptBackoff wraps a [lneto.BackoffStrategy] so its sleep can be cut short by +// a write to the returned irq channel. Each call builds its own timer so concurrent +// callers (Read's poll loop and blocking socket ops) do not race on a shared one; +// the capacity-1 irq is shared, so one interrupt wakes exactly one sleeper, which is +// the intended best-effort behavior. The returned strategy always reports +// [lneto.BackoffFlagNop] because the yield is performed entirely inside the wrapper. +func interruptBackoff(backoff lneto.BackoffStrategy) (interrupt chan<- event, _ lneto.BackoffStrategy) { + irq := make(chan event, 1) + wrapped := func(consecutiveBackoffs uint) time.Duration { + switch d := backoff(consecutiveBackoffs); d { + case lneto.BackoffFlagGosched: + runtime.Gosched() + case lneto.BackoffFlagNop: + // Do nothing. + default: + timer := time.NewTimer(d) // per-call: callers must not share a timer. + select { + case <-irq: + if !timer.Stop() && len(timer.C) > 0 { + <-timer.C + } + case <-timer.C: + } + } + return lneto.BackoffFlagNop // yield handled here; signal caller to do nothing. + } + return irq, wrapped +} + +// defaultStackBackoff is the idle backoff for stack protocol loops (DHCP, DNS, the +// egress poll, ...) on multi-threaded runtimes: exponential from 100µs up to 20ms. +func defaultStackBackoff(consecutiveBackoffs uint) time.Duration { + const ( + minWait = 100 * time.Microsecond + maxWait = 20 * time.Millisecond + maxShift = 15 + + _compileTimeOverflowCheck = minWait << maxShift + ) + sleep := minWait << min(consecutiveBackoffs, maxShift) + if sleep > maxWait { + sleep = maxWait + } + return sleep +} + +// defaultTCPBackoff is the per-connection read/write retry backoff for TCP streams. +// Shorter range than defaultStackBackoff to keep interactive sessions responsive. +func defaultTCPBackoff(consecutiveBackoffs uint) time.Duration { + const ( + minWait = 10 * time.Microsecond + maxWait = 1 * time.Millisecond + maxShift = 10 + + _compileTimeOverflowCheck = minWait << maxShift + ) + sleep := minWait << min(consecutiveBackoffs, maxShift) + if sleep > maxWait { + sleep = maxWait + } + return sleep +} + +// backoffYield never sleeps; it only yields cooperatively. Used on GOMAXPROCS==1 +// where sleeping the poll goroutine would starve egress processing. +func backoffYield(consecutiveBackoffs uint) time.Duration { + return lneto.BackoffFlagGosched +} + +// interrupt wakes one sleeper blocked in the interruptible backoff (Read's poll loop +// or a blocking socket op). Non-blocking and safe to call from any goroutine. +func (n *lnetoStack) interrupt() { + select { + case n.backoffirq <- event{}: + default: + } +} + +// --- tun.Device implementation --- + +func (n *lnetoStack) Name() (string, error) { return "go2", nil } +func (n *lnetoStack) File() *os.File { return nil } +func (n *lnetoStack) Events() <-chan tun.Event { return n.events } +func (n *lnetoStack) MTU() (int, error) { return n.mtu, nil } +func (n *lnetoStack) BatchSize() int { return 1 } + +// Write feeds incoming IP packets (WireGuard → stack) into the lneto stack. +func (n *lnetoStack) Write(bufs [][]byte, offset int) (int, error) { + wrote := false + for _, buf := range bufs { + if pkt := buf[offset:]; len(pkt) > 0 { + n.sa.IngressIP(pkt) // errors dropped; stack silently filters bad packets + debugIPPacket(false, pkt) + wrote = true + } + } + if wrote { + n.interrupt() // wake Read: ingress often produces an immediate egress reply. + } + return len(bufs), nil +} + +// Read blocks until the stack has an outgoing IP packet to send to WireGuard, polling +// EgressIP and sleeping on the interruptible backoff between empty polls. It writes +// directly into the caller's buffer (no intermediate copy). Returns os.ErrClosed once +// Close has been called. +func (n *lnetoStack) Read(bufs [][]byte, sizes []int, offset int) (int, error) { + dst := bufs[0][offset:] + var backoffs uint + for { + select { + case <-n.closed: + return 0, os.ErrClosed + default: + } + cnt, _ := n.sa.EgressIP(dst) + if cnt > 0 { + sizes[0] = cnt + debugIPPacket(true, dst[:cnt]) + return 1, nil + } + n.backoff.Do(backoffs) // interruptible; returns promptly when interrupt fires. + backoffs++ + } +} + +func (n *lnetoStack) Close() error { + n.closeOnce.Do(func() { + close(n.closed) // unblock Read. + n.interrupt() // wake a backoff sleeper so it observes closed promptly. + close(n.events) + }) + return nil +} + +var _ Stack = (*lnetoStack)(nil) + +// --- TCP --- + +// socketResult extracts a typed result from a SocketNetip call. +// SocketNetip's TCP dial branch returns connection errors as the value (not err) +// to distinguish stack-level failures from protocol errors, so we handle both. +func socketResult[T any](v any, err error) (T, error) { + var zero T + if err != nil { + return zero, err + } + if e, ok := v.(error); ok { + return zero, e + } + t, ok := v.(T) + if !ok { + return zero, fmt.Errorf("socket: unexpected type %T", v) + } + return t, nil +} + +// socket wraps sgo.SocketNetip. It selects the IPv4 or IPv6 network/family from the +// family-bearing endpoint (the remote for a dial, otherwise the local bind), and pokes +// the egress poll on entry and exit because connection setup (handshake, NDP/ARP) +// queues egress frames that Read must drain promptly. +func (n *lnetoStack) socket(ctx context.Context, proto string, sotype int, laddr, raddr netip.AddrPort) (any, error) { + fam := raddr + if !fam.IsValid() { + fam = laddr + } + network := proto + "4" + family := syscall.AF_INET + if fam.Addr().Is6() { + network = proto + "6" + family = syscall.AF_INET6 + } + n.interrupt() + defer n.interrupt() + return n.sgo.SocketNetip(ctx, network, family, sotype, laddr, raddr) +} + +func (n *lnetoStack) dialTCPCtx(ctx context.Context, addr netip.AddrPort) (TCPConn, error) { + v, err := n.socket(ctx, "tcp", syscall.SOCK_STREAM, netip.AddrPort{}, addr) + return socketResult[TCPConn](v, err) +} + +func (n *lnetoStack) DialContextTCPAddrPort(ctx context.Context, addr netip.AddrPort) (TCPConn, error) { + return n.dialTCPCtx(ctx, addr) +} + +func (n *lnetoStack) DialTCPAddrPort(addr netip.AddrPort) (TCPConn, error) { + return n.dialTCPCtx(context.Background(), addr) +} + +// --- TCP listener --- + +func (n *lnetoStack) ListenTCPAddrPort(addr netip.AddrPort) (TCPListener, error) { + v, err := n.socket(context.Background(), "tcp", syscall.SOCK_STREAM, addr, netip.AddrPort{}) + return socketResult[TCPListener](v, err) +} + +// --- UDP --- + +func (n *lnetoStack) ListenUDPAddrPort(laddr netip.AddrPort) (UDPConn, error) { + v, err := n.socket(context.Background(), "udp", syscall.SOCK_DGRAM, laddr, netip.AddrPort{}) + return socketResult[UDPConn](v, err) +} + +func (n *lnetoStack) DialUDPAddrPort(laddr, raddr netip.AddrPort) (UDPConn, error) { + v, err := n.socket(context.Background(), "udp", syscall.SOCK_DGRAM, laddr, raddr) + return socketResult[UDPConn](v, err) +} + +// --- Ping --- + +func (n *lnetoStack) DialPingAddr(_, _ netip.Addr) (PingConn, error) { + return nil, errors.New("ping not implemented for lnetoStack") +} + +func (n *lnetoStack) ListenPingAddr(_ netip.Addr) (PingConn, error) { + return nil, errors.New("ping not implemented for lnetoStack") +} + +// --- DNS --- + +// dnsError wraps a lookup failure as a *net.DNSError, flagging timeouts when the +// underlying error reports them. Mirrors the error shape produced by the gvisor Net. +func dnsError(host string, err error) *net.DNSError { + de := &net.DNSError{Err: err.Error(), Name: host} + if nerr, ok := err.(net.Error); ok && nerr.Timeout() { + de.IsTimeout = true + } + return de +} + +// LookupContextHost resolves host to a list of IP strings, matching the behaviour of +// the gvisor Net: literal IPs (with IPv6 zone stripping) pass through; empty or +// non-domain hosts and stacks with no address family return an IsNotFound DNSError; +// A and AAAA are queried for the enabled families and, when IPv6 is enabled, IPv6 +// results are ordered first (no RFC 6724). +func (n *lnetoStack) LookupContextHost(ctx context.Context, host string) ([]string, error) { + if host == "" || (!n.hasV4 && !n.hasV6) { + return nil, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true} + } + // Strip any IPv6 zone before attempting to parse a literal address. + zlen := len(host) + if strings.IndexByte(host, ':') != -1 { + if zidx := strings.LastIndexByte(host, '%'); zidx != -1 { + zlen = zidx + } + } + if ip, err := netip.ParseAddr(host[:zlen]); err == nil { + return []string{ip.String()}, nil + } + if !isDomainName(host) { + return nil, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true} + } + + timeout := 5 * time.Second + if dl, ok := ctx.Deadline(); ok { + if rem := time.Until(dl); rem < timeout { + timeout = rem + } + } + blk := n.sa.StackBlocking(n.backoff) + + var addrsV4, addrsV6 []netip.Addr + var lastErr error + if n.hasV4 { + if a, err := blk.DoLookupIPType(host, timeout, dns.TypeA); err != nil { + lastErr = dnsError(host, err) + } else { + addrsV4 = a + } + } + if n.hasV6 { + if a, err := blk.DoLookupIPType(host, timeout, dns.TypeAAAA); err != nil { + if lastErr == nil { + lastErr = dnsError(host, err) + } + } else { + addrsV6 = a + } + } + + // IPv6 first when enabled, mirroring the gvisor Net's ordering. + var addrs []netip.Addr + if n.hasV6 { + addrs = append(addrsV6, addrsV4...) + } else { + addrs = append(addrsV4, addrsV6...) + } + if len(addrs) == 0 { + if lastErr != nil { + return nil, lastErr + } + return nil, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true} + } + out := make([]string, len(addrs)) + for i, a := range addrs { + out[i] = a.String() + } + return out, nil +} diff --git a/tun/netstack/lneto_test.go b/tun/netstack/lneto_test.go new file mode 100644 index 000000000..634c258d9 --- /dev/null +++ b/tun/netstack/lneto_test.go @@ -0,0 +1,302 @@ +//go:build wglneto + +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. + */ + +package netstack + +import ( + "context" + "errors" + "net" + "net/netip" + "os" + "sync" + "testing" + "time" + + "golang.zx2c4.com/wireguard/tun" +) + +// TestNet2_Construct is a regression test: the previous CreateNetTUNLneto omitted +// TCPPoolConfig.NewBackoff, which made StackGo panic at construction. +func TestNet2_Construct(t *testing.T) { + dev, net2, err := CreateNetTUNLneto( + []netip.Addr{netip.MustParseAddr("10.0.0.1")}, + []netip.Addr{netip.MustParseAddr("8.8.8.8")}, + 1500, + ) + if err != nil { + t.Fatal(err) + } + defer dev.Close() + if net2 == nil { + t.Fatal("nil Net") + } + select { + case ev := <-dev.Events(): + if ev != tun.EventUp { + t.Fatalf("want EventUp, got %v", ev) + } + case <-time.After(time.Second): + t.Fatal("no EventUp event") + } +} + +// TestNet2_CloseRace stresses the Read/Write/Close interaction that previously +// panicked with "send on closed channel". Run with -race. +func TestNet2_CloseRace(t *testing.T) { + for i := 0; i < 50; i++ { + dev, _, err := CreateNetTUNLneto( + []netip.Addr{netip.MustParseAddr("10.0.0.1")}, + nil, 1500, + ) + if err != nil { + t.Fatal(err) + } + <-dev.Events() // drain EventUp + + var wg sync.WaitGroup + // Reader: must return os.ErrClosed once Close fires, never panic. + wg.Add(1) + go func() { + defer wg.Done() + bufs := [][]byte{make([]byte, 2048)} + sizes := []int{0} + for { + _, err := dev.Read(bufs, sizes, 0) + if errors.Is(err, os.ErrClosed) { + return + } + } + }() + // Writer: feed junk ingress concurrently with Close. + wg.Add(1) + go func() { + defer wg.Done() + pkt := make([]byte, 40) + pkt[0] = 0x45 // IPv4, IHL 5 + for j := 0; j < 1000; j++ { + if _, err := dev.Write([][]byte{pkt}, 0); err != nil { + return + } + } + }() + + time.Sleep(time.Millisecond) + if err := dev.Close(); err != nil { + t.Fatal(err) + } + // Double close must be safe (no panic). + if err := dev.Close(); err != nil { + t.Fatal(err) + } + wg.Wait() + } +} + +// TestNet2_ListenTCPPort0 covers gap D: listening on port 0 must auto-assign an +// ephemeral port instead of failing (the library previously returned ErrZeroSource). +func TestNet2_ListenTCPPort0(t *testing.T) { + dev, net2, err := CreateNetTUNLneto([]netip.Addr{netip.MustParseAddr("10.0.0.1")}, nil, 1500) + if err != nil { + t.Fatal(err) + } + defer dev.Close() + <-dev.Events() + + ln, err := net2.ListenTCPAddrPort(netip.AddrPort{}) + if err != nil { + t.Fatal("listen on port 0:", err) + } + defer ln.Close() + if a, ok := ln.Addr().(*net.TCPAddr); !ok || a.Port == 0 { + t.Fatalf("expected an auto-assigned ephemeral port, got %v", ln.Addr()) + } +} + +// TestNet2_UDPEcho wires two Net2 instances back-to-back and performs a connected +// UDP dial against a UDP PacketConn listener, exercising the UDP socket paths. +func TestNet2_UDPEcho(t *testing.T) { + const ( + addrA = "10.0.0.1" + addrB = "10.0.0.2" + port = 9999 + ) + devA, netA, err := CreateNetTUNLneto([]netip.Addr{netip.MustParseAddr(addrA)}, nil, 1500) + if err != nil { + t.Fatal(err) + } + devB, netB, err := CreateNetTUNLneto([]netip.Addr{netip.MustParseAddr(addrB)}, nil, 1500) + if err != nil { + t.Fatal(err) + } + <-devA.Events() + <-devB.Events() + + var pumps sync.WaitGroup + pumps.Add(2) + go func() { defer pumps.Done(); pump(devA, devB) }() + go func() { defer pumps.Done(); pump(devB, devA) }() + defer func() { + devA.Close() + devB.Close() + pumps.Wait() + }() + + srv, err := netB.ListenUDPAddrPort(netip.AddrPortFrom(netip.MustParseAddr(addrB), port)) + if err != nil { + t.Fatal("listen udp:", err) + } + defer srv.Close() + + const msg = "hello over lneto udp" + srvDone := make(chan error, 1) + go func() { + buf := make([]byte, 512) + srv.SetDeadline(time.Now().Add(5 * time.Second)) + n, from, err := srv.ReadFrom(buf) + if err != nil { + srvDone <- err + return + } + _, err = srv.WriteTo(buf[:n], from) // echo back to sender + srvDone <- err + }() + + cli, err := netA.DialUDPAddrPort(netip.AddrPort{}, netip.AddrPortFrom(netip.MustParseAddr(addrB), port)) + if err != nil { + t.Fatal("dial udp:", err) + } + defer cli.Close() + cli.SetDeadline(time.Now().Add(5 * time.Second)) + if _, err := cli.Write([]byte(msg)); err != nil { + t.Fatal("udp write:", err) + } + echo := make([]byte, 512) + n, err := cli.Read(echo) + if err != nil { + t.Fatal("udp read echo:", err) + } + if string(echo[:n]) != msg { + t.Fatalf("udp echo mismatch: want %q got %q", msg, string(echo[:n])) + } + if err := <-srvDone; err != nil { + t.Fatal("udp server:", err) + } +} + +// pump forwards IP frames produced by src into dst until src is closed. +func pump(src, dst tun.Device) { + bufs := [][]byte{make([]byte, 2048)} + sizes := []int{0} + for { + n, err := src.Read(bufs, sizes, 0) + if err != nil { + return + } + if n == 0 || sizes[0] == 0 { + continue + } + out := make([]byte, sizes[0]) + copy(out, bufs[0][:sizes[0]]) + if _, err := dst.Write([][]byte{out}, 0); err != nil { + return + } + } +} + +// TestNet2_TCPEcho wires two Net2 instances back-to-back (one's egress is the +// other's ingress) and performs a TCP dial + echo, exercising the egress poll, +// the dial path, and the listener/accept path end-to-end for both IPv4 and IPv6. +func TestNet2_TCPEcho(t *testing.T) { + t.Run("ipv4", func(t *testing.T) { testTCPEcho(t, "10.0.0.1", "10.0.0.2") }) + t.Run("ipv6", func(t *testing.T) { testTCPEcho(t, "fd00::1", "fd00::2") }) +} + +func testTCPEcho(t *testing.T, addrA, addrB string) { + const port = 1234 + devA, netA, err := CreateNetTUNLneto([]netip.Addr{netip.MustParseAddr(addrA)}, nil, 1500) + if err != nil { + t.Fatal(err) + } + devB, netB, err := CreateNetTUNLneto([]netip.Addr{netip.MustParseAddr(addrB)}, nil, 1500) + if err != nil { + t.Fatal(err) + } + <-devA.Events() + <-devB.Events() + + var pumps sync.WaitGroup + pumps.Add(2) + go func() { defer pumps.Done(); pump(devA, devB) }() + go func() { defer pumps.Done(); pump(devB, devA) }() + + ln, err := netB.ListenTCPAddrPort(netip.AddrPortFrom(netip.MustParseAddr(addrB), port)) + if err != nil { + t.Fatal("listen:", err) + } + // Teardown order matters: stop ingress (close devices → pumps exit) BEFORE + // closing the listener, since tcp.Listener.Close is not synchronized against + // the stack's ingress demux in the lneto library (see review notes). + defer func() { + devA.Close() + devB.Close() + pumps.Wait() + ln.Close() + }() + + const msg = "hello over lneto tcp" + srvDone := make(chan error, 1) + go func() { + conn, err := ln.Accept() + if err != nil { + srvDone <- err + return + } + defer conn.Close() + buf := make([]byte, len(msg)) + conn.SetDeadline(time.Now().Add(5 * time.Second)) + var got int + for got < len(msg) { + n, err := conn.Read(buf[got:]) + if err != nil { + srvDone <- err + return + } + got += n + } + _, err = conn.Write(buf[:got]) // echo back + srvDone <- err + }() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + conn, err := netA.DialContextTCPAddrPort(ctx, netip.AddrPortFrom(netip.MustParseAddr(addrB), port)) + if err != nil { + t.Fatal("dial:", err) + } + defer conn.Close() + + conn.SetDeadline(time.Now().Add(5 * time.Second)) + if _, err := conn.Write([]byte(msg)); err != nil { + t.Fatal("write:", err) + } + echo := make([]byte, len(msg)) + got := 0 + for got < len(msg) { + n, err := conn.Read(echo[got:]) + if err != nil { + t.Fatal("read echo:", err) + } + got += n + } + if string(echo) != msg { + t.Fatalf("echo mismatch: want %q got %q", msg, string(echo)) + } + if err := <-srvDone; err != nil { + t.Fatal("server:", err) + } +} diff --git a/tun/netstack/lneto_tuncount.go b/tun/netstack/lneto_tuncount.go new file mode 100644 index 000000000..b499df924 --- /dev/null +++ b/tun/netstack/lneto_tuncount.go @@ -0,0 +1,30 @@ +//go:build debugheaplog + +package netstack + +import "fmt" + +// TUN-boundary packet counters, compiled in only under the debugheaplog tag +// (the start.sh -debug flag). They disambiguate a one-way data path: egress is +// what the stack hands to WireGuard (stack -> WG), ingress is what WireGuard +// feeds back into the stack (WG -> stack). A stuck SYN-SENT with egress>0 and +// ingress==0 means replies never reach the TUN; egress==0 means the stack is +// not draining its tx. Single counters are fine: js/wasm is single-threaded. +var ( + egressPkts, egressBytes int + ingressPkts, ingressBytes int +) + +// countEgress records one non-empty packet pulled from the stack toward WireGuard. +func countEgress(n int) { + egressPkts++ + egressBytes += n + fmt.Printf("[TUNCOUNT] egress pkts=%d bytes=%d last=%d\n", egressPkts, egressBytes, n) +} + +// countIngress records one IP packet handed from WireGuard into the stack. +func countIngress(n int) { + ingressPkts++ + ingressBytes += n + fmt.Printf("[TUNCOUNT] ingress pkts=%d bytes=%d last=%d\n", ingressPkts, ingressBytes, n) +} diff --git a/tun/netstack/lneto_tuncount_off.go b/tun/netstack/lneto_tuncount_off.go new file mode 100644 index 000000000..6bdabf943 --- /dev/null +++ b/tun/netstack/lneto_tuncount_off.go @@ -0,0 +1,7 @@ +//go:build !debugheaplog + +package netstack + +// No-op TUN-boundary counters for non-debug builds; calls are inlined away. +func countEgress(int) {} +func countIngress(int) {} diff --git a/tun/netstack/net.go b/tun/netstack/net.go new file mode 100644 index 000000000..e40af8d3e --- /dev/null +++ b/tun/netstack/net.go @@ -0,0 +1,407 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. + */ + +package netstack + +import ( + "context" + "errors" + "net" + "net/netip" + "regexp" + "strconv" + "time" +) + +// Net is the userspace networking API (Dial/Listen/DNS) layered over a backend +// [Stack]. It is backend-agnostic: [CreateNetTUN] wraps the gvisor backend and +// [CreateNetTUNLneto] wraps the lneto backend, but both return a *Net with the +// same surface so callers can swap backends transparently. +// +// The backend object also implements [golang.zx2c4.com/wireguard/tun.Device] +// (returned as the first value from the constructors) and is the data plane that +// moves IP packets to and from WireGuard. +type Net struct { + stack Stack +} + +// Stack is the backend-specific primitive set that [Net] delegates to. Both the +// gvisor netTun and the lneto stack implement it. Higher-level conveniences +// (the *net.TCPAddr/*net.UDPAddr/*PingAddr overloads, generic Dial/DialContext, +// LookupHost) live once on [Net] and are written in terms of these primitives. +type Stack interface { + DialContextTCPAddrPort(ctx context.Context, addr netip.AddrPort) (TCPConn, error) + DialTCPAddrPort(addr netip.AddrPort) (TCPConn, error) + ListenTCPAddrPort(addr netip.AddrPort) (TCPListener, error) + DialUDPAddrPort(laddr, raddr netip.AddrPort) (UDPConn, error) + ListenUDPAddrPort(laddr netip.AddrPort) (UDPConn, error) + DialPingAddr(laddr, raddr netip.Addr) (PingConn, error) + ListenPingAddr(laddr netip.Addr) (PingConn, error) + LookupContextHost(ctx context.Context, host string) ([]string, error) +} + +// TCPConn is the connection type returned by the TCP dial methods. gvisor's +// *gonet.TCPConn and lneto's TCP connection both satisfy it. +type TCPConn interface { + Close() error + CloseRead() error + CloseWrite() error + LocalAddr() net.Addr + Read(b []byte) (int, error) + RemoteAddr() net.Addr + SetDeadline(t time.Time) error + SetReadDeadline(t time.Time) error + SetWriteDeadline(t time.Time) error + Write(b []byte) (int, error) +} + +// UDPConn is the connection type returned by the UDP dial/listen methods. +type UDPConn interface { + Close() error + LocalAddr() net.Addr + Read(b []byte) (int, error) + ReadFrom(b []byte) (int, net.Addr, error) + RemoteAddr() net.Addr + SetDeadline(t time.Time) error + SetReadDeadline(t time.Time) error + SetWriteDeadline(t time.Time) error + Write(b []byte) (int, error) + WriteTo(b []byte, addr net.Addr) (int, error) +} + +// TCPListener is the listener type returned by the TCP listen methods. +type TCPListener interface { + Accept() (net.Conn, error) + Addr() net.Addr + Close() error + Shutdown() +} + +// PingConn is the ICMP "ping" connection returned by the ping methods. It is both a +// [net.Conn] (Read/Write against the dialed peer) and supports addressed I/O via +// ReadFrom/WriteTo. The gvisor backend returns a concrete implementation; the lneto +// backend does not implement ping and returns an error. +type PingConn interface { + net.Conn + ReadFrom(p []byte) (int, net.Addr, error) + WriteTo(p []byte, addr net.Addr) (int, error) +} + +// PingAddr is a [net.Addr] for the "ping" pseudo-networks. It wraps a bare +// [netip.Addr] (no port) and is backend-neutral. +type PingAddr struct{ addr netip.Addr } + +func (ia PingAddr) String() string { + return ia.addr.String() +} + +func (ia PingAddr) Network() string { + if ia.addr.Is4() { + return "ping4" + } else if ia.addr.Is6() { + return "ping6" + } + return "ping" +} + +func (ia PingAddr) Addr() netip.Addr { + return ia.addr +} + +func PingAddrFromAddr(addr netip.Addr) *PingAddr { + return &PingAddr{addr} +} + +// --- TCP --- + +func (n *Net) DialContextTCPAddrPort(ctx context.Context, addr netip.AddrPort) (TCPConn, error) { + return n.stack.DialContextTCPAddrPort(ctx, addr) +} + +func (n *Net) DialContextTCP(ctx context.Context, addr *net.TCPAddr) (TCPConn, error) { + if addr == nil { + return n.stack.DialContextTCPAddrPort(ctx, netip.AddrPort{}) + } + ip, _ := netip.AddrFromSlice(addr.IP) + return n.stack.DialContextTCPAddrPort(ctx, netip.AddrPortFrom(ip.Unmap(), uint16(addr.Port))) +} + +func (n *Net) DialTCPAddrPort(addr netip.AddrPort) (TCPConn, error) { + return n.stack.DialTCPAddrPort(addr) +} + +func (n *Net) DialTCP(addr *net.TCPAddr) (TCPConn, error) { + if addr == nil { + return n.stack.DialTCPAddrPort(netip.AddrPort{}) + } + ip, _ := netip.AddrFromSlice(addr.IP) + return n.stack.DialTCPAddrPort(netip.AddrPortFrom(ip.Unmap(), uint16(addr.Port))) +} + +func (n *Net) ListenTCPAddrPort(addr netip.AddrPort) (TCPListener, error) { + return n.stack.ListenTCPAddrPort(addr) +} + +func (n *Net) ListenTCP(addr *net.TCPAddr) (TCPListener, error) { + if addr == nil { + return n.stack.ListenTCPAddrPort(netip.AddrPort{}) + } + ip, _ := netip.AddrFromSlice(addr.IP) + return n.stack.ListenTCPAddrPort(netip.AddrPortFrom(ip.Unmap(), uint16(addr.Port))) +} + +// --- UDP --- + +func (n *Net) DialUDPAddrPort(laddr, raddr netip.AddrPort) (UDPConn, error) { + return n.stack.DialUDPAddrPort(laddr, raddr) +} + +func (n *Net) ListenUDPAddrPort(laddr netip.AddrPort) (UDPConn, error) { + return n.stack.ListenUDPAddrPort(laddr) +} + +func (n *Net) DialUDP(laddr, raddr *net.UDPAddr) (UDPConn, error) { + var la, ra netip.AddrPort + if laddr != nil { + ip, _ := netip.AddrFromSlice(laddr.IP) + la = netip.AddrPortFrom(ip.Unmap(), uint16(laddr.Port)) + } + if raddr != nil { + ip, _ := netip.AddrFromSlice(raddr.IP) + ra = netip.AddrPortFrom(ip.Unmap(), uint16(raddr.Port)) + } + return n.stack.DialUDPAddrPort(la, ra) +} + +func (n *Net) ListenUDP(laddr *net.UDPAddr) (UDPConn, error) { + return n.DialUDP(laddr, nil) +} + +// --- Ping --- + +func (n *Net) DialPingAddr(laddr, raddr netip.Addr) (PingConn, error) { + return n.stack.DialPingAddr(laddr, raddr) +} + +func (n *Net) ListenPingAddr(laddr netip.Addr) (PingConn, error) { + return n.stack.ListenPingAddr(laddr) +} + +func (n *Net) DialPing(laddr, raddr *PingAddr) (PingConn, error) { + var la, ra netip.Addr + if laddr != nil { + la = laddr.addr + } + if raddr != nil { + ra = raddr.addr + } + return n.stack.DialPingAddr(la, ra) +} + +func (n *Net) ListenPing(laddr *PingAddr) (PingConn, error) { + var la netip.Addr + if laddr != nil { + la = laddr.addr + } + return n.stack.ListenPingAddr(la) +} + +// --- DNS --- + +func (n *Net) LookupContextHost(ctx context.Context, host string) ([]string, error) { + return n.stack.LookupContextHost(ctx, host) +} + +func (n *Net) LookupHost(host string) ([]string, error) { + return n.stack.LookupContextHost(context.Background(), host) +} + +// --- Generic Dial --- + +var protoSplitter = regexp.MustCompile(`^(tcp|udp|ping)(4|6)?$`) + +func (n *Net) DialContext(ctx context.Context, network, address string) (net.Conn, error) { + if ctx == nil { + panic("nil context") + } + var acceptV4, acceptV6 bool + matches := protoSplitter.FindStringSubmatch(network) + if matches == nil { + return nil, &net.OpError{Op: "dial", Err: net.UnknownNetworkError(network)} + } else if len(matches[2]) == 0 { + acceptV4 = true + acceptV6 = true + } else { + acceptV4 = matches[2][0] == '4' + acceptV6 = !acceptV4 + } + var host string + var port int + if matches[1] == "ping" { + host = address + } else { + var sport string + var err error + host, sport, err = net.SplitHostPort(address) + if err != nil { + return nil, &net.OpError{Op: "dial", Err: err} + } + port, err = strconv.Atoi(sport) + if err != nil || port < 0 || port > 65535 { + return nil, &net.OpError{Op: "dial", Err: errNumericPort} + } + } + allAddr, err := n.LookupContextHost(ctx, host) + if err != nil { + return nil, &net.OpError{Op: "dial", Err: err} + } + var addrs []netip.AddrPort + for _, addr := range allAddr { + ip, err := netip.ParseAddr(addr) + if err == nil && ((ip.Is4() && acceptV4) || (ip.Is6() && acceptV6)) { + addrs = append(addrs, netip.AddrPortFrom(ip, uint16(port))) + } + } + if len(addrs) == 0 && len(allAddr) != 0 { + return nil, &net.OpError{Op: "dial", Err: errNoSuitableAddress} + } + + var firstErr error + for i, addr := range addrs { + select { + case <-ctx.Done(): + err := ctx.Err() + if err == context.Canceled { + err = errCanceled + } else if err == context.DeadlineExceeded { + err = errTimeout + } + return nil, &net.OpError{Op: "dial", Err: err} + default: + } + + dialCtx := ctx + var cancel context.CancelFunc + if deadline, hasDeadline := ctx.Deadline(); hasDeadline { + partialDeadline, err := partialDeadline(time.Now(), deadline, len(addrs)-i) + if err != nil { + if firstErr == nil { + firstErr = &net.OpError{Op: "dial", Err: err} + } + break + } + if partialDeadline.Before(deadline) { + dialCtx, cancel = context.WithDeadline(ctx, partialDeadline) + } + } + + var c net.Conn + switch matches[1] { + case "tcp": + c, err = n.DialContextTCPAddrPort(dialCtx, addr) + case "udp": + c, err = n.DialUDPAddrPort(netip.AddrPort{}, addr) + case "ping": + c, err = n.DialPingAddr(netip.Addr{}, addr.Addr()) + } + if cancel != nil { + // This cancel belongs to a function-local context so cancel required to avoid leaks. + cancel() + } + if err == nil { + return c, nil + } + if firstErr == nil { + firstErr = err + } + } + if firstErr == nil { + firstErr = &net.OpError{Op: "dial", Err: errMissingAddress} + } + return nil, firstErr +} + +func (n *Net) Dial(network, address string) (net.Conn, error) { + return n.DialContext(context.Background(), network, address) +} + +// --- shared helpers --- + +var ( + errNoSuchHost = errors.New("no such host") + errLameReferral = errors.New("lame referral") + errCannotUnmarshalDNSMessage = errors.New("cannot unmarshal DNS message") + errCannotMarshalDNSMessage = errors.New("cannot marshal DNS message") + errServerMisbehaving = errors.New("server misbehaving") + errInvalidDNSResponse = errors.New("invalid DNS response") + errNoAnswerFromDNSServer = errors.New("no answer from DNS server") + errServerTemporarilyMisbehaving = errors.New("server misbehaving") + errCanceled = errors.New("operation was canceled") + errTimeout = errors.New("i/o timeout") + errNumericPort = errors.New("port must be numeric") + errNoSuitableAddress = errors.New("no suitable address found") + errMissingAddress = errors.New("missing address") +) + +func isDomainName(s string) bool { + l := len(s) + if l == 0 || l > 254 || l == 254 && s[l-1] != '.' { + return false + } + last := byte('.') + nonNumeric := false + partlen := 0 + for i := 0; i < len(s); i++ { + c := s[i] + switch { + default: + return false + case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_': + nonNumeric = true + partlen++ + case '0' <= c && c <= '9': + partlen++ + case c == '-': + if last == '.' { + return false + } + partlen++ + nonNumeric = true + case c == '.': + if last == '.' || last == '-' { + return false + } + if partlen > 63 || partlen == 0 { + return false + } + partlen = 0 + } + last = c + } + if last == '-' || partlen > 63 { + return false + } + return nonNumeric +} + +func partialDeadline(now, deadline time.Time, addrsRemaining int) (time.Time, error) { + if deadline.IsZero() { + return deadline, nil + } + timeRemaining := deadline.Sub(now) + if timeRemaining <= 0 { + return time.Time{}, errTimeout + } + timeout := timeRemaining / time.Duration(addrsRemaining) + const saneMinimum = 2 * time.Second + if timeout < saneMinimum { + if timeRemaining < saneMinimum { + timeout = timeRemaining + } else { + timeout = saneMinimum + } + } + return now.Add(timeout), nil +} diff --git a/tun/netstack/net_debug.go b/tun/netstack/net_debug.go new file mode 100644 index 000000000..ceac04160 --- /dev/null +++ b/tun/netstack/net_debug.go @@ -0,0 +1,31 @@ +//go:build netstackdebug + +package netstack + +import ( + "os" + "sync" + "time" + + "github.com/soypat/lneto/x/xnet" +) + +var ( + _pcap xnet.CapturePrinter + _pcapOnce sync.Once +) + +func debugIPPacket(egress bool, pkt []byte) { + _pcapOnce.Do(func() { + _pcap.Configure(os.Stdout, xnet.CapturePrinterConfig{ + NamespaceWidth: 3, + TimePrecision: 4, + Now: time.Now, + }) + }) + if egress { + _pcap.PrintIP("OUT", pkt) + } else { + _pcap.PrintIP("IN ", pkt) + } +} diff --git a/tun/netstack/net_debugoff.go b/tun/netstack/net_debugoff.go new file mode 100644 index 000000000..2b42ed8bb --- /dev/null +++ b/tun/netstack/net_debugoff.go @@ -0,0 +1,5 @@ +//go:build !netstackdebug + +package netstack + +func debugIPPacket(egress bool, pkt []byte) {} diff --git a/tun/netstack/net_gvisor.go b/tun/netstack/net_gvisor.go new file mode 100644 index 000000000..464af07d8 --- /dev/null +++ b/tun/netstack/net_gvisor.go @@ -0,0 +1,15 @@ +//go:build !wglneto + +package netstack + +import ( + "net/netip" + + "golang.zx2c4.com/wireguard/tun" +) + +// CreateNetTUN builds the default gvisor-backed netstack TUN device. Build with +// -tags wglneto to select the lneto backend instead (see CreateNetTUNLneto). +func CreateNetTUN(localAddresses, dnsServers []netip.Addr, mtu int) (tun.Device, *Net, error) { + return CreateNetTUNGvisor(localAddresses, dnsServers, mtu) +} diff --git a/tun/netstack/net_lneto.go b/tun/netstack/net_lneto.go new file mode 100644 index 000000000..f860870e0 --- /dev/null +++ b/tun/netstack/net_lneto.go @@ -0,0 +1,15 @@ +//go:build wglneto + +package netstack + +import ( + "net/netip" + + "golang.zx2c4.com/wireguard/tun" +) + +// CreateNetTUN builds the lneto-backed netstack TUN device. This is the +// implementation selected when building with -tags wglneto. +func CreateNetTUN(localAddresses, dnsServers []netip.Addr, mtu int) (tun.Device, *Net, error) { + return CreateNetTUNLneto(localAddresses, dnsServers, mtu) +}