//go:build !tinygo && linux package main import ( "bytes" "encoding/hex" "errors" "flag" "fmt" "log" "log/slog" "math" "net" "net/netip" "os" "runtime" "runtime/pprof" "strings" "time" "github.com/soypat/lneto" "github.com/soypat/lneto/dns" "github.com/soypat/lneto/ethernet" "github.com/soypat/lneto/http/httpraw" "github.com/soypat/lneto/internal" "github.com/soypat/lneto/internal/ltesto" "github.com/soypat/lneto/internet/pcap" "github.com/soypat/lneto/ipv4" "github.com/soypat/lneto/tcp" "github.com/soypat/lneto/x/xnet" ) const pollTime = 5 * time.Millisecond var softRand = time.Now().Unix() func main() { err := run() if err != nil { fmt.Println(err) os.Exit(1) } fmt.Println("success") } func run() (err error) { var ( flagInterface = "tap0" flagUseHTTP = false flagHostToResolve = "" flagRequestedIP = "" flagLocalTCPPort = 0 flagDoNTP = false flagNoPcap = false flagPprof = false ) flag.StringVar(&flagInterface, "i", flagInterface, "Interface to use. Either tap* or the name of an existing interface to bridge to.") flag.BoolVar(&flagUseHTTP, "ihttp", flagUseHTTP, "Use HTTP tap interface.") flag.StringVar(&flagHostToResolve, "host", flagHostToResolve, "Hostname to resolve via DNS.") flag.StringVar(&flagRequestedIP, "addr", flagRequestedIP, "IP address to request via DHCP.") flag.IntVar(&flagLocalTCPPort, "port", flagLocalTCPPort, "Local TCP source port to use. Zero chooses a pseudo-random port.") flag.BoolVar(&flagDoNTP, "ntp", flagDoNTP, "Do NTP round and print result time") flag.BoolVar(&flagNoPcap, "nopcap", flagNoPcap, "Disable pcap logging.") flag.BoolVar(&flagPprof, "pprof", flagPprof, "Enable CPU profiling.") flag.Usage = func() { fmt.Fprintf(os.Stderr, "xcurl is a curl-like command line utility to test lneto's networking features.\n") flag.PrintDefaults() } flag.Parse() if flagPprof { var b bytes.Buffer pprof.StartCPUProfile(&b) defer func() { pprof.StopCPUProfile() os.WriteFile("xnet.pprof", b.Bytes(), 0777) }() } _, err = dns.NewName(flagHostToResolve) if err != nil { flag.Usage() return err } reqAddr := [4]byte{192, 168, 1, 96} requestedAddrSet := false if flagRequestedIP != "" { addr, err := netip.ParseAddr(flagRequestedIP) if err != nil || !addr.Is4() { flag.Usage() return fmt.Errorf("invalid requested IPv4 address %q", flagRequestedIP) } reqAddr = addr.As4() requestedAddrSet = true } if flagLocalTCPPort < 0 || flagLocalTCPPort > math.MaxUint16 { flag.Usage() return fmt.Errorf("invalid local TCP port %d", flagLocalTCPPort) } fmt.Println("softrand", softRand) var iface ltesto.Interface if flagUseHTTP { iface = ltesto.NewHTTPTapClient("http://127.0.0.1:7070") } else { if strings.HasPrefix(flagInterface, "tap") { tap, err := internal.NewTap(flagInterface, netip.MustParsePrefix("192.168.1.1/24")) if err != nil { return err } iface = tap } else { bridge, err := internal.NewBridge(flagInterface) if err != nil { return err } err = bridge.SetReadTimeout(5 * time.Millisecond) if err != nil { return err } iface = bridge } } defer iface.Close() nicHW, err := iface.HardwareAddress6() if err != nil { return err } brHW := nicHW mtu, err := iface.MTU() if err != nil { return err } nicAddr, err := iface.IPMask() if err != nil { return err } fmt.Println("NIC hardware address:", net.HardwareAddr(nicHW[:]).String(), "bridgeHW:", net.HardwareAddr(brHW[:]).String(), "mtu:", mtu, "addr:", nicAddr.String()) var stack xnet.StackAsync err = stack.Reset(xnet.StackConfig{ Hostname: "xnet-test", RandSeed: softRand, HardwareAddress: brHW, MTU: uint16(mtu), MaxActiveTCPPorts: 1, }) if err != nil { return err } // Loop goroutine. go func() { lastAction := time.Now() buf := make([]byte, math.MaxUint16) // Generic-receive Offload (GRO) can aggregate packets. var cap pcap.PacketBreakdown cap.SubfieldLimit = 30 // For chunky DNS. var frames []pcap.Frame pf := pcap.Formatter{ FilterClasses: []pcap.FieldClass{pcap.FieldClassFlags, pcap.FieldClassOperation, pcap.FieldClassDst, pcap.FieldClassSrc, pcap.FieldClassAddress, pcap.FieldClassTimestamp, pcap.FieldClassDNSName}, SubfieldLimit: 30, } var pfbuf []byte logFrames := func(context string, pkt []byte) error { if flagNoPcap { return nil } frames, err = cap.CaptureEthernet(frames[:0], pkt, 0) if err != nil { pkt := hex.EncodeToString(pkt) slog.Error(err.Error(), slog.Any("pkt", pkt)) return err } pfbuf = fmt.Appendf(pfbuf[:0], "%-3s %3d", context, len(pkt)) pfbuf = append(pfbuf, ' ', '[') pfbuf, err = pf.FormatFrames(pfbuf, frames, pkt) addr := stack.Addr4() pfbuf = bytes.ReplaceAll(pfbuf, addr[:], []byte("us")) pfbuf = bytes.ReplaceAll(pfbuf, ethernet.AppendAddr(nil, stack.HardwareAddr()), []byte("us")) pfbuf = append(pfbuf, ']', '\n') if err != nil { return err } _, err = os.Stdout.Write(pfbuf) return err } for { nwrite, err := stack.EgressEthernet(buf[:]) if err != nil { log.Println("ERR:ENCAPSULATE", err) } else if nwrite > 0 { err = logFrames("OUT", buf[:nwrite]) if err != nil { log.Println("ERR:OUTLOG", err) } n, err := iface.Write(buf[:nwrite]) if err != nil { log.Fatal("groutine encapsulate:", err) } else if n != nwrite { log.Fatalf("mismatch written bytes %d!=%d", nwrite, n) } } clear(buf[:nwrite]) // Poll before read if interface supports it, to avoid blocking indefinitely. ready, err := tryPoll(iface, pollTime) if err != nil { log.Fatal("goroutine poll:", err) } if !ready { continue } nread, err := iface.Read(buf) if err != nil { log.Fatal("groutine read:", err) } else if nread > 0 { err = stack.IngressEthernet(buf[:nread]) if !errors.Is(err, lneto.ErrPacketDrop) { // Only skip logging packet in case of dropped packet. err = logFrames("IN", buf[:nread]) if err != nil { log.Println("ERR:INLOG", err) } } } clear(buf[:nread]) if nread == 0 && nwrite == 0 && time.Since(lastAction) > 4*time.Second { time.Sleep(5 * time.Millisecond) } else { lastAction = time.Now() runtime.Gosched() } } }() rstack := stack.StackRetrying(stackBackoff) const ( dhcpTimeout = 6 * time.Second dhcpRetries = 2 ) timeDHCP := timer("DHCP request completed") results, err := rstack.DoDHCPv4(reqAddr, dhcpTimeout, dhcpRetries) if err != nil { return fmt.Errorf("DHCP failed: %w", err) } timeDHCP() if requestedAddrSet && results.AssignedAddr4 != reqAddr { return fmt.Errorf("DHCP assigned %s, not requested %s", netip.AddrFrom4(results.AssignedAddr4), netip.AddrFrom4(reqAddr)) } err = stack.AssimilateDHCPResults(results) if err != nil { return fmt.Errorf("assimilating DHCP results: %w", err) } slog.Info("dhcp-complete", slog.String("assignedIP", string(ipv4.AppendFormatAddr(nil, results.AssignedAddr4))), slog.String("routerIP", results.Router.String()), slog.Any("DNS", results.DNSServers), slog.Any("subnet", results.Subnet.String())) const ( arpTimeout = 2 * time.Second arpRetries = 2 ) const ( internetTimeout = 3 * time.Second internetRetries = 2 ) timeResolveRouterHW := timer("Router ARP resolution") routerHw, err := rstack.DoResolveHardwareAddress6(results.Router, arpTimeout, arpRetries) if err != nil { return fmt.Errorf("ARP resolution of router failed: %w", err) } timeResolveRouterHW() stack.SetGatewayHardwareAddr(routerHw) if flagDoNTP { timeLookupNTP := timer("NTP IP lookup") const ntpHost = "pool.ntp.org" addrs, err := rstack.DoLookupIP(ntpHost, internetTimeout, internetRetries) if err != nil { return fmt.Errorf("NTP address lookup of %q failed: %w", ntpHost, err) } timeLookupNTP() timeNTP := timer("NTP exchange") offset, err := rstack.DoNTP(addrs[0], internetTimeout, internetRetries) if err != nil { return fmt.Errorf("NTP address lookup of %q failed: %w", ntpHost, err) } timeNTP() relative := "behind" if offset < 0 { relative = "ahead" } fmt.Println("NTP completed. You are", offset.Abs().String(), relative, "of the NTP server") } timeResolveIP := timer("resolve " + flagHostToResolve) addrs, err := rstack.DoLookupIP(flagHostToResolve, internetTimeout, internetRetries) if err != nil { return fmt.Errorf("DNS of host %q failed: %w", flagHostToResolve, err) } timeResolveIP() fmt.Printf("DNS resolution of %q complete and resolved to %v\n", flagHostToResolve, addrs) var conn tcp.Conn conn.Configure(tcp.ConnConfig{ RxBuf: make([]byte, mtu), TxBuf: make([]byte, mtu), TxPacketQueueSize: 3, RWBackoff: tcpBackoff, }) timeHTTPCreate := timer("create HTTP GET request") var hdr httpraw.Header hdr.SetMethod("GET") hdr.SetRequestURI("/") hdr.SetProtocol("HTTP/1.1") hdr.Set("Host", flagHostToResolve) hdr.Set("User-Agent", "lneto") hdr.Set("Accept-Language", "en-US,en;q=0.5") hdr.Set("Connection", "close") // Encourage server to close connection after it finishes sending response. req, err := hdr.AppendRequest(nil) if err != nil { return err } timeHTTPCreate() timeTCPDial := timer("TCP dial (handshake)") const tcpDialTimeout = 8 * time.Second // Was 60 * time.Minute causing 3.6s sleep per iteration! target := netip.AddrPortFrom(addrs[0], 80) localPort := uint16(flagLocalTCPPort) if localPort == 0 { localPort = uint16(softRand&0xefff) + 1024 } fmt.Printf("TCP target %s local-port=%d\n", target, localPort) err = rstack.DoDialTCP(&conn, localPort, target, tcpDialTimeout, internetRetries) if err != nil { return fmt.Errorf("TCP failed: %w", err) } timeTCPDial() timeHTTPSend := timer("send HTTP request") conn.SetDeadline(time.Now().Add(internetTimeout)) _, err = conn.Write(req) if err != nil { return err } err = conn.Flush() if err != nil { return err } timeHTTPSend() timeHTTPRcv := timer("recv http request") rxbuf := make([]byte, 2048) var page []byte for { var n int n, err = conn.Read(rxbuf) page = append(page, rxbuf[:n]...) if err != nil { break } ptrimmed := bytes.TrimSpace(page) if bytes.EqualFold(ptrimmed[len(ptrimmed)-7:], []byte("")) { // We've received the last part of the HTML. we're done. break } } if len(page) == 0 { return err } timeHTTPRcv() os.Stdout.Write(page) return nil } func clear(buf []byte) { for i := range buf { buf[i] = 0 } } func timer(context string) func() { start := time.Now() return func() { elapsed := time.Since(start) fmt.Printf("[%s] %s\n", prettyDuration(elapsed), context) } } func prettyDuration(d time.Duration) string { switch { case d < time.Microsecond: // Print as is. case d < time.Millisecond: d = d.Round(time.Microsecond) case d < time.Second: d = d.Round(time.Millisecond) case d < 10*time.Second: d = d.Round(100 * time.Millisecond) case d < 10*time.Minute: d = d.Round(1000 * time.Millisecond) case d < time.Hour: d = d.Round(time.Minute) } return d.String() } func tryPoll(iface ltesto.Interface, poll time.Duration) (dataMayBeReady bool, _ error) { if poller, ok := iface.(interface { Poll(time.Duration) (bool, error) }); ok { ready, err := poller.Poll(poll) return ready, err } dataMayBeReady = true return dataMayBeReady, nil } func stackBackoff(consecutiveBackoffs uint) time.Duration { if consecutiveBackoffs < 10 { return time.Millisecond } return 10 * time.Millisecond } func tcpBackoff(consecutiveBackoffs uint) time.Duration { const ( minWait = uint32(time.Microsecond) maxWait = 5 * uint32(time.Millisecond) maxShift = 22 _overflowCheck = minWait << maxShift ) shifted := minWait << min(consecutiveBackoffs, maxShift) wait := min(shifted, maxWait) return time.Duration(wait) }