diff --git a/README.md b/README.md index fe65c02..36c5264 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,9 @@ Userspace networking primitives. - Stack can be fuzz tested very fast: - `go test ./x/xnet/ -run FuzzStackAsyncHTTP -fuzz=.` fuzzes Ethernet/IP/TCP/HTTP stack with 170k HTTP exchanges per second on 12 core machine: `fuzz: elapsed: 4m9s, execs: 42649941 (169428/sec), new interesting: 5 (total: 52)` +## [`min-working-example`](./examples/min-working-example/) - Quick lneto showcase +Get a quick showcase of how lneto can be configured and how to get a TCP listening server up and running. + ## `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. @@ -36,6 +39,7 @@ You may try lneto out on linux with the [xcurl example](./examples/xcurl/) which 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. diff --git a/examples/min-working-example/main-mwe.go b/examples/min-working-example/main-mwe.go new file mode 100644 index 0000000..dfcfdc7 --- /dev/null +++ b/examples/min-working-example/main-mwe.go @@ -0,0 +1,173 @@ +package main + +import ( + "context" + "fmt" + "net" + "net/netip" + "os" + "syscall" + "time" + + "github.com/soypat/lneto" + "github.com/soypat/lneto/ethernet" + "github.com/soypat/lneto/x/xnet" +) + +const pollTime = 5 * time.Millisecond +const protoTimeout = 5 * time.Second +const protoRetries = 3 + +const ( + tcpBufsize = 2048 + tcpPacketQueueSize = 4 + // Number of connections in TCP pool. + tcpConnPoolSize = 20 + // EstablishedTimeout sets the timeout for a TCP connection since it is acquired until it is established. + // If the connection does not establish in this time it will be closed by the pool. + tcpEstablishedTimeout = 4 * time.Second + tcpCloseTimeout = protoTimeout +) + +var nanotime = func() int64 { + return time.Now().UnixNano() +} + +var network Interface + +type Interface interface { + SendEth(frame []byte) error + RecvEth(dst []byte) (int, error) + HardwareAddress6() ([6]byte, error) + MaxFrameLength() (int, error) +} + +func main() { + var stack xnet.StackAsync + ctx := context.Background() + if err := run(ctx, &stack); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func run(ctx context.Context, stack *xnet.StackAsync) error { + hwaddr, err := network.HardwareAddress6() + if err != nil { + return err + } + framelen, err := network.MaxFrameLength() + if err != nil { + return err + } + err = stack.Reset(xnet.StackConfig{ + Hostname: "lneto-mwe", + RandSeed: time.Now().UnixNano(), + // A passive TCP listener to many remote ports takes up one spot, active TCP clients to one remote port take up a spot. + MaxTCPConns: 1, + // MaxUDPConns: 1 , // For MDNS support. + // AcceptMulticast: true, // For MDNS. + MTU: uint16(framelen - ethernet.MaxOverheadSize), + HardwareAddress: hwaddr, + }) + if err != nil { + return fmt.Errorf("configuring stack: %w", err) + } + ctx, cancel := context.WithCancel(ctx) + defer cancel() + // Start stack routine to leverage easy to use blocking+retrying APIs. + // Other option is to instead use async API which leads to more verbose + // and more stateful code. + go stackLoop(ctx, stack) + rstack := stack.StackRetrying(pollTime) + results, err := rstack.DoDHCPv4([4]byte{}, protoTimeout, protoRetries) + if err != nil { + return fmt.Errorf("doing DHCP: %w", err) + } + err = stack.AssimilateDHCPResults(results) + if err != nil { + return fmt.Errorf("assimilating DHCP: %w", err) + } + gateway, err := rstack.DoResolveHardwareAddress6(results.Router, protoTimeout, protoRetries) + if err != nil { + return fmt.Errorf("resolving router MAC: %w", err) + } + stack.SetGateway6(gateway) + berkstack := stack.StackBlocking(pollTime).StackBerkeley(xnet.BerkeleyConfig{ + ListenerPoolConfig: xnet.TCPPoolConfig{ + PoolSize: tcpConnPoolSize, + QueueSize: tcpPacketQueueSize, + TxBufSize: tcpBufsize, + RxBufSize: tcpBufsize, + NanoTime: nanotime, + EstablishedTimeout: tcpEstablishedTimeout, + ClosingTimeout: tcpCloseTimeout, + }, + }) + + laddr := net.TCPAddrFromAddrPort(netip.AddrPortFrom(results.AssignedAddr, 80)) + // raddr := net.TCPAddr{} // If active (client) connection then set raddr in which case a net.Conn type is returned. + const sockstream = 0x1 + c, err := berkstack.Socket(ctx, "tcp", syscall.AF_INET, sockstream, laddr, nil) + if err != nil { + return fmt.Errorf("creating AF_INET stream socket: %w", err) + } + listener := c.(net.Listener) + for ctx.Err() == nil { + time.Sleep(pollTime) + conn, err := listener.Accept() + if err != nil { + fmt.Println("conn failed:", err) + } + go handleConn(conn) + } + return nil +} + +func handleConn(conn net.Conn) { + // Always close conn on finishing work so connection is reused. + defer conn.Close() + + // Do something with conn. + conn.Write([]byte("Hello!")) +} + +func stackLoop(ctx context.Context, stack *xnet.StackAsync) { + // Enables logging of packets. + var cap xnet.CapturePrinter + must(cap.Configure(os.Stdout, xnet.CapturePrinterConfig{ + Now: time.Now, + TimePrecision: 3, + })) + frameLength, _ := network.MaxFrameLength() + buf := make([]byte, frameLength) + for ctx.Err() == nil { + nwrite, err := stack.Encapsulate(buf[:], -1, 0) + if err != nil { + fmt.Println("encaps err:", err) + } else if nwrite > 0 { + network.SendEth(buf[:nwrite]) + cap.PrintPacket("OUT", buf[:nwrite]) + } + nread, err := network.RecvEth(buf[:]) + if err != nil { + fmt.Println("network read err:", err) + } else if nread > 0 { + err = stack.Demux(buf[:nread], 0) + if err != nil && err != lneto.ErrPacketDrop { + fmt.Println("demux err:", err) + } else { + cap.PrintPacket("IN ", buf[:nread]) + } + } + if nwrite == 0 && nread == 0 { + time.Sleep(pollTime) + } + } +} + +func must(err error) { + if err != nil { + panic(err) + } +}