Fix slow exchanges on linux by adding polling and other improvements (#14)

* add prints to investigate slowness

* add loop sleep for blocking Stack

* add connection close http attribute and detect end of HTML page

* add timeout to bridge read

* examples/xnet: add ntp timestamp to prints and show output in readme

* add polling to http tap interface

* more digits in ntp format

* prevent crashes on non-8 timestamp
This commit is contained in:
Pat Whittingslow
2026-01-01 09:35:35 -03:00
committed by GitHub
parent 3cbef6fa92
commit f8166aea05
9 changed files with 297 additions and 36 deletions
+5
View File
@@ -15,6 +15,10 @@ vendor/
*.so *.so
*.dylib *.dylib
*.hex *.hex
# Profiling
*.pprof
# example binaries. # example binaries.
/tap /tap
/stack /stack
@@ -46,6 +50,7 @@ local
*.zip *.zip
*.gz *.gz
*.tar *.tar
*.txt
# If running a python script. # If running a python script.
*/__pycache__/* */__pycache__/*
+76 -1
View File
@@ -100,5 +100,80 @@ Now run the application you wish to test without elevated privilidges. Stackbasi
go run ./examples/stackbasic 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
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="http://www.google.com/">here</A>.
</BODY></HTML>
success
```
+91 -4
View File
@@ -13,6 +13,7 @@ import (
"net/netip" "net/netip"
"os" "os"
"runtime" "runtime"
"runtime/pprof"
"strings" "strings"
"time" "time"
@@ -47,6 +48,7 @@ func run() (err error) {
flagDoNTP = false flagDoNTP = false
flagHTTPGet = false flagHTTPGet = false
flagNoPcap = false flagNoPcap = false
flagPprof = false
) )
flag.BoolVar(&flagHTTPGet, "httpget", flagHTTPGet, "Do an HTTP GET request ") 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.") 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.StringVar(&flagRequestedIP, "addr", flagRequestedIP, "IP address to request via DHCP.")
flag.BoolVar(&flagDoNTP, "ntp", flagDoNTP, "Do NTP round and print result time") flag.BoolVar(&flagDoNTP, "ntp", flagDoNTP, "Do NTP round and print result time")
flag.BoolVar(&flagNoPcap, "nopcap", flagNoPcap, "Disable pcap logging.") flag.BoolVar(&flagNoPcap, "nopcap", flagNoPcap, "Disable pcap logging.")
flag.BoolVar(&flagPprof, "pprof", flagPprof, "Enable CPU profiling.")
flag.Parse() 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) fmt.Println("softrand", softRand)
_, err = dns.NewName(flagHostToResolve) _, err = dns.NewName(flagHostToResolve)
if err != nil { if err != nil {
@@ -77,6 +88,10 @@ func run() (err error) {
if err != nil { if err != nil {
return err return err
} }
err = bridge.SetReadTimeout(5 * time.Millisecond)
if err != nil {
return err
}
iface = bridge iface = bridge
} }
} }
@@ -108,6 +123,7 @@ func run() (err error) {
if err != nil { if err != nil {
return err return err
} }
// Loop goroutine. // Loop goroutine.
go func() { go func() {
lastAction := time.Now() lastAction := time.Now()
@@ -115,7 +131,7 @@ func run() (err error) {
var cap pcap.PacketBreakdown var cap pcap.PacketBreakdown
var frames []pcap.Frame var frames []pcap.Frame
pf := pcap.Formatter{ 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 var pfbuf []byte
logFrames := func(context string, pkt []byte) error { logFrames := func(context string, pkt []byte) error {
@@ -158,6 +174,14 @@ func run() (err error) {
} }
clear(buf[:nwrite]) 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) nread, err := iface.Read(buf)
if err != nil { if err != nil {
log.Fatal("groutine read:", err) log.Fatal("groutine read:", err)
@@ -181,16 +205,18 @@ func run() (err error) {
} }
}() }()
rstack := stack.StackRetrying() rstack := stack.StackRetrying(5 * time.Millisecond)
const ( const (
dhcpTimeout = 6 * time.Second dhcpTimeout = 6 * time.Second
dhcpRetries = 2 dhcpRetries = 2
) )
timeDHCP := timer("DHCP request completed")
results, err := rstack.DoDHCPv4([4]byte{192, 168, 1, 96}, dhcpTimeout, dhcpRetries) results, err := rstack.DoDHCPv4([4]byte{192, 168, 1, 96}, dhcpTimeout, dhcpRetries)
if err != nil { if err != nil {
return fmt.Errorf("DHCP failed: %w", err) return fmt.Errorf("DHCP failed: %w", err)
} }
timeDHCP()
err = stack.AssimilateDHCPResults(results) err = stack.AssimilateDHCPResults(results)
if err != nil { if err != nil {
return fmt.Errorf("assimilating DHCP results: %w", err) return fmt.Errorf("assimilating DHCP results: %w", err)
@@ -204,31 +230,39 @@ func run() (err error) {
internetTimeout = 3 * time.Second internetTimeout = 3 * time.Second
internetRetries = 2 internetRetries = 2
) )
timeResolveRouterHW := timer("Router ARP resolution")
routerHw, err := rstack.DoResolveHardwareAddress6(results.Router, arpTimeout, arpRetries) routerHw, err := rstack.DoResolveHardwareAddress6(results.Router, arpTimeout, arpRetries)
if err != nil { if err != nil {
return fmt.Errorf("ARP resolution of router failed: %w", err) return fmt.Errorf("ARP resolution of router failed: %w", err)
} }
timeResolveRouterHW()
stack.SetGateway6(routerHw) stack.SetGateway6(routerHw)
if flagDoNTP { if flagDoNTP {
timeLookupNTP := timer("NTP IP lookup")
const ntpHost = "pool.ntp.org" const ntpHost = "pool.ntp.org"
addrs, err := rstack.DoLookupIP(ntpHost, internetTimeout, internetRetries) addrs, err := rstack.DoLookupIP(ntpHost, internetTimeout, internetRetries)
if err != nil { if err != nil {
return fmt.Errorf("NTP address lookup of %q failed: %w", ntpHost, err) 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) offset, err := rstack.DoNTP(addrs[0], internetTimeout, internetRetries)
if err != nil { if err != nil {
return fmt.Errorf("NTP address lookup of %q failed: %w", ntpHost, err) return fmt.Errorf("NTP address lookup of %q failed: %w", ntpHost, err)
} }
timeNTP()
relative := "behind" relative := "behind"
if offset < 0 { if offset < 0 {
relative = "ahead" relative = "ahead"
} }
fmt.Println("NTP completed. You are", offset.Abs().String(), relative, "of the NTP server") 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) addrs, err := rstack.DoLookupIP(flagHostToResolve, internetTimeout, internetRetries)
if err != nil { if err != nil {
return fmt.Errorf("DNS of host %q failed: %w", flagHostToResolve, err) 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) fmt.Printf("DNS resolution of %q complete and resolved to %v\n", flagHostToResolve, addrs)
var conn tcp.Conn var conn tcp.Conn
conn.Configure(tcp.ConnConfig{ conn.Configure(tcp.ConnConfig{
@@ -237,6 +271,7 @@ func run() (err error) {
TxPacketQueueSize: 3, TxPacketQueueSize: 3,
}) })
if flagHTTPGet { if flagHTTPGet {
timeHTTPCreate := timer("create HTTP GET request")
var hdr httpraw.Header var hdr httpraw.Header
hdr.SetMethod("GET") hdr.SetMethod("GET")
hdr.SetRequestURI("/") hdr.SetRequestURI("/")
@@ -244,22 +279,31 @@ func run() (err error) {
hdr.Set("Host", flagHostToResolve) hdr.Set("Host", flagHostToResolve)
hdr.Set("User-Agent", "lneto") hdr.Set("User-Agent", "lneto")
hdr.Set("Accept-Language", "en-US,en;q=0.5") 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) req, err := hdr.AppendRequest(nil)
if err != nil { if err != nil {
return err 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) 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 { if err != nil {
return fmt.Errorf("TCP failed: %w", err) return fmt.Errorf("TCP failed: %w", err)
} }
timeTCPDial()
timeHTTPSend := timer("send HTTP request")
conn.SetDeadline(time.Now().Add(internetTimeout)) conn.SetDeadline(time.Now().Add(internetTimeout))
_, err = conn.Write(req) _, err = conn.Write(req)
timeHTTPSend()
timeHTTPRcv := timer("recv http request")
if err != nil { if err != nil {
return err return err
} }
rxbuf := make([]byte, 2048) rxbuf := make([]byte, 2048)
var page []byte var page []byte
for { for {
var n int var n int
@@ -268,10 +312,16 @@ func run() (err error) {
if err != nil { if err != nil {
break break
} }
ptrimmed := bytes.TrimSpace(page)
if bytes.EqualFold(ptrimmed[len(ptrimmed)-7:], []byte("</html>")) {
// We've received the last part of the HTML. we're done.
break
}
} }
if len(page) == 0 { if len(page) == 0 {
return err return err
} }
timeHTTPRcv()
os.Stdout.Write(page) os.Stdout.Write(page)
} }
return nil return nil
@@ -282,3 +332,40 @@ func clear(buf []byte) {
buf[i] = 0 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
}
+45 -14
View File
@@ -26,6 +26,16 @@ type Interface interface {
IPMask() (netip.Prefix, error) 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) var _ Interface = (*HTTPTapClient)(nil)
// NewHTTPTapClient returns a HTTPTapClient ready for use. // 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) err = fmt.Errorf("unable to get MTU from server: %w", err)
} }
}() }()
resp, err := h.c.Get(h.infoURL) info, err := h.info()
if err != nil {
return err
}
var info tapInfo
err = json.NewDecoder(resp.Body).Decode(&info)
if err != nil { if err != nil {
return err return err
} else if info.MTU <= minMTU { } else if info.MTU <= minMTU {
@@ -88,14 +93,14 @@ func (h *HTTPTapClient) ensureMTU() (err error) {
return nil return nil
} }
type HTTPTapClient struct { func (h *HTTPTapClient) info() (tapInfo, error) {
c http.Client resp, err := h.c.Get(h.infoURL)
infoURL string if err != nil {
recvurl string return tapInfo{}, err
sendurl string }
ip netip.Prefix var info tapInfo
hwaddr [6]byte err = json.NewDecoder(resp.Body).Decode(&info)
buf []byte return info, err
} }
func (h *HTTPTapClient) ReadDiscard() (err error) { func (h *HTTPTapClient) ReadDiscard() (err error) {
@@ -109,6 +114,24 @@ func (h *HTTPTapClient) ReadDiscard() (err error) {
return err 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) { func (h *HTTPTapClient) ReadBytes() (data []byte, err error) {
err = h.ensureMTU() err = h.ensureMTU()
if err != nil { if err != nil {
@@ -177,6 +200,7 @@ type tapInfo struct {
MTU int MTU int
IPPrefix string IPPrefix string
HardwareAddr string HardwareAddr string
DataReady bool
} }
func (sv *HTTPTapServer) OnTransfer(cb func(channel int, pkt []byte)) { 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() hwstr := net.HardwareAddr(hw6[:]).String()
ipstr := netmask.String() ipstr := netmask.String()
sv.HandleFunc("/info", func(w http.ResponseWriter, r *http.Request) { 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{ info := tapInfo{
MTU: mtu, MTU: mtu,
IPPrefix: ipstr, IPPrefix: ipstr,
HardwareAddr: hwstr, HardwareAddr: hwstr,
DataReady: dataready,
} }
json.NewEncoder(w).Encode(info) json.NewEncoder(w).Encode(info)
}) })
+44
View File
@@ -11,6 +11,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"syscall" "syscall"
"time"
"unsafe" "unsafe"
) )
@@ -66,6 +67,22 @@ func (tap *Tap) Read(b []byte) (int, error) {
return syscall.Read(tap.fd, b) 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) { func (tap *Tap) Write(b []byte) (int, error) {
return syscall.Write(tap.fd, b) return syscall.Write(tap.fd, b)
} }
@@ -247,6 +264,33 @@ func (br *Bridge) IPMask() (netip.Prefix, error) {
return getSocketMask(br.fd, br.name) 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) { func (br *Bridge) Addr() (netip.Addr, error) {
addrp, err := getSocketIP(br.fd, br.name) addrp, err := getSocketIP(br.fd, br.name)
if err != nil { if err != nil {
+5 -4
View File
@@ -671,6 +671,7 @@ const (
// FieldClassBinaryText represents long stretches of binary data such as BOOTP DHCPv4 field. // FieldClassBinaryText represents long stretches of binary data such as BOOTP DHCPv4 field.
FieldClassBinaryText // binary-text FieldClassBinaryText // binary-text
FieldClassOperation // op FieldClassOperation // op
FieldClassTimestamp // timestamp
) )
const octet = 8 const octet = 8
@@ -1058,25 +1059,25 @@ var baseNTPFields = [...]FrameField{
}, },
{ {
Name: "Reference Time", Name: "Reference Time",
Class: FieldClassText, Class: FieldClassTimestamp,
FrameBitOffset: 16 * octet, FrameBitOffset: 16 * octet,
BitLength: 8 * octet, BitLength: 8 * octet,
}, },
{ {
Name: "Origin Time", Name: "Origin Time",
Class: FieldClassText, Class: FieldClassTimestamp,
FrameBitOffset: 24 * octet, FrameBitOffset: 24 * octet,
BitLength: 8 * octet, BitLength: 8 * octet,
}, },
{ {
Name: "Receive Time", Name: "Receive Time",
Class: FieldClassText, Class: FieldClassTimestamp,
FrameBitOffset: 32 * octet, FrameBitOffset: 32 * octet,
BitLength: 8 * octet, BitLength: 8 * octet,
}, },
{ {
Name: "Transit Time", Name: "Transit Time",
Class: FieldClassText, Class: FieldClassTimestamp,
FrameBitOffset: 40 * octet, FrameBitOffset: 40 * octet,
BitLength: 8 * octet, BitLength: 8 * octet,
}, },
+12
View File
@@ -1,15 +1,19 @@
package pcap package pcap
import ( import (
"encoding/binary"
"encoding/hex" "encoding/hex"
"errors"
"fmt" "fmt"
"net/netip" "net/netip"
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
_ "time"
"github.com/soypat/lneto" "github.com/soypat/lneto"
"github.com/soypat/lneto/ethernet" "github.com/soypat/lneto/ethernet"
"github.com/soypat/lneto/ntp"
"github.com/soypat/lneto/tcp" "github.com/soypat/lneto/tcp"
) )
@@ -106,6 +110,14 @@ func (f *Formatter) formatField(dst []byte, pktStartOff int, field FrameField, p
switch field.Class { switch field.Class {
default: default:
fallthrough 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: case FieldClassChecksum, FieldClassID, FieldClassFlags, FieldClassOptions:
// Binary data to be printed as hexadecimal. // Binary data to be printed as hexadecimal.
dst = append(dst, "0x"...) dst = append(dst, "0x"...)
+17 -11
View File
@@ -10,23 +10,29 @@ import (
) )
const ( const (
maxIter = 1000 maxIter = 1000
maxTimeout = time.Minute
maxSleep = maxTimeout / maxIter
) )
var ( var (
errDeadlineExceed = errors.New("cywnet: deadline exceeded") 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{ return StackBlocking{
async: s, async: s,
loopSleep: loopSleep,
} }
} }
type StackBlocking struct { type StackBlocking struct {
async *StackAsync async *StackAsync
loopSleep time.Duration
} }
func (s StackBlocking) DoDHCPv4(reqAddr [4]byte, timeout time.Duration) (*DHCPResults, error) { 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 { if err != nil {
return nil, err return nil, err
} }
sleep := timeout/maxIter + 1 sleep := s.loopSleep
deadline := time.Now().Add(timeout) deadline := time.Now().Add(timeout)
requested := false requested := false
for i := 0; i < maxIter; i++ { for i := 0; i < maxIter; i++ {
@@ -57,7 +63,7 @@ func (s StackBlocking) DoNTP(hostAddr netip.Addr, timeout time.Duration) (offset
if err != nil { if err != nil {
return -1, err return -1, err
} }
sleep := timeout/maxIter + 1 sleep := s.loopSleep
deadline := time.Now().Add(timeout) deadline := time.Now().Add(timeout)
var done bool var done bool
for i := 0; i < maxIter; i++ { for i := 0; i < maxIter; i++ {
@@ -77,7 +83,7 @@ func (s StackBlocking) DoResolveHardwareAddress6(addr netip.Addr, timeout time.D
if err != nil { if err != nil {
return hw, err return hw, err
} }
sleep := timeout/maxIter + 1 sleep := s.loopSleep
deadline := time.Now().Add(timeout) deadline := time.Now().Add(timeout)
for i := 0; i < maxIter; i++ { for i := 0; i < maxIter; i++ {
hw, err = s.async.ResultResolveHardwareAddress6(addr) hw, err = s.async.ResultResolveHardwareAddress6(addr)
@@ -97,7 +103,7 @@ func (s StackBlocking) DoLookupIP(host string, timeout time.Duration) (addrs []n
if err != nil { if err != nil {
return nil, err return nil, err
} }
sleep := timeout/maxIter + 1 sleep := s.loopSleep
deadline := time.Now().Add(timeout) deadline := time.Now().Add(timeout)
for i := 0; i < maxIter; i++ { for i := 0; i < maxIter; i++ {
addrs, completed, err := s.async.ResultLookupIP(host) 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 { if err != nil {
return err return err
} }
sleep := timeout/maxIter + 1 sleep := s.loopSleep
deadline := time.Now().Add(timeout) deadline := time.Now().Add(timeout)
for i := 0; i < maxIter; i++ { for i := 0; i < maxIter; i++ {
state := conn.State() state := conn.State()
+2 -2
View File
@@ -8,9 +8,9 @@ import (
"github.com/soypat/lneto/tcp" "github.com/soypat/lneto/tcp"
) )
func (s *StackAsync) StackRetrying() StackRetrying { func (s *StackAsync) StackRetrying(loopSleep time.Duration) StackRetrying {
return StackRetrying{ return StackRetrying{
block: s.StackBlocking(), block: s.StackBlocking(loopSleep),
} }
} }