mirror of
https://github.com/soypat/lneto.git
synced 2026-08-15 12:23:44 +00:00
HTTP server example and DHCP Server rewrite (#38)
* improvements to dhcpv4 server * begin adding examples/httpserver * add better VLAN tagging methods * pcap: add error printing; fix bug in CRC * ipv4: ToS and Flags construction and flag manipulation improvements * add dhcp interception to httptap and improve httpserver example
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Arbitrary Waveform Generator Control</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
text-align: center;
|
||||
}
|
||||
.chip-control {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.chip-name {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.form-group {
|
||||
margin: 10px 0;
|
||||
}
|
||||
label {
|
||||
display: inline-block;
|
||||
width: 120px;
|
||||
font-weight: bold;
|
||||
color: #555;
|
||||
}
|
||||
input[type="number"] {
|
||||
width: 200px;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
button {
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #2980b9;
|
||||
}
|
||||
.current-value {
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
margin-left: 130px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>AD9850 DDS Control</h1>
|
||||
<div>
|
||||
<!-- Begin Poetry -->
|
||||
<p>
|
||||
Whose woods these are I think I know.<br>
|
||||
His house is in the village though;<br>
|
||||
He will not see me stopping here<br>
|
||||
To watch his woods fill up with snow.<br>
|
||||
<br>
|
||||
My little horse must think it queer<br>
|
||||
To stop without a farmhouse near<br>
|
||||
Between the woods and frozen lake<br>
|
||||
The darkest evening of the year.<br>
|
||||
<br>
|
||||
He gives his harness bells a shake<br>
|
||||
To ask if there is some mistake.<br>
|
||||
The only other sound’s the sweep<br>
|
||||
Of easy wind and downy flake.<br>
|
||||
<br>
|
||||
The woods are lovely, dark and deep,<br>
|
||||
But I have promises to keep,<br>
|
||||
And miles to go before I sleep,<br>
|
||||
</div
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,356 @@
|
||||
//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"
|
||||
"time"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
"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/pcap"
|
||||
"github.com/soypat/lneto/tcp"
|
||||
"github.com/soypat/lneto/x/xnet"
|
||||
)
|
||||
|
||||
//go:embed index.html
|
||||
var indexhtml string
|
||||
|
||||
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),
|
||||
MaxTCPConns: 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, stack.Addr().AppendTo(nil), []byte("us"))
|
||||
pfbuf = bytes.ReplaceAll(pfbuf, ethernet.AppendAddr(nil, stack.HardwareAddress()), []byte("us"))
|
||||
pfbuf = append(pfbuf, ']', '\n')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = os.Stdout.Write(pfbuf)
|
||||
return err
|
||||
}
|
||||
for {
|
||||
nwrite, err := stack.Encapsulate(buf[:], -1, 0)
|
||||
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.Demux(buf[:nread], 0)
|
||||
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(5 * time.Millisecond)
|
||||
|
||||
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", results.AssignedAddr.String()), 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.SetGateway6(routerHw)
|
||||
|
||||
svPort := uint16(flagPort)
|
||||
fmt.Printf("Listening on %s:%d\n", stack.Addr().String(), svPort)
|
||||
|
||||
// Serve connections in a loop.
|
||||
for {
|
||||
var conn tcp.Conn
|
||||
conn.Configure(tcp.ConnConfig{
|
||||
RxBuf: make([]byte, mtu),
|
||||
TxBuf: make([]byte, mtu),
|
||||
TxPacketQueueSize: 3,
|
||||
})
|
||||
err = stack.ListenTCP(&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())
|
||||
go func() {
|
||||
err = handleConnection(&conn)
|
||||
if err != nil {
|
||||
fmt.Println("handle error:", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func handleConnection(conn *tcp.Conn) error {
|
||||
conn.SetDeadline(time.Now().Add(10 * time.Second))
|
||||
|
||||
// Read HTTP request.
|
||||
var hdr httpraw.Header
|
||||
var needMore bool = true
|
||||
for needMore {
|
||||
_, err := hdr.ReadFromLimited(conn, 1024)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading request: %w", err)
|
||||
}
|
||||
const asResponse = false
|
||||
needMore, err = hdr.TryParse(asResponse)
|
||||
if err != nil && !needMore {
|
||||
return fmt.Errorf("parsing request: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
method := string(hdr.Method())
|
||||
uri := string(hdr.RequestURI())
|
||||
fmt.Printf("< %s %s\n", method, uri)
|
||||
|
||||
// Build response body.
|
||||
|
||||
// Build HTTP response.
|
||||
var resp httpraw.Header
|
||||
resp.SetProtocol("HTTP/1.1")
|
||||
resp.SetStatus("200", "OK")
|
||||
resp.Set("Content-Type", "text/html")
|
||||
resp.Set("Content-Length", strconv.Itoa(len(indexhtml)))
|
||||
resp.Set("Connection", "close")
|
||||
response, err := resp.AppendResponse(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("building response: %w", err)
|
||||
}
|
||||
response = append(response, indexhtml...)
|
||||
|
||||
// Send response.
|
||||
_, err = conn.Write(response)
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing response: %w", err)
|
||||
}
|
||||
err = conn.Flush()
|
||||
if err != nil {
|
||||
return fmt.Errorf("flushing response: %w", err)
|
||||
}
|
||||
fmt.Printf("> %d bytes sent\n", len(response))
|
||||
|
||||
conn.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
//go:build !tinygo && linux
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"sync"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
"github.com/soypat/lneto/arp"
|
||||
"github.com/soypat/lneto/dhcpv4"
|
||||
"github.com/soypat/lneto/ethernet"
|
||||
"github.com/soypat/lneto/internal/ltesto"
|
||||
"github.com/soypat/lneto/ipv4"
|
||||
"github.com/soypat/lneto/udp"
|
||||
)
|
||||
|
||||
const (
|
||||
sizeEthernet = 14
|
||||
sizeIPv4 = 20
|
||||
sizeUDP = 8
|
||||
sizeARPv4 = 28
|
||||
sizeDHCPMin = dhcpv4.OptionsOffset + 256 // Minimum space for DHCP frame + options.
|
||||
)
|
||||
|
||||
// dhcpInterceptor wraps an ltesto.Interface and intercepts DHCP traffic.
|
||||
// DHCP packets from the client are handled by an embedded dhcpv4.Server
|
||||
// and never forwarded to the real interface. DHCP responses are returned
|
||||
// on subsequent Read calls. All non-DHCP traffic passes through unchanged.
|
||||
type dhcpInterceptor struct {
|
||||
mu sync.Mutex
|
||||
inner ltesto.Interface
|
||||
sv dhcpv4.Server
|
||||
|
||||
// Server network identity.
|
||||
svMAC [6]byte
|
||||
svIP [4]byte
|
||||
|
||||
// Pending ARP reply.
|
||||
arpReply [sizeEthernet + sizeARPv4]byte
|
||||
arpReady bool
|
||||
|
||||
// ARP cache for gateway forwarding: maps IP→MAC from snooped traffic.
|
||||
arpCache [8]arpEntry
|
||||
}
|
||||
|
||||
type arpEntry struct {
|
||||
mac [6]byte
|
||||
ip [4]byte
|
||||
}
|
||||
|
||||
// newDHCPInterceptor creates a dhcpInterceptor that wraps iface and serves
|
||||
// DHCP from the given server address and subnet.
|
||||
func newDHCPInterceptor(iface ltesto.Interface, svIP [4]byte, svMAC [6]byte, subnet netip.Prefix) (*dhcpInterceptor, error) {
|
||||
d := &dhcpInterceptor{
|
||||
inner: iface,
|
||||
svMAC: svMAC,
|
||||
svIP: svIP,
|
||||
}
|
||||
err := d.sv.Configure(dhcpv4.ServerConfig{
|
||||
ServerAddr: svIP,
|
||||
Gateway: svIP,
|
||||
DNS: [4]byte{8, 8, 8, 8},
|
||||
Subnet: subnet,
|
||||
})
|
||||
return d, err
|
||||
}
|
||||
|
||||
func (d *dhcpInterceptor) Write(b []byte) (int, error) {
|
||||
if d.isARPRequestForUs(b) {
|
||||
d.mu.Lock()
|
||||
d.buildARPReply(b)
|
||||
d.mu.Unlock()
|
||||
return len(b), nil
|
||||
}
|
||||
if isDHCPRequest(b) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
dhcpOff := dhcpOffset(b)
|
||||
if dhcpOff < 0 {
|
||||
return d.inner.Write(b) // Malformed, pass through.
|
||||
}
|
||||
err := d.sv.Demux(b, dhcpOff)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("dhcp server demux: %w", err)
|
||||
}
|
||||
return len(b), nil // Consumed by DHCP server, don't forward.
|
||||
}
|
||||
d.rewriteEthernetDst(b)
|
||||
return d.inner.Write(b)
|
||||
}
|
||||
|
||||
func (d *dhcpInterceptor) Read(b []byte) (int, error) {
|
||||
d.mu.Lock()
|
||||
if d.arpReady {
|
||||
n := copy(b, d.arpReply[:])
|
||||
d.arpReady = false
|
||||
d.mu.Unlock()
|
||||
return n, nil
|
||||
}
|
||||
n, err := d.buildDHCPResponse(b)
|
||||
d.mu.Unlock()
|
||||
if n > 0 {
|
||||
return n, nil
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
n, err = d.inner.Read(b)
|
||||
if n >= sizeEthernet+sizeARPv4 && binary.BigEndian.Uint16(b[12:14]) == uint16(ethernet.TypeARP) {
|
||||
d.snoopARP(b[:n])
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// buildDHCPResponse tries to get a pending DHCP response from the server and
|
||||
// wraps it in Ethernet + IPv4 + UDP headers. Returns 0 if no response pending.
|
||||
// Caller must hold d.mu.
|
||||
func (d *dhcpInterceptor) buildDHCPResponse(buf []byte) (int, error) {
|
||||
if len(buf) < sizeEthernet+sizeIPv4+sizeUDP+sizeDHCPMin {
|
||||
return 0, nil
|
||||
}
|
||||
// Build Ethernet+IPv4 headers since DHCP server may use hardware/ip addr.
|
||||
efrm, _ := ethernet.NewFrame(buf)
|
||||
*efrm.DestinationHardwareAddr() = [6]byte{}
|
||||
*efrm.SourceHardwareAddr() = d.svMAC
|
||||
efrm.SetEtherType(ethernet.TypeIPv4)
|
||||
|
||||
ifrm, _ := ipv4.NewFrame(buf[sizeEthernet:])
|
||||
ifrm.SetVersionAndIHL(4, 5)
|
||||
ifrm.SetToS(0)
|
||||
ifrm.SetFlags(ipv4.FlagDontFragment)
|
||||
ifrm.SetTTL(64)
|
||||
ifrm.SetProtocol(lneto.IPProtoUDP)
|
||||
*ifrm.SourceAddr() = d.svIP
|
||||
*ifrm.DestinationAddr() = [4]byte{}
|
||||
|
||||
// Build UDP header.
|
||||
ufrm, _ := udp.NewFrame(buf[sizeEthernet+sizeIPv4:])
|
||||
ufrm.SetSourcePort(dhcpv4.DefaultServerPort)
|
||||
ufrm.SetDestinationPort(dhcpv4.DefaultClientPort)
|
||||
|
||||
dhcpStart := sizeEthernet + sizeIPv4 + sizeUDP
|
||||
// Ask DHCP server to fill in the payload. offsetToIP=sizeEthernet so
|
||||
// the server can set IP src/dst via internal.SetIPAddrs.
|
||||
dhcpLen, err := d.sv.Encapsulate(buf, sizeEthernet, dhcpStart)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("dhcp server encapsulate: %w", err)
|
||||
}
|
||||
if dhcpLen == 0 {
|
||||
return 0, nil // No pending response.
|
||||
}
|
||||
|
||||
totalIPLen := uint16(sizeIPv4 + sizeUDP + dhcpLen)
|
||||
udpLen := uint16(sizeUDP + dhcpLen)
|
||||
totalFrameLen := sizeEthernet + int(totalIPLen)
|
||||
|
||||
// DHCP responses must be broadcast since the client doesn't have
|
||||
// an IP configured yet and the stack would drop unicast packets.
|
||||
*efrm.DestinationHardwareAddr() = ethernet.BroadcastAddr()
|
||||
*ifrm.DestinationAddr() = [4]byte{255, 255, 255, 255}
|
||||
ifrm.SetTotalLength(totalIPLen)
|
||||
ufrm.SetLength(udpLen)
|
||||
// Source and destination IPs already set by dhcpv4.Server.Encapsulate.
|
||||
ifrm.SetCRC(0)
|
||||
prelimCRC := ifrm.CalculateHeaderCRC()
|
||||
ifrm.SetID(^(^prelimCRC * 37))
|
||||
ifrm.SetCRC(0)
|
||||
ifrm.SetCRC(ifrm.CalculateHeaderCRC())
|
||||
// Compute UDP checksum (required, the lneto stack validates it on Demux).
|
||||
ufrm.SetCRC(0)
|
||||
var udpCRC lneto.CRC791
|
||||
ifrm.CRCWriteUDPPseudo(&udpCRC, udpLen)
|
||||
ufrm.SetCRC(lneto.NeverZeroSum(udpCRC.PayloadSum16(ufrm.RawData()[:udpLen])))
|
||||
return totalFrameLen, nil
|
||||
}
|
||||
|
||||
// isDHCPRequest checks if a raw Ethernet frame is a DHCP request (client → server).
|
||||
// Checks: EtherType=IPv4, IP proto=UDP, UDP dst port=67, DHCP magic cookie.
|
||||
func isDHCPRequest(b []byte) bool {
|
||||
if len(b) < sizeEthernet+sizeIPv4+sizeUDP+dhcpv4.OptionsOffset {
|
||||
return false
|
||||
}
|
||||
// EtherType must be IPv4.
|
||||
if binary.BigEndian.Uint16(b[12:14]) != uint16(ethernet.TypeIPv4) {
|
||||
return false
|
||||
}
|
||||
// IP header length (IHL) to find UDP header.
|
||||
ihl := int(b[sizeEthernet]&0xf) * 4
|
||||
if ihl < sizeIPv4 {
|
||||
return false
|
||||
}
|
||||
ipStart := sizeEthernet
|
||||
// IP protocol must be UDP.
|
||||
if b[ipStart+9] != uint8(lneto.IPProtoUDP) {
|
||||
return false
|
||||
}
|
||||
udpStart := ipStart + ihl
|
||||
if len(b) < udpStart+sizeUDP {
|
||||
return false
|
||||
}
|
||||
// UDP destination port must be DHCP server port (67).
|
||||
dstPort := binary.BigEndian.Uint16(b[udpStart+2 : udpStart+4])
|
||||
if dstPort != dhcpv4.DefaultServerPort {
|
||||
return false
|
||||
}
|
||||
// Verify DHCP magic cookie.
|
||||
dhcpStart := udpStart + sizeUDP
|
||||
return dhcpv4.PayloadIsDHCPv4(b[dhcpStart:])
|
||||
}
|
||||
|
||||
// dhcpOffset returns the byte offset where the DHCP payload begins
|
||||
// within a raw Ethernet frame. Returns -1 if the frame is too short.
|
||||
func dhcpOffset(b []byte) int {
|
||||
if len(b) < sizeEthernet+sizeIPv4+sizeUDP {
|
||||
return -1
|
||||
}
|
||||
ihl := int(b[sizeEthernet]&0xf) * 4
|
||||
off := sizeEthernet + ihl + sizeUDP
|
||||
if off > len(b) {
|
||||
return -1
|
||||
}
|
||||
return off
|
||||
}
|
||||
|
||||
// isARPRequestForUs checks if b is an ARP request targeting d.svIP.
|
||||
func (d *dhcpInterceptor) isARPRequestForUs(b []byte) bool {
|
||||
if len(b) < sizeEthernet+sizeARPv4 {
|
||||
return false
|
||||
}
|
||||
if binary.BigEndian.Uint16(b[12:14]) != uint16(ethernet.TypeARP) {
|
||||
return false
|
||||
}
|
||||
afrm, err := arp.NewFrame(b[sizeEthernet:])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if afrm.Operation() != arp.OpRequest {
|
||||
return false
|
||||
}
|
||||
_, targetIP := afrm.Target4()
|
||||
return *targetIP == d.svIP
|
||||
}
|
||||
|
||||
// buildARPReply constructs an ARP reply in d.arpReply from the given ARP request.
|
||||
// Caller must hold d.mu.
|
||||
func (d *dhcpInterceptor) buildARPReply(request []byte) {
|
||||
reqARP, _ := arp.NewFrame(request[sizeEthernet:])
|
||||
senderHW, senderIP := reqARP.Sender4()
|
||||
|
||||
buf := d.arpReply[:]
|
||||
// Ethernet header: reply to requester.
|
||||
efrm, _ := ethernet.NewFrame(buf)
|
||||
*efrm.DestinationHardwareAddr() = *senderHW
|
||||
*efrm.SourceHardwareAddr() = d.svMAC
|
||||
efrm.SetEtherType(ethernet.TypeARP)
|
||||
|
||||
// ARP reply.
|
||||
afrm, _ := arp.NewFrame(buf[sizeEthernet:])
|
||||
afrm.SetHardware(1, 6) // Ethernet, 6-byte addresses
|
||||
afrm.SetProtocol(ethernet.TypeIPv4, 4) // IPv4, 4-byte addresses
|
||||
afrm.SetOperation(arp.OpReply)
|
||||
replySndrHW, replySndrIP := afrm.Sender4()
|
||||
*replySndrHW = d.svMAC
|
||||
*replySndrIP = d.svIP
|
||||
replyTgtHW, replyTgtIP := afrm.Target4()
|
||||
*replyTgtHW = *senderHW
|
||||
*replyTgtIP = *senderIP
|
||||
|
||||
d.arpReady = true
|
||||
}
|
||||
|
||||
// snoopARP records the sender's IP→MAC mapping from an ARP packet.
|
||||
func (d *dhcpInterceptor) snoopARP(b []byte) {
|
||||
afrm, err := arp.NewFrame(b[sizeEthernet:])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
senderHW, senderIP := afrm.Sender4()
|
||||
if *senderIP == ([4]byte{}) {
|
||||
return
|
||||
}
|
||||
d.mu.Lock()
|
||||
d.arpCacheStore(*senderHW, *senderIP)
|
||||
d.mu.Unlock()
|
||||
}
|
||||
|
||||
// rewriteEthernetDst rewrites the Ethernet destination MAC for frames
|
||||
// addressed to the gateway (svMAC). Acts as a basic IP forwarder by
|
||||
// looking up the destination IP in the ARP cache.
|
||||
func (d *dhcpInterceptor) rewriteEthernetDst(b []byte) {
|
||||
if len(b) < sizeEthernet+sizeIPv4 {
|
||||
return
|
||||
}
|
||||
// Only rewrite frames addressed to the gateway.
|
||||
if *(*[6]byte)(b[0:6]) != d.svMAC {
|
||||
return
|
||||
}
|
||||
// Only rewrite IPv4 frames.
|
||||
if binary.BigEndian.Uint16(b[12:14]) != uint16(ethernet.TypeIPv4) {
|
||||
return
|
||||
}
|
||||
dstIP := *(*[4]byte)(b[sizeEthernet+16 : sizeEthernet+20])
|
||||
d.mu.Lock()
|
||||
mac, ok := d.arpCacheLookup(dstIP)
|
||||
d.mu.Unlock()
|
||||
if ok {
|
||||
copy(b[0:6], mac[:])
|
||||
}
|
||||
}
|
||||
|
||||
// arpCacheLookup finds a MAC for the given IP. Caller must hold d.mu.
|
||||
func (d *dhcpInterceptor) arpCacheLookup(ip [4]byte) ([6]byte, bool) {
|
||||
for i := range d.arpCache {
|
||||
if d.arpCache[i].ip == ip {
|
||||
return d.arpCache[i].mac, true
|
||||
}
|
||||
}
|
||||
return [6]byte{}, false
|
||||
}
|
||||
|
||||
// arpCacheStore adds or updates an IP→MAC entry. Caller must hold d.mu.
|
||||
func (d *dhcpInterceptor) arpCacheStore(mac [6]byte, ip [4]byte) {
|
||||
// Update existing entry.
|
||||
for i := range d.arpCache {
|
||||
if d.arpCache[i].ip == ip {
|
||||
d.arpCache[i].mac = mac
|
||||
return
|
||||
}
|
||||
}
|
||||
// Find empty slot.
|
||||
for i := range d.arpCache {
|
||||
if d.arpCache[i].ip == ([4]byte{}) {
|
||||
d.arpCache[i] = arpEntry{mac: mac, ip: ip}
|
||||
return
|
||||
}
|
||||
}
|
||||
// Evict first entry.
|
||||
copy(d.arpCache[:], d.arpCache[1:])
|
||||
d.arpCache[len(d.arpCache)-1] = arpEntry{mac: mac, ip: ip}
|
||||
}
|
||||
|
||||
// Delegate remaining ltesto.Interface methods to inner.
|
||||
|
||||
func (d *dhcpInterceptor) Close() error { return d.inner.Close() }
|
||||
func (d *dhcpInterceptor) HardwareAddress6() ([6]byte, error) { return d.inner.HardwareAddress6() }
|
||||
func (d *dhcpInterceptor) MTU() (int, error) { return d.inner.MTU() }
|
||||
func (d *dhcpInterceptor) IPMask() (netip.Prefix, error) { return d.inner.IPMask() }
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -61,6 +62,21 @@ func run() error {
|
||||
iface = br
|
||||
}
|
||||
|
||||
// Wrap interface with DHCP server interceptor.
|
||||
hwaddr, err := iface.HardwareAddress6()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ipMask, err := iface.IPMask()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
svIP := ipMask.Addr().As4()
|
||||
iface, err = newDHCPInterceptor(iface, svIP, hwaddr, ipMask.Masked())
|
||||
if err != nil {
|
||||
return fmt.Errorf("DHCP interceptor: %w", err)
|
||||
}
|
||||
|
||||
sv, err := ltesto.NewHTTPTapServer(iface, flagMinMTU, flagPacketQueueSize, flagPacketQueueSize)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -72,6 +88,10 @@ func run() error {
|
||||
}
|
||||
var pfbuf []byte
|
||||
sv.OnTransfer(func(channel int, pkt []byte) {
|
||||
channelstr := "OS"
|
||||
if channel != 0 {
|
||||
channelstr = strconv.Itoa(channel) // Will not allocate for values 99 and under (stdlib).
|
||||
}
|
||||
captime := time.Now()
|
||||
frames, err := cap.CaptureEthernet(nil, pkt, 0)
|
||||
if err == nil {
|
||||
@@ -79,15 +99,15 @@ func run() error {
|
||||
pfbuf, err = pf.FormatFrames(pfbuf, frames, pkt)
|
||||
pfbuf = append(pfbuf, ']')
|
||||
if err != nil {
|
||||
fmt.Printf("%d %s !err:%s\n", channel, captime.Format("15:04:05.000"), err)
|
||||
fmt.Printf("%-2s %s !err:%s\n", channelstr, captime.Format("15:04:05.000"), err)
|
||||
} else {
|
||||
fmt.Printf("%d %s %s\n", channel, captime.Format("15:04:05.000"), pfbuf)
|
||||
fmt.Printf("%-2s %s %s\n", channelstr, captime.Format("15:04:05.000"), pfbuf)
|
||||
}
|
||||
} else {
|
||||
fmt.Println(channel, captime.Format("15:04:05.000"), "cap ERR", frames, err.Error())
|
||||
fmt.Printf("%-2s %s %s %v %s\n", channelstr, captime.Format("15:04:05.000"), "cap ERR", frames, err.Error())
|
||||
}
|
||||
})
|
||||
hwaddr, err := sv.HardwareAddress6()
|
||||
hwaddr, err = sv.HardwareAddress6()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user