diff --git a/README.md b/README.md index 42d9e75..e2944c2 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ You may try lneto out on linux with the [xcurl example](./examples/xcurl/) which - HTTP over TCP/IPv4/Ethernet connection using - NTP time check (optional) - Print packet captures using lneto's [internet/pcap](./internet/pcap) package +- Optional fixed TCP source port with `-port`, useful when debugging raw-socket interactions with the host OS See Developing section below for more information. @@ -171,6 +172,19 @@ sudo ethtool -K gro off Depending on the interface and driver, other offloading features may also need to be disabled. +When running `xcurl` directly on a normal Linux interface (for example `-i wlan0` or `-i eth0`), lneto shares the host interface IP and MAC address. The Linux TCP stack also sees incoming packets for that IP. If xcurl's TCP handshake succeeds but the HTTP request is followed by a TCP RST from the server, the host kernel may have sent its own outbound RST for xcurl's raw TCP flow because no kernel socket owns that source port. + +For a short validation test, use a fixed xcurl source port and temporarily drop kernel-generated outbound RSTs for that port: + +```sh +go build -o examples/xcurl/xcurl ./examples/xcurl +sudo iptables -I OUTPUT -p tcp --sport 2300 --tcp-flags RST RST -j DROP +sudo ./examples/xcurl/xcurl -i -port 2300 -host example.com +sudo iptables -D OUTPUT -p tcp --sport 2300 --tcp-flags RST RST -j DROP +``` + +This is a debugging workaround, not the preferred operating mode. Prefer `-ihttp`, a TAP interface, a network namespace, or an otherwise isolated interface/IP when testing xcurl without host TCP stack interference. + - [`examples/httptap`](./examples/httptap) (linux only, root privilidges required) Program opens a TAP interface and assigns an IP address to it and exposes the interface via a HTTP interface. This program is run with root privilidges to facilitate debugging of lneto since no root privilidges are required to interact with the HTTP interface exposed. - `POST http://127.0.0.1:7070/send`: Receives a POST with request body containing JSON string of data to send over TAP interface. Response contains only status code. - `GET http://127.0.0.1:7070/recv`: Receives a GET request. Response contains a JSON string of oldest unread TAP interface packet. If string is empty then there is no more data to read. diff --git a/examples/xcurl/main.go b/examples/xcurl/main.go index 5859003..85cd425 100644 --- a/examples/xcurl/main.go +++ b/examples/xcurl/main.go @@ -50,6 +50,7 @@ func run() (err error) { flagUseHTTP = false flagHostToResolve = "" flagRequestedIP = "" + flagLocalTCPPort = 0 flagDoNTP = false flagNoPcap = false flagPprof = false @@ -58,6 +59,7 @@ func run() (err error) { 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.") @@ -79,6 +81,21 @@ func run() (err error) { 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 { @@ -223,11 +240,14 @@ func run() (err error) { dhcpRetries = 2 ) timeDHCP := timer("DHCP request completed") - results, err := rstack.DoDHCPv4([4]byte{192, 168, 1, 96}, dhcpTimeout, dhcpRetries) + 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 { @@ -302,7 +322,12 @@ func run() (err error) { 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, tcpDialTimeout, internetRetries) + 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) }