mirror of
https://github.com/soypat/lneto.git
synced 2026-08-10 09:53:44 +00:00
d05cd14018
* httphi: RouterConfig refactor to enable DefaultRouterConfig * RequestHeader not case sensitive anymore * httphi: use DefaultRouterConfig in examples * httphi: remove gated stage complexity Misusing Stage methods by calling them once header has been written is totally harmless as far as I can tell. We simplify the codebase on this occasion by removing the headerWritten check for all stage methods * httphi: improve APIs * httphi: remove status
400 lines
11 KiB
Go
400 lines
11 KiB
Go
//go:build !tinygo && linux
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
_ "embed"
|
|
"encoding/hex"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"log/slog"
|
|
"math"
|
|
"net"
|
|
"net/netip"
|
|
"os"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/soypat/lneto"
|
|
"github.com/soypat/lneto/ethernet"
|
|
"github.com/soypat/lneto/http/httphi"
|
|
"github.com/soypat/lneto/internal"
|
|
"github.com/soypat/lneto/internal/ltesto"
|
|
"github.com/soypat/lneto/internet/pcap"
|
|
"github.com/soypat/lneto/ipv4"
|
|
"github.com/soypat/lneto/tcp"
|
|
"github.com/soypat/lneto/x/xnet"
|
|
)
|
|
|
|
//go:embed index.html
|
|
var indexhtml string
|
|
|
|
// Router memory. The router allocates all of it on Configure and never again,
|
|
// so these are the whole cost of serving HTTP over the stack.
|
|
const (
|
|
httpConnMemoryUse = 4 * 1024
|
|
// One exchange is allocated per worker, and a worker holds its exchange for
|
|
// the whole request, so this is what bounds requests served at once.
|
|
numWorkers = 2
|
|
// requestTimeout drops a peer that opens a connection and then stalls,
|
|
// rather than letting it hold one of the workers.
|
|
requestTimeout = 10 * time.Second
|
|
)
|
|
|
|
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
|
|
flagNoPcap = false
|
|
flagPort = 80
|
|
)
|
|
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.BoolVar(&flagNoPcap, "nopcap", flagNoPcap, "Disable pcap logging.")
|
|
flag.IntVar(&flagPort, "port", flagPort, "Port to listen on.")
|
|
flag.Usage = func() {
|
|
fmt.Fprintf(os.Stderr, "httpserver is a minimal HTTP server using the lneto networking stack.\n")
|
|
flag.PrintDefaults()
|
|
}
|
|
flag.Parse()
|
|
fmt.Println("softrand", softRand)
|
|
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
|
|
}
|
|
err = bridge.SetReadTimeout(5 * time.Millisecond)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
iface = bridge
|
|
}
|
|
}
|
|
defer iface.Close()
|
|
|
|
nicHW, err := iface.HardwareAddress6()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
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(), "mtu:", mtu, "addr:", nicAddr.String())
|
|
|
|
var stack xnet.StackAsync
|
|
err = stack.Reset(xnet.StackConfig{
|
|
Hostname: "httpserver",
|
|
RandSeed: softRand,
|
|
HardwareAddress: nicHW,
|
|
MTU: uint16(mtu),
|
|
MaxActiveTCPPorts: 1000,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Loop goroutine handles packet encapsulation/decapsulation.
|
|
go func() {
|
|
lastAction := time.Now()
|
|
buf := make([]byte, math.MaxUint16)
|
|
var cap pcap.PacketBreakdown
|
|
var frames []pcap.Frame
|
|
pf := pcap.Formatter{
|
|
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 {
|
|
if flagNoPcap {
|
|
return nil
|
|
}
|
|
frames, err = cap.CaptureEthernet(frames[:0], pkt, 0)
|
|
if err != nil {
|
|
pkt := hex.EncodeToString(pkt)
|
|
slog.Error(err.Error(), slog.Any("pkt", pkt))
|
|
return err
|
|
}
|
|
pfbuf = fmt.Appendf(pfbuf[:0], "%-3s %3d", context, len(pkt))
|
|
pfbuf = append(pfbuf, ' ', '[')
|
|
pfbuf, err = pf.FormatFrames(pfbuf, frames, pkt)
|
|
|
|
pfbuf = bytes.ReplaceAll(pfbuf, ipv4.AppendFormatAddr(nil, stack.Addr4()), []byte("us"))
|
|
pfbuf = bytes.ReplaceAll(pfbuf, ethernet.AppendAddr(nil, stack.HardwareAddr()), []byte("us"))
|
|
pfbuf = append(pfbuf, ']', '\n')
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = os.Stdout.Write(pfbuf)
|
|
return err
|
|
}
|
|
for {
|
|
nwrite, err := stack.EgressEthernet(buf[:])
|
|
if err != nil {
|
|
log.Println("ERR:ENCAPSULATE", err)
|
|
} else if nwrite > 0 {
|
|
err = logFrames("OUT", buf[:nwrite])
|
|
if err != nil {
|
|
log.Println("ERR:OUTLOG", err)
|
|
}
|
|
n, err := iface.Write(buf[:nwrite])
|
|
if err != nil {
|
|
log.Fatal("goroutine encapsulate:", err)
|
|
} else if n != nwrite {
|
|
log.Fatalf("mismatch written bytes %d!=%d", nwrite, n)
|
|
}
|
|
}
|
|
|
|
clear(buf[:nwrite])
|
|
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("goroutine read:", err)
|
|
} else if nread > 0 {
|
|
err = stack.IngressEthernet(buf[:nread])
|
|
if !errors.Is(err, lneto.ErrPacketDrop) {
|
|
err = logFrames("IN", buf[:nread])
|
|
if err != nil {
|
|
log.Println("ERR:INLOG", err)
|
|
}
|
|
}
|
|
}
|
|
clear(buf[:nread])
|
|
if nread == 0 && nwrite == 0 && time.Since(lastAction) > 4*time.Second {
|
|
time.Sleep(5 * time.Millisecond)
|
|
} else {
|
|
lastAction = time.Now()
|
|
runtime.Gosched()
|
|
}
|
|
}
|
|
}()
|
|
|
|
rstack := stack.StackRetrying(stackBackoff)
|
|
|
|
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)
|
|
}
|
|
slog.Info("dhcp-complete", slog.String("assignedIP", string(ipv4.AppendFormatAddr(nil, results.AssignedAddr4))), slog.String("routerIP", results.Router.String()))
|
|
|
|
const (
|
|
arpTimeout = 2 * time.Second
|
|
arpRetries = 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.SetGatewayHardwareAddr(routerHw)
|
|
|
|
svPort := uint16(flagPort)
|
|
fmt.Printf("Listening on %s:%d\n", ipv4.AppendFormatAddr(nil, stack.Addr4()), svPort)
|
|
|
|
// Routes are registered before Configure: the router reads the mux to size
|
|
// the exchanges it allocates, and refuses a mux with nothing registered.
|
|
server := httpServer{start: time.Now()}
|
|
// "{$}" matches the empty path and nothing else, so anything unregistered
|
|
// gets a 404 rather than the index page.
|
|
server.handle("GET /{$}", server.index)
|
|
server.handle("GET /stats", server.stats)
|
|
|
|
var router httphi.Router
|
|
cfg := httphi.DefaultRouterConfig(numWorkers, httpConnMemoryUse, server.mux.MaxPathValues())
|
|
cfg.Logger = slog.Default()
|
|
err = router.Configure(&server.mux, cfg)
|
|
if err != nil {
|
|
return fmt.Errorf("configuring HTTP router: %w", err)
|
|
}
|
|
defer router.Shutdown()
|
|
|
|
// Serve connections in a loop.
|
|
for {
|
|
var conn tcp.Conn
|
|
conn.Configure(tcp.ConnConfig{
|
|
RxBuf: make([]byte, mtu),
|
|
TxBuf: make([]byte, mtu),
|
|
TxPacketQueueSize: 3,
|
|
RWBackoff: tcpBackoff,
|
|
})
|
|
err = stack.ListenTCP4(&conn, svPort)
|
|
if err != nil {
|
|
return fmt.Errorf("listen TCP: %w", err)
|
|
}
|
|
fmt.Println("waiting for connection...")
|
|
|
|
// Wait for TCP handshake to complete.
|
|
deadline := time.Now().Add(60 * time.Second)
|
|
for conn.State() != tcp.StateEstablished {
|
|
if time.Now().After(deadline) {
|
|
conn.Abort()
|
|
fmt.Println("listen timeout, retrying...")
|
|
break
|
|
}
|
|
time.Sleep(5 * time.Millisecond)
|
|
}
|
|
if conn.State() != tcp.StateEstablished {
|
|
continue
|
|
}
|
|
fmt.Println("connection established from", net.IP(conn.RemoteAddr()).String())
|
|
// The connection owns the idle policy: a peer that stalls fails its read
|
|
// instead of holding a worker. conn is declared inside the loop, so the
|
|
// worker keeps serving this one while the next iteration listens anew.
|
|
conn.SetDeadline(time.Now().Add(requestTimeout))
|
|
err = router.Handle(&conn)
|
|
if err != nil {
|
|
// Every worker is busy. Dropping is the backpressure that keeps the
|
|
// stack's memory bounded, see numWorkers.
|
|
slog.Warn("dropped connection", slog.String("err", err.Error()))
|
|
conn.Abort()
|
|
}
|
|
}
|
|
}
|
|
|
|
// httpServer holds what the handlers answer with. Routes are registered on its
|
|
// mux before [httphi.Router.Configure] runs, which reads the mux to size the
|
|
// path values every exchange must hold.
|
|
type httpServer struct {
|
|
mux httphi.MuxSlice
|
|
served atomic.Uint64
|
|
start time.Time
|
|
}
|
|
|
|
// handle registers handler and wraps it in the logging and counting every
|
|
// request goes through, i.e: the "< GET /" line this example has always printed.
|
|
func (sv *httpServer) handle(pattern string, handler httphi.HandlerFunc) {
|
|
sv.mux.Handle(pattern, func(exch *httphi.Exchange) {
|
|
sv.served.Add(1)
|
|
fmt.Printf("< %s %s\n", exch.RequestMethodBytes(), exch.RequestTarget())
|
|
handler(exch)
|
|
})
|
|
}
|
|
|
|
// index serves the embedded page. The body goes straight to the connection, so
|
|
// only its header ever sits in the exchange's buffer and the page's size does
|
|
// not enter into how the router is configured.
|
|
func (sv *httpServer) index(exch *httphi.Exchange) {
|
|
exch.RespondString(httphi.StatusOK, "text/html", indexhtml)
|
|
}
|
|
|
|
// stats reports what the stack has served, which is the quickest way to tell a
|
|
// working link from a page that came out of a browser cache.
|
|
func (sv *httpServer) stats(exch *httphi.Exchange) {
|
|
var buf [128]byte
|
|
body := append(buf[:0], "requests served: "...)
|
|
body = strconv.AppendUint(body, sv.served.Load(), 10)
|
|
body = append(body, "\nuptime: "...)
|
|
body = append(body, prettyDuration(time.Since(sv.start))...)
|
|
body = append(body, '\n')
|
|
exch.Respond(httphi.StatusOK, "text/plain", body)
|
|
}
|
|
|
|
func clear(buf []byte) {
|
|
for i := range buf {
|
|
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
|
|
}
|
|
|
|
func stackBackoff(consecutiveBackoffs uint) time.Duration {
|
|
if consecutiveBackoffs < 10 {
|
|
return time.Millisecond
|
|
}
|
|
return 10 * time.Millisecond
|
|
}
|
|
|
|
func tcpBackoff(consecutiveBackoffs uint) time.Duration {
|
|
const (
|
|
minWait = uint32(time.Microsecond)
|
|
maxWait = 5 * uint32(time.Millisecond)
|
|
maxShift = 22
|
|
_overflowCheck = minWait << maxShift
|
|
)
|
|
shifted := minWait << min(consecutiveBackoffs, maxShift)
|
|
wait := min(shifted, maxWait)
|
|
return time.Duration(wait)
|
|
}
|