diff --git a/.gitignore b/.gitignore index 3d00455..0be8894 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,10 @@ vendor/ *.so *.dylib *.hex + +# Profiling +*.pprof + # example binaries. /tap /stack @@ -46,6 +50,7 @@ local *.zip *.gz *.tar +*.txt # If running a python script. */__pycache__/* diff --git a/README.md b/README.md index 77cc069..4b0949b 100644 --- a/README.md +++ b/README.md @@ -100,5 +100,80 @@ Now run the application you wish to test without elevated privilidges. Stackbasi go run ./examples/stackbasic ``` -**Wireshark**: Using the provided method of interfacing mean's you'll always be able to easily reach the TAP interface on your machine over HTTP from any process, be it Python or Go. To visualize the packets over the interface we suggest using wireshark and selecting the `tap0` interface which will show all activity over the HTTP TAP interface created with [`./examples/tap`](./examples/tap/main.go). + +### Wireshark and Packet Capture API +Using the provided method of interfacing mean's you'll always be able to easily reach the TAP interface on your machine over HTTP from any process, be it Python or Go. To visualize the packets over the interface we suggest using **Wireshark** and selecting the `tap0` interface which will show all activity over the HTTP TAP interface created with [`./examples/tap`](./examples/tap/main.go). + +Alternatively there's the [`internet/pcap`](./internet/pcap) package that does the same thing as Wireshark but as a Go API. Here's the result of running xnet example with pcap logging: + + +```log +go run ./examples/xnet -httpget -host google.com -ihttp -ntp +softrand 1767229198 +NIC hardware address: d8:5e:d3:43:03:eb bridgeHW: d8:5e:d3:43:03:eb mtu: 1500 addr: 192.168.1.53/24 +OUT 328 [Ethernet len=14; destination=ff:ff:ff:ff:ff:ff; source=us | IPv4 len=20; (Type of Service)=0x00; flags=0x4000; source=us; destination=255.255.255.255 | UDP [RFC768] len=8; (Source port)=68; (Destination port)=67 | DHCPv4 len=285; op=1; Flags=0x0000; (Client Address)=us; (Offered Address)=us; (Server Next Address)=255.255.255.255; (Relay Agent Address)=us; (Client Hardware Address)=d85e:d343:3eb::] +IN 98 [Ethernet len=14; destination=us; source=e8:4d:74:9f:61:4a | IPv4 len=20; (Type of Service)=0x00; flags=0x4000; source=192.168.1.1; destination=192.168.1.53 | ICMP [RFC792] len=64] +IN 98 [Ethernet len=14; destination=us; source=e8:4d:74:9f:61:4a | IPv4 len=20; (Type of Service)=0x00; flags=0x4000; source=192.168.1.1; destination=192.168.1.53 | ICMP [RFC792] len=64] +IN 98 [Ethernet len=14; destination=us; source=e8:4d:74:9f:61:4a | IPv4 len=20; (Type of Service)=0x00; flags=0x4000; source=192.168.1.1; destination=192.168.1.53 | ICMP [RFC792] len=64] +IN 98 [Ethernet len=14; destination=us; source=e8:4d:74:9f:61:4a | IPv4 len=20; (Type of Service)=0x00; flags=0x4000; source=192.168.1.1; destination=192.168.1.53 | ICMP [RFC792] len=64] +IN 98 [Ethernet len=14; destination=us; source=e8:4d:74:9f:61:4a | IPv4 len=20; (Type of Service)=0x00; flags=0x4000; source=192.168.1.1; destination=192.168.1.53 | ICMP [RFC792] len=64] +IN 98 [Ethernet len=14; destination=us; source=e8:4d:74:9f:61:4a | IPv4 len=20; (Type of Service)=0x00; flags=0x4000; source=192.168.1.1; destination=192.168.1.53 | ICMP [RFC792] len=64] +IN 60 [Ethernet len=14; destination=ff:ff:ff:ff:ff:ff; source=e8:4d:74:9f:61:4a | ARP len=28; op=1; (Sender hardware address)=e8:4d:74:9f:61:4a; (Sender protocol address)=192.168.1.1; (Target hardware address)=00:00:00:00:00:00; (Target protocol address)=192.168.1.53] +IN 590 [Ethernet len=14; destination=us; source=e8:4d:74:9f:61:4a | IPv4 len=20; (Type of Service)=0x00; flags=0x0000; source=192.168.1.1; destination=192.168.1.53 | UDP [RFC768] len=8; (Source port)=67; (Destination port)=68 | DHCPv4 len=273; op=2; Flags=0x0000; (Client Address)=us; (Offered Address)=192.168.1.53; (Server Next Address)=us; (Relay Agent Address)=us; (Client Hardware Address)=d85e:d343:3eb::] +OUT 326 [Ethernet len=14; destination=ff:ff:ff:ff:ff:ff; source=us | IPv4 len=20; (Type of Service)=0x00; flags=0x4000; source=us; destination=255.255.255.255 | UDP [RFC768] len=8; (Source port)=68; (Destination port)=67 | DHCPv4 len=283; op=1; Flags=0x0000; (Client Address)=us; (Offered Address)=192.168.1.53; (Server Next Address)=us; (Relay Agent Address)=us; (Client Hardware Address)=d85e:d343:3eb::] +IN 590 [Ethernet len=14; destination=us; source=e8:4d:74:9f:61:4a | IPv4 len=20; (Type of Service)=0x00; flags=0x0000; source=192.168.1.1; destination=192.168.1.53 | UDP [RFC768] len=8; (Source port)=67; (Destination port)=68 | DHCPv4 len=273; op=2; Flags=0x0000; (Client Address)=us; (Offered Address)=192.168.1.53; (Server Next Address)=us; (Relay Agent Address)=us; (Client Hardware Address)=d85e:d343:3eb::] +[119ms] DHCP request completed +2025/12/31 21:59:58 INFO dhcp-complete assignedIP=192.168.1.53 routerIP=192.168.1.1 DNS=[192.168.1.1] subnet=192.168.1.0/24 +OUT 42 [Ethernet len=14; destination=ff:ff:ff:ff:ff:ff; source=us | ARP len=28; op=1; (Sender hardware address)=us; (Sender protocol address)=us; (Target hardware address)=00:00:00:00:00:00; (Target protocol address)=192.168.1.1] +IN 60 [Ethernet len=14; destination=us; source=e8:4d:74:9f:61:4a | ARP len=28; op=2; (Sender hardware address)=e8:4d:74:9f:61:4a; (Sender protocol address)=192.168.1.1; (Target hardware address)=us; (Target protocol address)=us] +[1.1s] Router ARP resolution +IN 60 [Ethernet len=14; destination=us; source=e8:4d:74:9f:61:4a | ARP len=28; op=1; (Sender hardware address)=e8:4d:74:9f:61:4a; (Sender protocol address)=192.168.1.1; (Target hardware address)=00:00:00:00:00:00; (Target protocol address)=us] +OUT 42 [Ethernet len=14; destination=e8:4d:74:9f:61:4a; source=us | ARP len=28; op=2; (Sender hardware address)=us; (Sender protocol address)=us; (Target hardware address)=e8:4d:74:9f:61:4a; (Target protocol address)=192.168.1.1] +OUT 83 [Ethernet len=14; destination=e8:4d:74:9f:61:4a; source=us | IPv4 len=20; (Type of Service)=0x00; flags=0x4000; source=us; destination=192.168.1.1 | UDP [RFC768] len=8; (Source port)=57216; (Destination port)=53 | DNS len=41] +IN 147 [Ethernet len=14; destination=us; source=e8:4d:74:9f:61:4a | IPv4 len=20; (Type of Service)=0x00; flags=0x4000; source=192.168.1.1; destination=us | UDP [RFC768] len=8; (Source port)=53; (Destination port)=57216 | DNS len=105] +[3.5s] NTP IP lookup +OUT 90 [Ethernet len=14; destination=e8:4d:74:9f:61:4a; source=us | IPv4 len=20; (Type of Service)=0x00; flags=0x4000; source=us; destination=170.210.222.10 | UDP [RFC768] len=8; (Source port)=1023; (Destination port)=123 | NTP len=48; (Reference Time)=1900-01-01T00:00:00; (Origin Time)=1900-01-01T00:00:00; (Receive Time)=1900-01-01T00:00:00; (Transit Time)=1900-01-01T00:00:00] +IN 90 [Ethernet len=14; destination=us; source=e8:4d:74:9f:61:4a | IPv4 len=20; (Type of Service)=0x00; flags=0x0000; source=170.210.222.10; destination=us | UDP [RFC768] len=8; (Source port)=123; (Destination port)=1023 | NTP len=48; (Reference Time)=2026-01-01T00:43:04; (Origin Time)=1900-01-01T00:00:00; (Receive Time)=2026-01-01T01:00:03; (Transit Time)=2026-01-01T01:00:03] +[786ms] NTP exchange +NTP completed. You are 11.493283ms ahead of the NTP server +OUT 81 [Ethernet len=14; destination=e8:4d:74:9f:61:4a; source=us | IPv4 len=20; (Type of Service)=0x00; flags=0x4000; source=us; destination=192.168.1.1 | UDP [RFC768] len=8; (Source port)=56316; (Destination port)=53 | DNS len=39] +IN 97 [Ethernet len=14; destination=us; source=e8:4d:74:9f:61:4a | IPv4 len=20; (Type of Service)=0x00; flags=0x4000; source=192.168.1.1; destination=us | UDP [RFC768] len=8; (Source port)=53; (Destination port)=56316 | DNS len=55] +[1s] resolve google.com +DNS resolution of "google.com" complete and resolved to [142.251.129.142] +[10µs] create HTTP GET request +IN 98 [Ethernet len=14; destination=us; source=e8:4d:74:9f:61:4a | IPv4 len=20; (Type of Service)=0x00; flags=0x4000; source=192.168.1.1; destination=us | ICMP [RFC792] len=64] +OUT 58 [Ethernet len=14; destination=e8:4d:74:9f:61:4a; source=us | IPv4 len=20; (Type of Service)=0x00; flags=0x4000; source=us; destination=142.251.129.142 | TCP [RFC9293] len=24; (Source port)=51982; (Destination port)=80; flags=SYN] +IN 98 [Ethernet len=14; destination=us; source=e8:4d:74:9f:61:4a | IPv4 len=20; (Type of Service)=0x00; flags=0x4000; source=192.168.1.1; destination=us | ICMP [RFC792] len=64] +IN 60 [Ethernet len=14; destination=us; source=e8:4d:74:9f:61:4a | IPv4 len=20; (Type of Service)=0x00; flags=0x4000; source=142.251.129.142; destination=us | TCP [RFC9293] len=24; (Source port)=80; (Destination port)=51982; flags=SYN,ACK | payload? len=2] +OUT 54 [Ethernet len=14; destination=e8:4d:74:9f:61:4a; source=us | IPv4 len=20; (Type of Service)=0x00; flags=0x4000; source=us; destination=142.251.129.142 | TCP [RFC9293] len=20; (Source port)=51982; (Destination port)=80; flags=ACK] +[646ms] TCP dial (handshake) +[5µs] send HTTP request +OUT 161 [Ethernet len=14; destination=e8:4d:74:9f:61:4a; source=us | IPv4 len=20; (Type of Service)=0x00; flags=0x4000; source=us; destination=142.251.129.142 | TCP [RFC9293] len=20; (Source port)=51982; (Destination port)=80; flags=PSH,ACK | HTTP len=107] +IN 60 [Ethernet len=14; destination=us; source=e8:4d:74:9f:61:4a | IPv4 len=20; (Type of Service)=0x00; flags=0x0000; source=142.251.129.142; destination=us | TCP [RFC9293] len=20; (Source port)=80; (Destination port)=51982; flags=ACK | payload? len=6] +IN 846 [Ethernet len=14; destination=us; source=e8:4d:74:9f:61:4a | IPv4 len=20; (Type of Service)=0x00; flags=0x0000; source=142.251.129.142; destination=us | TCP [RFC9293] len=20; (Source port)=80; (Destination port)=51982; flags=PSH,ACK | HTTP len=792] +IN 60 [Ethernet len=14; destination=us; source=e8:4d:74:9f:61:4a | IPv4 len=20; (Type of Service)=0x00; flags=0x0000; source=142.251.129.142; destination=us | TCP [RFC9293] len=20; (Source port)=80; (Destination port)=51982; flags=FIN,ACK | payload? len=6] +IN 804 [Ethernet len=14; destination=us; source=e8:4d:74:9f:61:4a | IPv4 len=20; (Type of Service)=0x00; flags=0x0000; source=142.251.129.142; destination=us | TCP [RFC9293] len=20; (Source port)=80; (Destination port)=51982; flags=ACK | HTTP len=750] +OUT 54 [Ethernet len=14; destination=e8:4d:74:9f:61:4a; source=us | IPv4 len=20; (Type of Service)=0x00; flags=0x4000; source=us; destination=142.251.129.142 | TCP [RFC9293] len=20; (Source port)=51982; (Destination port)=80; flags=ACK] +[2.9s] recv http request +HTTP/1.1 301 Moved Permanently +Location: http://www.google.com/ +Content-Type: text/html; charset=UTF-8 +Content-Security-Policy-Report-Only: object-src 'none';base-uri 'self';script-src 'nonce-v-ysoE0WjLlAMlo2ek5UrA' 'strict-dynamic' 'report-sample' 'unsafe-eval' 'unsafe-inline' https: http:;report-uri https://csp.withgoogle.com/csp/gws/other-hp +Date: Thu, 01 Jan 2026 01:00:08 GMT +Expires: Sat, 31 Jan 2026 01:00:08 GMT +Cache-Control: public, max-age=2592000 +Server: gws +Content-Length: 219 +X-XSS-Protection: 0 +X-Frame-Options: SAMEORIGIN +Connection: close + + +301 Moved +

301 Moved

+The document has moved +here. + +success +``` \ No newline at end of file diff --git a/examples/xnet/main.go b/examples/xnet/main.go index 7667f9d..a26f017 100644 --- a/examples/xnet/main.go +++ b/examples/xnet/main.go @@ -13,6 +13,7 @@ import ( "net/netip" "os" "runtime" + "runtime/pprof" "strings" "time" @@ -47,6 +48,7 @@ func run() (err error) { flagDoNTP = false flagHTTPGet = false flagNoPcap = false + flagPprof = false ) flag.BoolVar(&flagHTTPGet, "httpget", flagHTTPGet, "Do an HTTP GET request ") flag.StringVar(&flagInterface, "i", flagInterface, "Interface to use. Either tap* or the name of an existing interface to bridge to.") @@ -55,7 +57,16 @@ func run() (err error) { flag.StringVar(&flagRequestedIP, "addr", flagRequestedIP, "IP address to request via DHCP.") 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.Parse() + if flagPprof { + var b bytes.Buffer + pprof.StartCPUProfile(&b) + defer func() { + pprof.StopCPUProfile() + os.WriteFile("xnet.pprof", b.Bytes(), 0777) + }() + } fmt.Println("softrand", softRand) _, err = dns.NewName(flagHostToResolve) if err != nil { @@ -77,6 +88,10 @@ func run() (err error) { if err != nil { return err } + err = bridge.SetReadTimeout(5 * time.Millisecond) + if err != nil { + return err + } iface = bridge } } @@ -108,6 +123,7 @@ func run() (err error) { if err != nil { return err } + // Loop goroutine. go func() { lastAction := time.Now() @@ -115,7 +131,7 @@ func run() (err error) { var cap pcap.PacketBreakdown var frames []pcap.Frame pf := pcap.Formatter{ - FilterClasses: []pcap.FieldClass{pcap.FieldClassFlags, pcap.FieldClassOperation, pcap.FieldClassDst, pcap.FieldClassSrc, pcap.FieldClassAddress}, + FilterClasses: []pcap.FieldClass{pcap.FieldClassFlags, pcap.FieldClassOperation, pcap.FieldClassDst, pcap.FieldClassSrc, pcap.FieldClassAddress, pcap.FieldClassTimestamp}, } var pfbuf []byte logFrames := func(context string, pkt []byte) error { @@ -158,6 +174,14 @@ func run() (err error) { } clear(buf[:nwrite]) + // Poll before read if interface supports it, to avoid blocking indefinitely. + ready, err := tryPoll(iface, 5*time.Millisecond) + if err != nil { + log.Fatal("goroutine poll:", err) + } + if !ready { + continue + } nread, err := iface.Read(buf) if err != nil { log.Fatal("groutine read:", err) @@ -181,16 +205,18 @@ func run() (err error) { } }() - rstack := stack.StackRetrying() + rstack := stack.StackRetrying(5 * time.Millisecond) const ( dhcpTimeout = 6 * time.Second dhcpRetries = 2 ) + timeDHCP := timer("DHCP request completed") results, err := rstack.DoDHCPv4([4]byte{192, 168, 1, 96}, dhcpTimeout, dhcpRetries) if err != nil { return fmt.Errorf("DHCP failed: %w", err) } + timeDHCP() err = stack.AssimilateDHCPResults(results) if err != nil { return fmt.Errorf("assimilating DHCP results: %w", err) @@ -204,31 +230,39 @@ func run() (err error) { 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.SetGateway6(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{ @@ -237,6 +271,7 @@ func run() (err error) { TxPacketQueueSize: 3, }) if flagHTTPGet { + timeHTTPCreate := timer("create HTTP GET request") var hdr httpraw.Header hdr.SetMethod("GET") hdr.SetRequestURI("/") @@ -244,22 +279,31 @@ func run() (err error) { 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 } - const tcpDebugTimeout = 60 * time.Minute + 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) - err = rstack.DoDialTCP(&conn, uint16(softRand&0xefff)+1024, target, tcpDebugTimeout, internetRetries) + err = rstack.DoDialTCP(&conn, uint16(softRand&0xefff)+1024, 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) + timeHTTPSend() + timeHTTPRcv := timer("recv http request") if err != nil { return err } rxbuf := make([]byte, 2048) + var page []byte for { var n int @@ -268,10 +312,16 @@ func run() (err error) { 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 @@ -282,3 +332,40 @@ func clear(buf []byte) { 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 +} diff --git a/internal/ltesto/httptap.go b/internal/ltesto/httptap.go index f016390..0cca450 100644 --- a/internal/ltesto/httptap.go +++ b/internal/ltesto/httptap.go @@ -26,6 +26,16 @@ type Interface interface { IPMask() (netip.Prefix, error) } +type HTTPTapClient struct { + c http.Client + infoURL string + recvurl string + sendurl string + ip netip.Prefix + hwaddr [6]byte + buf []byte +} + var _ Interface = (*HTTPTapClient)(nil) // NewHTTPTapClient returns a HTTPTapClient ready for use. @@ -65,12 +75,7 @@ func (h *HTTPTapClient) ensureMTU() (err error) { err = fmt.Errorf("unable to get MTU from server: %w", err) } }() - resp, err := h.c.Get(h.infoURL) - if err != nil { - return err - } - var info tapInfo - err = json.NewDecoder(resp.Body).Decode(&info) + info, err := h.info() if err != nil { return err } else if info.MTU <= minMTU { @@ -88,14 +93,14 @@ func (h *HTTPTapClient) ensureMTU() (err error) { return nil } -type HTTPTapClient struct { - c http.Client - infoURL string - recvurl string - sendurl string - ip netip.Prefix - hwaddr [6]byte - buf []byte +func (h *HTTPTapClient) info() (tapInfo, error) { + resp, err := h.c.Get(h.infoURL) + if err != nil { + return tapInfo{}, err + } + var info tapInfo + err = json.NewDecoder(resp.Body).Decode(&info) + return info, err } func (h *HTTPTapClient) ReadDiscard() (err error) { @@ -109,6 +114,24 @@ func (h *HTTPTapClient) ReadDiscard() (err error) { return err } +func (h *HTTPTapClient) Poll(d time.Duration) (ready bool, err error) { + info, err := h.info() + if err != nil { + return false, err + } else if info.DataReady { + return true, nil + } + deadline := time.Now().Add(d) + for !info.DataReady && time.Until(deadline) > 0 { + time.Sleep(5 * time.Millisecond) + info, err = h.info() + if err != nil { + return false, err + } + } + return info.DataReady, err +} + func (h *HTTPTapClient) ReadBytes() (data []byte, err error) { err = h.ensureMTU() if err != nil { @@ -177,6 +200,7 @@ type tapInfo struct { MTU int IPPrefix string HardwareAddr string + DataReady bool } func (sv *HTTPTapServer) OnTransfer(cb func(channel int, pkt []byte)) { @@ -255,10 +279,17 @@ func NewHTTPTapServer(iface Interface, minMTU, queueOut, queueIn int) (*HTTPTapS hwstr := net.HardwareAddr(hw6[:]).String() ipstr := netmask.String() sv.HandleFunc("/info", func(w http.ResponseWriter, r *http.Request) { + var dataready bool = true + if poller, ok := taps.tap.(interface { + Poll(time.Duration) (bool, error) + }); ok { + dataready, err = poller.Poll(0) + } info := tapInfo{ MTU: mtu, IPPrefix: ipstr, HardwareAddr: hwstr, + DataReady: dataready, } json.NewEncoder(w).Encode(info) }) diff --git a/internal/tap.go b/internal/tap.go index 535a6bb..784a6b0 100644 --- a/internal/tap.go +++ b/internal/tap.go @@ -11,6 +11,7 @@ import ( "os" "os/exec" "syscall" + "time" "unsafe" ) @@ -66,6 +67,22 @@ func (tap *Tap) Read(b []byte) (int, error) { return syscall.Read(tap.fd, b) } +// Poll waits up to timeout for the tap device to have data available for reading. +// Returns true if data is available, false if timeout was reached. +func (tap *Tap) Poll(timeout time.Duration) (bool, error) { + var readfds syscall.FdSet + readfds.Bits[tap.fd/64] |= 1 << (uint(tap.fd) % 64) + tv := syscall.Timeval{ + Sec: int64(timeout / time.Second), + Usec: int64((timeout % time.Second) / time.Microsecond), + } + n, err := syscall.Select(tap.fd+1, &readfds, nil, nil, &tv) + if err != nil { + return false, err + } + return n > 0, nil +} + func (tap *Tap) Write(b []byte) (int, error) { return syscall.Write(tap.fd, b) } @@ -247,6 +264,33 @@ func (br *Bridge) IPMask() (netip.Prefix, error) { return getSocketMask(br.fd, br.name) } +// SetReadTimeout sets the receive timeout for the bridge socket. +// This prevents Read from blocking indefinitely, allowing the caller +// to periodically call Encapsulate even when no packets arrive. +func (br *Bridge) SetReadTimeout(timeout time.Duration) error { + tv := syscall.Timeval{ + Sec: int64(timeout / time.Second), + Usec: int64((timeout % time.Second) / time.Microsecond), + } + return syscall.SetsockoptTimeval(br.fd, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &tv) +} + +// Poll waits up to timeout for the bridge socket to have data available for reading. +// Returns true if data is available, false if timeout was reached. +func (br *Bridge) Poll(timeout time.Duration) (bool, error) { + var readfds syscall.FdSet + readfds.Bits[br.fd/64] |= 1 << (uint(br.fd) % 64) + tv := syscall.Timeval{ + Sec: int64(timeout / time.Second), + Usec: int64((timeout % time.Second) / time.Microsecond), + } + n, err := syscall.Select(br.fd+1, &readfds, nil, nil, &tv) + if err != nil { + return false, err + } + return n > 0, nil +} + func (br *Bridge) Addr() (netip.Addr, error) { addrp, err := getSocketIP(br.fd, br.name) if err != nil { diff --git a/internet/pcap/capture.go b/internet/pcap/capture.go index 3b4e1d0..bf6cd20 100644 --- a/internet/pcap/capture.go +++ b/internet/pcap/capture.go @@ -671,6 +671,7 @@ const ( // FieldClassBinaryText represents long stretches of binary data such as BOOTP DHCPv4 field. FieldClassBinaryText // binary-text FieldClassOperation // op + FieldClassTimestamp // timestamp ) const octet = 8 @@ -1058,25 +1059,25 @@ var baseNTPFields = [...]FrameField{ }, { Name: "Reference Time", - Class: FieldClassText, + Class: FieldClassTimestamp, FrameBitOffset: 16 * octet, BitLength: 8 * octet, }, { Name: "Origin Time", - Class: FieldClassText, + Class: FieldClassTimestamp, FrameBitOffset: 24 * octet, BitLength: 8 * octet, }, { Name: "Receive Time", - Class: FieldClassText, + Class: FieldClassTimestamp, FrameBitOffset: 32 * octet, BitLength: 8 * octet, }, { Name: "Transit Time", - Class: FieldClassText, + Class: FieldClassTimestamp, FrameBitOffset: 40 * octet, BitLength: 8 * octet, }, diff --git a/internet/pcap/format.go b/internet/pcap/format.go index 2d54439..c99ce16 100644 --- a/internet/pcap/format.go +++ b/internet/pcap/format.go @@ -1,15 +1,19 @@ package pcap import ( + "encoding/binary" "encoding/hex" + "errors" "fmt" "net/netip" "slices" "strconv" "strings" + _ "time" "github.com/soypat/lneto" "github.com/soypat/lneto/ethernet" + "github.com/soypat/lneto/ntp" "github.com/soypat/lneto/tcp" ) @@ -106,6 +110,14 @@ func (f *Formatter) formatField(dst []byte, pktStartOff int, field FrameField, p switch field.Class { default: fallthrough + case FieldClassTimestamp: + // inspired by [time.RFC3339] + const littlerfc3339 = "2006-01-02T15:04:05.9999" + if len(f.buf) != 8 { + return dst, errors.New("only timestamp8 supported") + } + ts := ntp.TimestampFromUint64(binary.BigEndian.Uint64(f.buf)) + dst = ts.Time().AppendFormat(dst, littlerfc3339) case FieldClassChecksum, FieldClassID, FieldClassFlags, FieldClassOptions: // Binary data to be printed as hexadecimal. dst = append(dst, "0x"...) diff --git a/x/xnet/stack-blocking.go b/x/xnet/stack-blocking.go index 0afee0a..f4fc389 100644 --- a/x/xnet/stack-blocking.go +++ b/x/xnet/stack-blocking.go @@ -10,23 +10,29 @@ import ( ) const ( - maxIter = 1000 - maxTimeout = time.Minute - maxSleep = maxTimeout / maxIter + maxIter = 1000 ) var ( errDeadlineExceed = errors.New("cywnet: deadline exceeded") ) -func (s *StackAsync) StackBlocking() StackBlocking { +func (s *StackAsync) StackBlocking(loopSleep time.Duration) StackBlocking { + if loopSleep < 0 { + panic("invalid sleep") + } else if loopSleep > 3*time.Second { + // loopSleep should be a very small amount of time for stack to remain responsive. + panic("StackBlocking sleep too large") + } return StackBlocking{ - async: s, + async: s, + loopSleep: loopSleep, } } type StackBlocking struct { - async *StackAsync + async *StackAsync + loopSleep time.Duration } func (s StackBlocking) DoDHCPv4(reqAddr [4]byte, timeout time.Duration) (*DHCPResults, error) { @@ -34,7 +40,7 @@ func (s StackBlocking) DoDHCPv4(reqAddr [4]byte, timeout time.Duration) (*DHCPRe if err != nil { return nil, err } - sleep := timeout/maxIter + 1 + sleep := s.loopSleep deadline := time.Now().Add(timeout) requested := false for i := 0; i < maxIter; i++ { @@ -57,7 +63,7 @@ func (s StackBlocking) DoNTP(hostAddr netip.Addr, timeout time.Duration) (offset if err != nil { return -1, err } - sleep := timeout/maxIter + 1 + sleep := s.loopSleep deadline := time.Now().Add(timeout) var done bool for i := 0; i < maxIter; i++ { @@ -77,7 +83,7 @@ func (s StackBlocking) DoResolveHardwareAddress6(addr netip.Addr, timeout time.D if err != nil { return hw, err } - sleep := timeout/maxIter + 1 + sleep := s.loopSleep deadline := time.Now().Add(timeout) for i := 0; i < maxIter; i++ { hw, err = s.async.ResultResolveHardwareAddress6(addr) @@ -97,7 +103,7 @@ func (s StackBlocking) DoLookupIP(host string, timeout time.Duration) (addrs []n if err != nil { return nil, err } - sleep := timeout/maxIter + 1 + sleep := s.loopSleep deadline := time.Now().Add(timeout) for i := 0; i < maxIter; i++ { addrs, completed, err := s.async.ResultLookupIP(host) @@ -118,7 +124,7 @@ func (s StackBlocking) DoDialTCP(conn *tcp.Conn, localPort uint16, addrp netip.A if err != nil { return err } - sleep := timeout/maxIter + 1 + sleep := s.loopSleep deadline := time.Now().Add(timeout) for i := 0; i < maxIter; i++ { state := conn.State() diff --git a/x/xnet/stack-retrying.go b/x/xnet/stack-retrying.go index 464e2b1..226a6ab 100644 --- a/x/xnet/stack-retrying.go +++ b/x/xnet/stack-retrying.go @@ -8,9 +8,9 @@ import ( "github.com/soypat/lneto/tcp" ) -func (s *StackAsync) StackRetrying() StackRetrying { +func (s *StackAsync) StackRetrying(loopSleep time.Duration) StackRetrying { return StackRetrying{ - block: s.StackBlocking(), + block: s.StackBlocking(loopSleep), } }