remove old examples and update readme

This commit is contained in:
soypat
2026-01-08 12:41:19 -03:00
parent 7cbf0fb301
commit be194ad7ea
8 changed files with 90 additions and 1254 deletions
+5 -1
View File
@@ -21,11 +21,15 @@ vendor/
# example binaries.
/tap
/httptap
/xcurl
/xnet
/httpclient
/stack
**__debug_bin*
# `__debug_bin` Debug binary generated in VSCode when using the built-in debugger.
*bin
/xnet
/bridge
# IDE
.vscode/
+23 -11
View File
@@ -22,6 +22,18 @@ Userspace networking primitives.
- Extremely simple networking stack construction. Can be used to teach basics of networking
- Only one networking interface fulfilled by all implementations. See [abstractions](#abstractions).
## `xcurl` example
You may try lneto out on linux with the [xcurl example](./examples/xcurl/) which gets an HTTP page by doing all the low-level networking part using absolutely no standard library.
- DHCP client address lease
- ARP address resolution
- DNS address resolution of requested host
- HTTP over TCP/IPv4/Ethernet connection using
- NTP time check (optional)
- Print packet captures using lneto's [internet/pcap](./internet/pcap) package
See Developing section below for more information.
## Why?(!)
`lneto` was created to have networking on systems with a networking interface (wifi or ethernet cable) but no operating-system provided networking facilties.
@@ -82,34 +94,34 @@ go mod download github.com/soypat/lneto@latest
## Developing (linux)
- [`tap`](./examples/tap) (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.
- [`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.
- [`stack`](./examples/stack) Contains stack implementation which can interact with `tap` program. No root privilidges required.
- Can expose a HTTP server.
- [`xcurl`](./examples/xcurl) Contains example of a application that uses lneto and can attach to a linux tap/bridge interface or a [httptap](./examples/httptap)(with -ihttp flag) to work. When using httptap can be run as non-root user to be debugged comfortably.
- Example: `go run ./examples/xcurl -host google.com -ihttp`
To run the HTTP TAP server run the following commands. Requires elevated privilidges!
### Quick run xcurl
Run xcurl over httptap interface. Requires running two programs in separate shell/consoles in linux:
```sh
# Build+Run HTTP Tap server from one shell, this will expose the `tap0` TAP interface over an HTTP interface at http://127.0.0.1:7070 on /recv and /send endpoints.
go build ./examples/tap && sudo ./tap
go build ./examples/httptap && sudo ./httpap
```
Now run the application you wish to test without elevated privilidges. Stackbasic shows a basic HTTP demo in action.
No privilidge escalation required for xcurl using `-ihttp` flag which taps using `httptap`:
```sh
go run ./examples/stackbasic
go run ./examples/xcurl -host google.com -ihttp
```
### 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).
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/httptap`](./examples/httptap/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:
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 xcurl example with pcap logging:
```log
go run ./examples/xnet -httpget -host google.com -ihttp -ntp
go run ./examples/xcurl -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::]
-459
View File
@@ -1,459 +0,0 @@
package main
import (
"crypto/rand"
"encoding/binary"
"errors"
"flag"
"fmt"
"log/slog"
"net"
"net/netip"
"os"
"runtime"
"strings"
"time"
"github.com/soypat/lneto/arp"
"github.com/soypat/lneto/dhcpv4"
"github.com/soypat/lneto/dns"
"github.com/soypat/lneto/ethernet"
"github.com/soypat/lneto/internal"
"github.com/soypat/lneto/internal/ltesto"
"github.com/soypat/lneto/internet"
"github.com/soypat/lneto/internet/pcap"
"github.com/soypat/lneto/ntp"
)
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 = ""
flagDoNTP = false
)
flag.StringVar(&flagInterface, "i", flagInterface, "Interface to use. Either tap* or the name of an existing interface to bridge to.")
flag.BoolVar(&flagUseHTTP, "http", 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.BoolVar(&flagDoNTP, "ntp", flagDoNTP, "Do NTP round and print result time")
flag.Parse()
fmt.Println("softrand", softRand)
_, err = dns.NewName(flagHostToResolve)
if err != nil {
flag.Usage()
return err
}
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
}
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 Stack
err = stack.Reset(brHW, netip.AddrFrom4([4]byte{}), uint16(mtu))
if err != nil {
return err
}
buf := make([]byte, mtu)
lastAction := time.Now()
const (
stateDHCP = iota
stateInitARP
stateDNSNTP
stateNTP
stateDNS
stateDone
)
err = stack.BeginDHCPRequest([4]byte{192, 168, 1, 96})
if err != nil {
return err
}
state := stateDHCP
prevState := state
for {
switch state {
case stateDHCP:
dhcpIsDone := stack.dhcp.State() == dhcpv4.StateBound
if dhcpIsDone {
state = stateInitARP
assigned4, ok := stack.dhcp.AssignedAddr()
if !ok {
return errors.New("DHCP client address not assigned")
}
err = stack.ip.SetAddr(netip.AddrFrom4(assigned4))
if err != nil {
return err
}
routeraddr, ok := stack.dhcp.RouterAddr()
if !ok {
return errors.New("DHCP router address not assigned")
}
err = stack.StartResolveHardwareAddress6(netip.AddrFrom4(routeraddr))
if err != nil {
return err
}
}
case stateInitARP:
router, ok := stack.dhcp.RouterAddr()
if !ok {
return errors.New("DHCP router address not assigned")
}
hw, err := stack.ResultResolveHardwareAddress6(netip.AddrFrom4(router))
if err == nil {
stack.link.SetGateway6(hw)
if flagDoNTP {
state = stateDNSNTP
err = stack.StartLookupIP("pool.ntp.org")
} else {
state = stateDNS
err = stack.StartLookupIP(flagHostToResolve)
}
if err != nil {
return err
}
}
case stateDNSNTP:
addrs, done, err := stack.ResultLookupIP()
if err == nil {
state = stateNTP
fmt.Println("START NTP")
err = stack.StartNTP(addrs[0])
} else if !done {
err = nil
}
if err != nil {
return err
}
case stateNTP:
offset, done := stack.ResultNTP()
if done {
relative := "behind"
if offset < 0 {
relative = "ahead"
}
fmt.Println("NTP completed. You are", offset.Abs(), relative, "of the NTP server")
state = stateDNS
err = stack.StartLookupIP(flagHostToResolve)
if err != nil {
return err
}
}
case stateDNS:
addrs, done, err := stack.ResultLookupIP()
if err == nil {
fmt.Println(flagHostToResolve, "resolved to", addrs)
return nil
} else if done {
return err
}
}
if prevState != state {
fmt.Println("STATE CHANGE", prevState, state)
}
prevState = state
clear(buf)
nwrite, err := stack.Encapsulate(buf[:], -1, 0)
if err != nil {
fmt.Println("ERR:ENCAPSULATE", err)
} else if nwrite > 0 {
n, err := iface.Write(buf[:nwrite])
if err != nil {
return err
} else if n != nwrite {
return fmt.Errorf("mismatch written bytes %d!=%d", nwrite, n)
}
}
clear(buf)
nread, err := iface.Read(buf)
if err != nil {
return err
} else if nread > 0 {
err = stack.Demux(buf[:nread], 0)
if err != nil {
fmt.Println("ERR:DEMUX", err)
}
}
if nread == 0 && nwrite == 0 && time.Since(lastAction) > 4*time.Second {
time.Sleep(5 * time.Millisecond)
} else {
lastAction = time.Now()
runtime.Gosched()
}
}
return nil
}
type Stack struct {
link internet.StackEthernet
ip internet.StackIP
arp arp.Handler
udps internet.StackPorts
dhcp dhcpv4.Client
dns dns.Client
ednsopt dns.Resource
lookup dns.Message
ntp ntp.Client
sysprec int8 // NTP system precision.
// Packet capture and top level filtering.
shark pcap.PacketBreakdown
aux []pcap.Frame
}
func (s *Stack) Demux(b []byte, _ int) (err error) {
s.aux, err = s.shark.CaptureEthernet(s.aux[:0], b, 0)
topFrame := s.aux[len(s.aux)-1]
isOK := topFrame.Protocol == "DHCPv4" || // Allow DHCP, DNS and NTP responses.
topFrame.Protocol == "DNS" ||
topFrame.Protocol == "NTP" ||
topFrame.Protocol == ethernet.TypeARP // Allow ARP responses.
if !isOK {
return nil
}
if err != nil {
fmt.Println("IN", s.aux, err.Error())
} else {
fmt.Println("IN", s.aux)
}
return s.link.Demux(b, 0)
}
func (s *Stack) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int) (int, error) {
n, err := s.link.Encapsulate(carrierData, offsetToIP, offsetToFrame)
if n > 0 {
iframes, errpcap := s.shark.CaptureEthernet(s.aux[:0], carrierData[:n], 0)
if errpcap != nil {
fmt.Println("OU", iframes, errpcap.Error())
} else {
fmt.Println("OU", iframes)
}
}
return n, err
}
func (s *Stack) Reset(mac [6]byte, addr netip.Addr, mtu uint16) error {
const maxNodes = 8
err := s.link.Reset6(mac, ethernet.BroadcastAddr(), int(mtu), maxNodes)
if err != nil {
return err
}
err = s.ip.Reset(addr, maxNodes)
if err != nil {
return err
}
ipaddr := addr.AsSlice()
proto := ethernet.TypeIPv4
if addr.Is6() {
proto = ethernet.TypeIPv6
}
err = s.arp.Reset(arp.HandlerConfig{
HardwareAddr: mac[:],
ProtocolAddr: ipaddr,
MaxQueries: 3,
MaxPending: 3,
HardwareType: 1,
ProtocolType: proto,
})
if err != nil {
return err
}
err = s.udps.ResetUDP(maxNodes)
if err != nil {
return err
}
// Now setup stacks.
err = s.link.Register(&s.arp) // ARP.
if err != nil {
return err
}
err = s.link.Register(&s.ip) // IPv4 | IPv6
if err != nil {
return err
}
err = s.ip.Register(&s.udps)
if err != nil {
return err
}
s.ip.SetLogger(slog.Default())
var timebuf [32]time.Time
s.sysprec = ntp.CalculateSystemPrecision(time.Now, timebuf[:])
return nil
}
func (s *Stack) StartLookupIP(host string) error {
dnsSrvs := s.dhcp.DNSServerFirst()
if !dnsSrvs.IsValid() {
return errors.New("no valid DNS server")
}
name, err := dns.NewName(host)
if err != nil {
return err
}
s.ednsopt.SetEDNS0(uint16(s.link.MTU())-100, 0, 0, nil)
err = s.dns.StartResolve(uint16(softRand>>1)+1024, uint16(softRand), dns.ResolveConfig{
Questions: []dns.Question{
{
Name: name,
Type: dns.TypeA,
Class: dns.ClassINET,
},
},
Additional: []dns.Resource{
s.ednsopt,
},
EnableRecursion: true,
})
if err != nil {
return err
}
var u internet.StackUDPPort
dns4 := dnsSrvs.As4()
u.SetStackNode(&s.dns, dns4[:], dns.ServerPort)
err = s.udps.Register(&u)
if err != nil {
return err
}
fmt.Println("START LOOKUP", host, dns4[:])
return nil
}
func (s *Stack) ResultLookupIP() ([]netip.Addr, bool, error) {
done, err := s.dns.MessageCopyTo(&s.lookup)
if err != nil {
return nil, done, err
} else if !done {
return nil, done, errors.New("DNS not done")
}
var addrs []netip.Addr
ans := s.lookup.Answers
for i := range ans {
data := ans[i].RawData()
if len(data) == 4 {
addrs = append(addrs, netip.AddrFrom4([4]byte(data)))
} else if len(data) == 16 {
addrs = append(addrs, netip.AddrFrom16([16]byte(data)))
}
}
return addrs, done, nil
}
func (s *Stack) ResultNTP() (time.Duration, bool) {
return s.ntp.Offset(), s.ntp.IsDone()
}
func (s *Stack) BeginDHCPRequest(request [4]byte) error {
var buf [4]byte
rand.Read(buf[:])
xid := binary.LittleEndian.Uint32(buf[:])
err := s.dhcp.BeginRequest(xid, dhcpv4.RequestConfig{
RequestedAddr: request,
ClientHardwareAddr: s.link.HardwareAddr6(),
Hostname: "lneto",
})
if err != nil {
return err
}
var u internet.StackUDPPort
u.SetStackNode(&s.dhcp, nil, dhcpv4.DefaultServerPort)
err = s.udps.Register(&u)
if err != nil {
return err
}
return err
}
func (s *Stack) StartNTP(addr netip.Addr) error {
s.ntp.Reset(s.sysprec, time.Now)
var u internet.StackUDPPort
addr4 := addr.As4()
u.SetStackNode(&s.ntp, addr4[:], ntp.ServerPort)
err := s.udps.Register(&u)
return err
}
func (s *Stack) StartResolveHardwareAddress6(ip netip.Addr) error {
if !ip.Is4() {
return errors.New("unsupported or invalid IP address")
}
addr := ip.As4()
return s.arp.StartQuery(nil, addr[:])
}
func (s *Stack) ResultResolveHardwareAddress6(ip netip.Addr) (hw [6]byte, err error) {
if !ip.Is4() {
return hw, errors.New("unsupported or invalid IP address")
}
addr := ip.As4()
hwslice, err := s.arp.QueryResult(addr[:])
if err != nil {
return hw, err
} else if len(hwslice) != 6 {
panic("unreachable slice hw length")
}
return [6]byte(hwslice), nil
}
func clear(buf []byte) {
for i := range buf {
buf[i] = 0
}
}
func getField(frame pcap.Frame, pkt []byte, class pcap.FieldClass) uint64 {
idx, err := frame.FieldByClass(class)
if err != nil {
return 0
}
v, _ := frame.FieldAsUint(idx, pkt)
return v
}
-311
View File
@@ -1,311 +0,0 @@
package main
import (
"crypto/rand"
"encoding/binary"
"fmt"
"log"
"log/slog"
"net"
"net/netip"
"os"
"runtime"
"time"
"github.com/soypat/lneto"
"github.com/soypat/lneto/arp"
"github.com/soypat/lneto/ethernet"
"github.com/soypat/lneto/http/httpraw"
"github.com/soypat/lneto/internal/ltesto"
"github.com/soypat/lneto/internet"
"github.com/soypat/lneto/internet/pcap"
"github.com/soypat/lneto/tcp"
)
const (
stackIP = "192.168.10.2"
stackPort = 80
iss = 100
)
var stackHWAddr = [6]byte{0xc0, 0xff, 0xee, 0x00, 0xde, 0xad}
func main() {
ip := netip.MustParseAddr(stackIP)
tap := ltesto.NewHTTPTapClient("http://127.0.0.1:7070")
ippfx, _ := tap.IPMask()
if !ippfx.Contains(ip) {
log.Fatal("interface does not contain stack address")
}
addrPort := netip.AddrPortFrom(ip, stackPort)
lg := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
gatewayMAC, _ := tap.HardwareAddress6()
mtu, _ := tap.MTU()
var stack Stack
err := stack.Reset(stackHWAddr, gatewayMAC, addrPort.Addr(), mtu)
if err != nil {
log.Fatal(err)
}
listener, err := stack.OpenTCPListener(addrPort.Port())
if err != nil {
log.Fatal(err)
}
defer tap.Close()
tap.ReadDiscard() // Discard all unread content.
fmt.Println("hosting server at ", addrPort.String(), "over tap interface of mtu:", mtu, "prefix:", ippfx, "gateway:", net.HardwareAddr(gatewayMAC[:]).String())
buf := make([]byte, mtu)
var hdr httpraw.Header
hdr.Reset(make([]byte, 0, 1024))
const standbyDuration = 5 * time.Second
lastHit := time.Now().Add(-standbyDuration)
var cap pcap.PacketBreakdown
var conn *tcp.Conn
accepted := 0
for {
nread, err := tap.Read(buf[:])
if err != nil {
lg.Error("tap-err", slog.String("err", err.Error()))
log.Fatal(err)
} else if nread > 0 {
frames, err := cap.CaptureEthernet(nil, buf[:nread], 0)
if err == nil {
flags := getTCPFlags(frames, buf[:nread])
if flags == 0 {
fmt.Println("IN", time.Now().Format("15:04:05.000"), frames)
} else {
fmt.Println("IN", time.Now().Format("15:04:05.000"), frames, flags.String())
}
}
err = stack.ethernet.Demux(buf[:nread], 0)
if err != nil {
lg.Error("recv", slog.String("err", err.Error()), slog.Int("plen", nread))
}
}
if conn == nil && listener.NumberOfReadyToAccept() > 0 {
conn, err = listener.TryAccept()
if err != nil {
lg.Error("tryaccept", slog.String("err", err.Error()))
}
accepted++
hdr.Reset(nil)
lg.Info("ACCEPT!")
}
if conn != nil {
done, err := doHTTP(conn, &hdr)
if done {
lg.Info("close forever")
conn.Close()
conn = nil
}
if err != nil {
lg.Error("doHTTP", slog.String("err", err.Error()))
}
}
nw, err := stack.ethernet.Encapsulate(buf[:], -1, 0)
if err != nil {
lg.Error("handle", slog.String("err", err.Error()))
} else if nw > 0 {
frames, err := cap.CaptureEthernet(nil, buf[:nread], 0)
if err == nil {
flags := getTCPFlags(frames, buf[:nread])
if flags == 0 {
fmt.Println("OU", time.Now().Format("15:04:05.000"), frames)
} else {
fmt.Println("OU", time.Now().Format("15:04:05.000"), frames, flags.String())
}
}
_, err = tap.Write(buf[:nw])
if err != nil {
log.Fatal(err)
}
}
hit := nread > 0 || nw > 0
if hit {
// slogger.info("exchange", slog.Int("read", nread), slog.Int("nwrite", nw))
lastHit = time.Now()
} else {
if time.Since(lastHit) > standbyDuration {
time.Sleep(5 * time.Millisecond)
} else {
runtime.Gosched()
}
}
}
}
func doHTTP(conn *tcp.Conn, hdr *httpraw.Header) (done bool, err error) {
const asRequest = false
if conn.State() != tcp.StateEstablished || conn.BufferedInput() == 0 {
return false, nil // No data yet.
}
fmt.Println("state is established; check request and send response")
_, err = hdr.ReadFromLimited(conn, hdr.BufferFree())
if err != nil {
return false, err
}
needMore, err := hdr.TryParse(asRequest)
if needMore {
return false, nil
} else if err != nil {
return true, err
}
// HTTP parsed succesfully!
fmt.Println("GOT HTTP:\n", hdr.String())
fmt.Println("sending response...")
hdr.Reset(nil)
hdr.SetStatus("200", "OK")
data := `{"ok":true}`
response, err := hdr.AppendResponse(nil)
if err != nil {
return true, err
}
response = append(response, data...)
_, err = conn.Write(response)
if err != nil {
return true, err
}
err = conn.Close()
if err != nil {
return true, err
}
return true, nil
}
type Stack struct {
ethernet internet.StackEthernet
ip internet.StackIP
tcpports internet.StackPorts
arp arp.Handler
}
func (stack *Stack) Reset(ourMAC, gwMAC [6]byte, ip netip.Addr, mtu int) (err error) {
const maxNodes = 8
err = stack.ethernet.Reset6(ourMAC, gwMAC, mtu, maxNodes)
if err != nil {
return err
}
err = stack.ip.Reset(ip, maxNodes)
if err != nil {
return err
}
stack.tcpports.ResetTCP(maxNodes)
ipaddr := ip.As4()
err = stack.arp.Reset(arp.HandlerConfig{
HardwareAddr: ourMAC[:],
ProtocolAddr: ipaddr[:],
MaxQueries: 2,
MaxPending: 2,
HardwareType: 1,
ProtocolType: ethernet.TypeIPv4,
})
if err != nil {
return err
}
// Register stacks and nodes.
err = stack.ethernet.Register(&stack.arp)
if err != nil {
return err
}
err = stack.ethernet.Register(&stack.ip)
if err != nil {
return err
}
err = stack.ip.Register(&stack.tcpports)
if err != nil {
return err
}
return nil
}
func (stack *Stack) Recv(b []byte) error {
return stack.ethernet.Demux(b, 0)
}
func (stack *Stack) Send(b []byte) (int, error) {
return stack.ethernet.Encapsulate(b, -1, 0)
}
func (stack *Stack) OpenTCPListener(port uint16) (*tcp.Listener, error) {
var listener tcp.Listener
err := listener.Reset(port, naiveTCPPool{})
if err != nil {
return nil, err
}
err = stack.tcpports.Register(&listener) // Passive TCP requires no MAC setting.
if err != nil {
return nil, err
}
return &listener, nil
}
func (stack *Stack) OpenPassiveTCP(port uint16, iss tcp.Value) (*tcp.Conn, error) {
mtu := stack.ethernet.MTU()
conn := new(tcp.Conn)
err := conn.Configure(tcp.ConnConfig{
RxBuf: make([]byte, mtu),
TxBuf: make([]byte, mtu),
TxPacketQueueSize: 3,
})
if err != nil {
return nil, err
}
err = conn.OpenListen(port, iss)
if err != nil {
return nil, err
}
err = stack.tcpports.Register(conn) // Passive MAC with no listening.
if err != nil {
return nil, err
}
return conn, nil
}
const tblhex = "0123456789abcdef"
func getTCPFlags(frames []pcap.Frame, pkt []byte) (flags tcp.Flags) {
for i := range frames {
if frames[i].Protocol != lneto.IPProtoTCP {
continue
}
iflags, err := frames[i].FieldByClass(pcap.FieldClassFlags)
if err != nil {
return 0
}
v, err := frames[i].FieldAsUint(iflags, pkt)
if err != nil {
return 0
}
return tcp.Flags(v)
}
return 0
}
type naiveTCPPool struct {
}
func (naiveTCPPool) GetTCP() (*tcp.Conn, tcp.Value) {
var buf [4]byte
rand.Read(buf[:])
randVal := binary.LittleEndian.Uint32(buf[:])
var conn tcp.Conn
err := conn.Configure(tcp.ConnConfig{
RxBuf: make([]byte, 1024),
TxBuf: make([]byte, 1024),
TxPacketQueueSize: 3,
Logger: slog.Default(),
})
if err != nil {
panic(err)
}
return &conn, tcp.Value(randVal)
}
func (naiveTCPPool) PutTCP(*tcp.Conn) {}
-412
View File
@@ -1,412 +0,0 @@
package main
import (
"errors"
"fmt"
"io"
"log"
"log/slog"
"net"
"net/netip"
"os"
"runtime"
"time"
"github.com/soypat/lneto"
"github.com/soypat/lneto/arp"
"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"
"github.com/soypat/lneto/internet/pcap"
"github.com/soypat/lneto/ipv4"
"github.com/soypat/lneto/tcp"
)
const (
stackIP = "192.168.10.2"
stackPort = 80
iss = 100
)
var stackHWAddr = [6]byte{0xc0, 0xff, 0xee, 0x00, 0xde, 0xad}
func main() {
ip := netip.MustParseAddr(stackIP)
tap := ltesto.NewHTTPTapClient("http://127.0.0.1:7070")
ippfx, _ := tap.IPMask()
if !ippfx.Contains(ip) {
log.Fatal("interface does not contain stack address")
}
addrPort := netip.AddrPortFrom(ip, stackPort)
lg := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
slogger := logger{lg}
gatewayMAC, _ := tap.HardwareAddress6()
mtu, _ := tap.MTU()
lStack, handler, err := NewEthernetTCPStack(stackHWAddr, gatewayMAC, addrPort, uint16(mtu), slogger)
if err != nil {
log.Fatal(err)
}
err = handler.OpenListen(addrPort.Port(), iss)
if err != nil {
log.Fatal(err)
}
defer tap.Close()
tap.ReadDiscard() // Discard all unread content.
fmt.Println("hosting server at ", addrPort.String(), "over tap interface of mtu:", mtu, "prefix:", ippfx, "gateway:", net.HardwareAddr(gatewayMAC[:]).String())
buf := make([]byte, mtu)
var hdr httpraw.Header
hdr.Reset(make([]byte, 0, 1024))
const standbyDuration = 5 * time.Second
lastHit := time.Now().Add(-standbyDuration)
var cap pcap.PacketBreakdown
for {
nread, err := tap.Read(buf[:])
if err != nil {
slogger.error("tap-err", slog.String("err", err.Error()))
log.Fatal(err)
} else if nread > 0 {
frames, err := cap.CaptureEthernet(nil, buf[:nread], 0)
if err == nil {
flags := getTCPFlags(frames, buf[:nread])
if flags == 0 {
fmt.Println("IN", time.Now().Format("15:04:05.000"), frames)
} else {
fmt.Println("IN", time.Now().Format("15:04:05.000"), frames, flags.String())
}
}
err = lStack.RecvEth(buf[:nread])
if err != nil {
slogger.error("recv", slog.String("err", err.Error()), slog.Int("plen", nread))
}
}
doHTTP(handler, &hdr)
nw, err := lStack.HandleEth(buf[:])
if err != nil {
slogger.error("handle", slog.String("err", err.Error()))
} else if nw > 0 {
frames, err := cap.CaptureEthernet(nil, buf[:nread], 0)
if err == nil {
flags := getTCPFlags(frames, buf[:nread])
if flags == 0 {
fmt.Println("OU", time.Now().Format("15:04:05.000"), frames)
} else {
fmt.Println("OU", time.Now().Format("15:04:05.000"), frames, flags.String())
}
}
_, err = tap.Write(buf[:nw])
if err != nil {
log.Fatal(err)
}
}
hit := nread > 0 || nw > 0
if hit {
// slogger.info("exchange", slog.Int("read", nread), slog.Int("nwrite", nw))
lastHit = time.Now()
} else {
if time.Since(lastHit) > standbyDuration {
time.Sleep(5 * time.Millisecond)
} else {
runtime.Gosched()
}
}
}
}
func doHTTP(conn *tcp.Conn, hdr *httpraw.Header) error {
const asRequest = false
if conn.State() != tcp.StateEstablished || conn.BufferedInput() == 0 {
return nil // No data yet.
}
fmt.Println("state is established; check request and send response")
_, err := hdr.ReadFromLimited(conn, hdr.BufferFree())
if err != nil {
return err
}
needMore, err := hdr.TryParse(asRequest)
if err != nil {
if !needMore {
fmt.Println("IT's SO GOVER")
conn.Close()
}
return err
}
// HTTP parsed succesfully!
fmt.Println("GOT HTTP:\n", hdr.String())
fmt.Println("sending response...")
hdr.Reset(nil)
hdr.SetStatus("200", "OK")
data := `{"ok":true}`
response, err := hdr.AppendResponse(nil)
if err != nil {
return err
}
response = append(response, data...)
_, err = conn.Write(response)
if err != nil {
return err
}
err = conn.Close()
if err != nil {
return err
}
return nil
}
func NewEthernetTCPStack(ourMAC, gwMAC [6]byte, ip netip.AddrPort, mtu uint16, slogger logger) (*LinkStack, *tcp.Conn, error) {
var err error
lStack := LinkStack{
logger: slogger,
mac: ourMAC,
mtu: mtu,
gwmac: gwMAC,
}
var ipStack internet.StackIP
addr := ip.Addr()
addr4 := addr.As4()
_ = addr4
ipStack.SetAddr(addr)
lStack.Register(handler{
raddr: nil, //addr4[:],
recv: ipStack.Demux,
handle: ipStack.Encapsulate,
proto: ethernet.TypeIPv4,
lport: 0,
})
var conn tcp.Conn
err = conn.Configure(tcp.ConnConfig{
RxBuf: make([]byte, mtu),
TxBuf: make([]byte, mtu),
TxPacketQueueSize: 3,
Logger: slog.Default(),
})
if err != nil {
return nil, nil, err
}
err = conn.OpenListen(ip.Port(), 100)
if err != nil {
return nil, nil, err
}
err = ipStack.Register(&conn)
if err != nil {
return nil, nil, err
}
proto := ethernet.TypeIPv4
if ip.Addr().Is6() {
proto = ethernet.TypeIPv6
}
var narp arp.Handler
err = narp.Reset(arp.HandlerConfig{
HardwareAddr: ourMAC[:],
ProtocolAddr: ip.Addr().AsSlice(),
MaxQueries: 4,
MaxPending: 4,
HardwareType: 1,
ProtocolType: proto,
})
if err != nil {
return nil, nil, err
}
arpStack := ARPStack{
handler: narp,
}
err = lStack.Register(handler{
recv: arpStack.Recv,
handle: arpStack.Handle,
proto: ethernet.TypeARP,
})
if err != nil {
return nil, nil, err
}
return &lStack, &conn, nil
}
type handler struct {
raddr []byte
recv func([]byte, int) error
handle func([]byte, int, int) (int, error)
proto ethernet.Type
lport uint16
}
type LinkStack struct {
handlers []handler
logger
mac [6]byte
gwmac [6]byte
mtu uint16
}
func (ls *LinkStack) Register(h handler) error {
proto := h.proto
for i := range ls.handlers {
if proto == ls.handlers[i].proto {
return errors.New("protocol already registered")
}
}
ls.handlers = append(ls.handlers, h)
return nil
}
func (ls *LinkStack) RecvEth(ethFrame []byte) (err error) {
efrm, err := ethernet.NewFrame(ethFrame)
if err != nil {
return err
}
etype := efrm.EtherTypeOrSize()
dstaddr := efrm.DestinationHardwareAddr()
var vld lneto.Validator
if !efrm.IsBroadcast() && ls.mac != *dstaddr {
goto DROP
}
efrm.ValidateSize(&vld)
if err := vld.ErrPop(); err != nil {
return err
}
for i := range ls.handlers {
h := &ls.handlers[i]
if h.proto == etype {
return h.recv(efrm.Payload(), 0)
}
}
DROP:
ls.info("LinkStack:drop-packet", slog.String("dsthw", net.HardwareAddr(dstaddr[:]).String()), slog.String("ethertype", efrm.EtherTypeOrSize().String()))
return nil
}
func (ls *LinkStack) HandleEth(dst []byte) (n int, err error) {
mtu := ls.mtu
if len(dst) < int(mtu) {
return 0, io.ErrShortBuffer
}
efrm, err := ethernet.NewFrame(dst)
if err != nil {
return 0, err
}
copy(efrm.DestinationHardwareAddr()[:], ls.gwmac[:]) // default set the gateway.
for i := range ls.handlers {
h := &ls.handlers[i]
n, err = h.handle(dst[:mtu], 14, 14)
if err != nil {
ls.error("handling", slog.String("proto", ethernet.Type(h.proto).String()), slog.String("err", err.Error()))
continue
}
if n > 0 {
// Found packet
*efrm.SourceHardwareAddr() = ls.mac
efrm.SetEtherType(ethernet.Type(h.proto))
return n + 14, nil
}
}
return 0, err
}
type ARPStack struct {
handler arp.Handler
}
func (as *ARPStack) Protocol() uint32 { return uint32(ethernet.TypeARP) }
func (as *ARPStack) Recv(EtherFrame []byte, arpOff int) error {
afrm, _ := arp.NewFrame(EtherFrame[arpOff:])
slog.Info("recv", slog.String("in", afrm.String()))
return as.handler.Demux(EtherFrame, arpOff)
}
func (as *ARPStack) Handle(EtherFrame []byte, offsetToIP, arpOff int) (int, error) {
n, err := as.handler.Encapsulate(EtherFrame, offsetToIP, arpOff)
if err != nil || n == 0 {
return 0, err
}
afrm, _ := arp.NewFrame(EtherFrame[arpOff:])
hwaddr, _ := afrm.Target()
efrm, _ := ethernet.NewFrame(EtherFrame)
copy(efrm.DestinationHardwareAddr()[:], hwaddr)
slog.Info("handle", slog.String("out", afrm.String()))
return n, err
}
type logger struct {
log *slog.Logger
}
func (l logger) error(msg string, attrs ...slog.Attr) {
internal.LogAttrs(l.log, slog.LevelError, msg, attrs...)
}
func (l logger) info(msg string, attrs ...slog.Attr) {
internal.LogAttrs(l.log, slog.LevelInfo, msg, attrs...)
}
func (l logger) warn(msg string, attrs ...slog.Attr) {
internal.LogAttrs(l.log, slog.LevelWarn, msg, attrs...)
}
func (l logger) debug(msg string, attrs ...slog.Attr) {
internal.LogAttrs(l.log, slog.LevelDebug, msg, attrs...)
}
func (l logger) trace(msg string, attrs ...slog.Attr) {
internal.LogAttrs(l.log, internal.LevelTrace, msg, attrs...)
}
func debugEthPacket(logger *slog.Logger, prefix string, b []byte) {
frm, err := ethernet.NewFrame(b)
if err != nil {
return
}
if frm.EtherTypeOrSize() != ethernet.TypeIPv4 {
return
}
ihdr, err := ipv4.NewFrame(frm.Payload())
if err != nil {
return
}
if ihdr.Protocol() != lneto.IPProtoTCP {
return
}
thdr, err := tcp.NewFrame(ihdr.Payload())
if err != nil {
return
}
fmt.Println(prefix, ihdr.String()+" TCP:"+thdr.String())
payload := thdr.Payload()
if len(payload) > 0 {
fmt.Println("PAYLOAD:", string(payload))
}
}
func debugHex(b []byte) string {
var d []byte
for i := 0; i < len(b); i++ {
c1 := tblhex[b[i]&0xf]
c2 := tblhex[b[i]>>4]
d = append(d, c2, c1, ' ')
}
return string(d)
}
const tblhex = "0123456789abcdef"
func getTCPFlags(frames []pcap.Frame, pkt []byte) (flags tcp.Flags) {
for i := range frames {
if frames[i].Protocol != lneto.IPProtoTCP {
continue
}
iflags, err := frames[i].FieldByClass(pcap.FieldClassFlags)
if err != nil {
return 0
}
v, err := frames[i].FieldAsUint(iflags, pkt)
if err != nil {
return 0
}
return tcp.Flags(v)
}
return 0
}
@@ -48,11 +48,9 @@ func run() (err error) {
flagHostToResolve = ""
flagRequestedIP = ""
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.")
flag.BoolVar(&flagUseHTTP, "ihttp", flagUseHTTP, "Use HTTP tap interface.")
flag.StringVar(&flagHostToResolve, "host", flagHostToResolve, "Hostname to resolve via DNS.")
@@ -60,6 +58,10 @@ func run() (err error) {
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
@@ -69,12 +71,12 @@ func run() (err error) {
os.WriteFile("xnet.pprof", b.Bytes(), 0777)
}()
}
fmt.Println("softrand", softRand)
_, err = dns.NewName(flagHostToResolve)
if err != nil {
flag.Usage()
return err
}
fmt.Println("softrand", softRand)
var iface ltesto.Interface
if flagUseHTTP {
iface = ltesto.NewHTTPTapClient("http://127.0.0.1:7070")
@@ -272,65 +274,65 @@ func run() (err error) {
TxBuf: make([]byte, mtu),
TxPacketQueueSize: 3,
})
if flagHTTPGet {
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)
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)
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("</html>")) {
// We've received the last part of the HTML. we're done.
break
}
}
if len(page) == 0 {
return err
}
timeHTTPRcv()
os.Stdout.Write(page)
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)
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)
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("</html>")) {
// 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
}