mirror of
https://github.com/soypat/lneto.git
synced 2026-08-21 15:09:05 +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:
@@ -25,6 +25,8 @@ vendor/
|
|||||||
/xcurl
|
/xcurl
|
||||||
/xnet
|
/xnet
|
||||||
/httpclient
|
/httpclient
|
||||||
|
/httpserver
|
||||||
|
**/local*
|
||||||
/stack
|
/stack
|
||||||
**__debug_bin*
|
**__debug_bin*
|
||||||
# `__debug_bin` Debug binary generated in VSCode when using the built-in debugger.
|
# `__debug_bin` Debug binary generated in VSCode when using the built-in debugger.
|
||||||
@@ -33,6 +35,7 @@ vendor/
|
|||||||
/bridge
|
/bridge
|
||||||
# IDE
|
# IDE
|
||||||
.vscode/
|
.vscode/
|
||||||
|
agents.md
|
||||||
|
|
||||||
# For local development and testing create `local` directories.
|
# For local development and testing create `local` directories.
|
||||||
local
|
local
|
||||||
|
|||||||
+5
-1
@@ -2,6 +2,7 @@ package dhcpv4
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"net/netip"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,7 +26,10 @@ func TestClientServer(t *testing.T) {
|
|||||||
t.Errorf("want client state %s, got %s", state.String(), cl.State().String())
|
t.Errorf("want client state %s, got %s", state.String(), cl.State().String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sv.Reset(svAddr, DefaultServerPort)
|
sv.Configure(ServerConfig{
|
||||||
|
ServerAddr: svAddr,
|
||||||
|
Subnet: netip.PrefixFrom(netip.AddrFrom4(svAddr), 24),
|
||||||
|
})
|
||||||
// CLIENT DISCOVER.
|
// CLIENT DISCOVER.
|
||||||
assertClState(StateInit)
|
assertClState(StateInit)
|
||||||
var buf [1024]byte
|
var buf [1024]byte
|
||||||
|
|||||||
+144
-34
@@ -11,15 +11,33 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
connID uint64
|
connID uint64
|
||||||
nextAddr netip.Addr
|
nextAddr netip.Addr
|
||||||
prefix netip.Prefix
|
prefix netip.Prefix
|
||||||
hosts map[[36]byte]serverEntry
|
hosts map[[36]byte]serverEntry
|
||||||
vld lneto.Validator
|
vld lneto.Validator
|
||||||
pending int
|
pending int
|
||||||
port uint16
|
leaseSeconds uint32
|
||||||
siaddr [4]byte
|
port uint16
|
||||||
gwaddr [4]byte
|
siaddr [4]byte
|
||||||
|
gwaddr [4]byte
|
||||||
|
dns [4]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerConfig contains configuration parameters for [Server.Configure].
|
||||||
|
type ServerConfig struct {
|
||||||
|
// ServerAddr is the DHCP server's own IPv4 address.
|
||||||
|
ServerAddr [4]byte
|
||||||
|
// Gateway advertised to clients as default router. Zero value omits the option.
|
||||||
|
Gateway [4]byte
|
||||||
|
// DNS server address advertised to clients. Zero value omits the option.
|
||||||
|
DNS [4]byte
|
||||||
|
// Subnet defines the network prefix for address allocation and subnet mask responses.
|
||||||
|
Subnet netip.Prefix
|
||||||
|
// LeaseSeconds is the lease duration. Zero defaults to 3600.
|
||||||
|
LeaseSeconds uint32
|
||||||
|
// Port is the server listening port. Zero defaults to DefaultServerPort.
|
||||||
|
Port uint16
|
||||||
}
|
}
|
||||||
|
|
||||||
type serverEntry struct {
|
type serverEntry struct {
|
||||||
@@ -35,30 +53,53 @@ type serverEntry struct {
|
|||||||
// - Init: Server received discover, pending Offer sent out.
|
// - Init: Server received discover, pending Offer sent out.
|
||||||
// - Selecting: Server sent out offer, request not received.
|
// - Selecting: Server sent out offer, request not received.
|
||||||
// - Requesting: Request received, pending Ack sent out.
|
// - Requesting: Request received, pending Ack sent out.
|
||||||
// - Bound: Request sent out, no more pending data to be sent.
|
// - Bound: Ack sent out, no more pending data to be sent.
|
||||||
state ClientState
|
state ClientState
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sv *Server) Reset(serverAddr [4]byte, port uint16) {
|
// Configure resets and configures the server with the given configuration.
|
||||||
*sv = Server{
|
// The connection ID is incremented on each call to invalidate existing connections.
|
||||||
connID: sv.connID + 1,
|
// The hosts map is reused across calls to avoid reallocation.
|
||||||
siaddr: serverAddr,
|
func (sv *Server) Configure(cfg ServerConfig) error {
|
||||||
port: port,
|
svAddr := netip.AddrFrom4(cfg.ServerAddr)
|
||||||
hosts: sv.hosts,
|
if !cfg.Subnet.IsValid() {
|
||||||
nextAddr: netip.AddrFrom4(serverAddr),
|
return errors.New("dhcpv4 server: invalid subnet")
|
||||||
|
} else if !cfg.Subnet.Contains(svAddr) {
|
||||||
|
return errors.New("dhcpv4 server: server address outside subnet")
|
||||||
}
|
}
|
||||||
if sv.hosts == nil {
|
port := cfg.Port
|
||||||
sv.hosts = make(map[[36]byte]serverEntry)
|
if port == 0 {
|
||||||
|
port = DefaultServerPort
|
||||||
|
}
|
||||||
|
lease := cfg.LeaseSeconds
|
||||||
|
if lease == 0 {
|
||||||
|
lease = 3600
|
||||||
|
}
|
||||||
|
hosts := sv.hosts
|
||||||
|
if hosts == nil {
|
||||||
|
hosts = make(map[[36]byte]serverEntry)
|
||||||
} else {
|
} else {
|
||||||
for k := range sv.hosts {
|
for k := range hosts {
|
||||||
delete(sv.hosts, k)
|
delete(hosts, k)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*sv = Server{
|
||||||
|
connID: sv.connID + 1,
|
||||||
|
siaddr: cfg.ServerAddr,
|
||||||
|
gwaddr: cfg.Gateway,
|
||||||
|
dns: cfg.DNS,
|
||||||
|
prefix: cfg.Subnet,
|
||||||
|
port: port,
|
||||||
|
leaseSeconds: lease,
|
||||||
|
nextAddr: svAddr,
|
||||||
|
hosts: hosts,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sv *Server) ConnectionID() *uint64 { return &sv.connID }
|
func (sv *Server) ConnectionID() *uint64 { return &sv.connID }
|
||||||
func (sv *Server) Protocol() uint64 { return uint64(lneto.IPProtoUDP) }
|
func (sv *Server) Protocol() uint64 { return uint64(lneto.IPProtoUDP) }
|
||||||
func (sv *Server) Port() uint16 { return sv.port }
|
func (sv *Server) LocalPort() uint16 { return sv.port }
|
||||||
|
|
||||||
func (sv *Server) Demux(carrierData []byte, frameOffset int) error {
|
func (sv *Server) Demux(carrierData []byte, frameOffset int) error {
|
||||||
isIPLayer := frameOffset >= 28
|
isIPLayer := frameOffset >= 28
|
||||||
@@ -103,6 +144,9 @@ func (sv *Server) Demux(carrierData []byte, frameOffset int) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
var clientIDRaw [36]byte
|
var clientIDRaw [36]byte
|
||||||
var client serverEntry
|
var client serverEntry
|
||||||
var clientExists bool
|
var clientExists bool
|
||||||
@@ -115,16 +159,17 @@ func (sv *Server) Demux(carrierData []byte, frameOffset int) error {
|
|||||||
|
|
||||||
switch msgType {
|
switch msgType {
|
||||||
case MsgDiscover:
|
case MsgDiscover:
|
||||||
if clientExists {
|
if clientExists && (client.state == StateInit || client.state == StateRequesting) {
|
||||||
err = errors.New("DHCP Discover on initialized client")
|
sv.pending-- // Cancel unfulfilled pending response.
|
||||||
break
|
|
||||||
}
|
}
|
||||||
if len(reqAddr) == 4 {
|
if !clientExists {
|
||||||
println("requested", reqAddr[0], reqAddr[1], reqAddr[2], reqAddr[3])
|
addr, ok := sv.allocAddr(reqAddr)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("dhcpv4 server: address pool exhausted")
|
||||||
|
}
|
||||||
|
client.addr = addr
|
||||||
}
|
}
|
||||||
sv.nextAddr = sv.nextAddr.Next()
|
|
||||||
copy(client.requestlist[:], reqlist)
|
copy(client.requestlist[:], reqlist)
|
||||||
client.addr = sv.nextAddr.As4()
|
|
||||||
client.state = StateInit
|
client.state = StateInit
|
||||||
client.hostname = string(hostname)
|
client.hostname = string(hostname)
|
||||||
client.xid = dfrm.XID()
|
client.xid = dfrm.XID()
|
||||||
@@ -137,7 +182,7 @@ func (sv *Server) Demux(carrierData []byte, frameOffset int) error {
|
|||||||
|
|
||||||
case MsgRequest:
|
case MsgRequest:
|
||||||
if !clientExists {
|
if !clientExists {
|
||||||
err = errors.New("request for non existing client?")
|
err = errors.New("request for non existing client")
|
||||||
} else if dfrm.XID() != client.xid {
|
} else if dfrm.XID() != client.xid {
|
||||||
err = errors.New("unexpected XID for client")
|
err = errors.New("unexpected XID for client")
|
||||||
} else if client.state != StateSelecting && client.state != StateRequesting {
|
} else if client.state != StateSelecting && client.state != StateRequesting {
|
||||||
@@ -146,11 +191,22 @@ func (sv *Server) Demux(carrierData []byte, frameOffset int) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
client.state = StateRequesting
|
if client.state == StateSelecting {
|
||||||
sv.pending++
|
client.state = StateRequesting
|
||||||
|
sv.pending++
|
||||||
|
}
|
||||||
|
|
||||||
|
case MsgRelease:
|
||||||
|
if clientExists {
|
||||||
|
if client.state == StateInit || client.state == StateRequesting {
|
||||||
|
sv.pending--
|
||||||
|
}
|
||||||
|
delete(sv.hosts, clientIDRaw)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
err = errors.New("unhandled message type")
|
err = fmt.Errorf("unhandled message type %s", msgType.String())
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("msgtype=%s client=%+v: %w", msgType.String(), client, err)
|
return fmt.Errorf("msgtype=%s client=%+v: %w", msgType.String(), client, err)
|
||||||
@@ -169,7 +225,7 @@ func (sv *Server) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int)
|
|||||||
return 0, errOptionNotFit
|
return 0, errOptionNotFit
|
||||||
}
|
}
|
||||||
if sv.pending == 0 {
|
if sv.pending == 0 {
|
||||||
return 0, nil // No pending outgoing frames.a
|
return 0, nil // No pending outgoing frames.
|
||||||
}
|
}
|
||||||
|
|
||||||
var client serverEntry
|
var client serverEntry
|
||||||
@@ -205,6 +261,26 @@ func (sv *Server) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int)
|
|||||||
n, _ = EncodeOption(optBuf[nopt:], OptRouter, sv.gwaddr[:]...)
|
n, _ = EncodeOption(optBuf[nopt:], OptRouter, sv.gwaddr[:]...)
|
||||||
nopt += n
|
nopt += n
|
||||||
}
|
}
|
||||||
|
if sv.prefix.IsValid() {
|
||||||
|
bits := uint(sv.prefix.Bits())
|
||||||
|
mask := ^uint32(0) << (32 - bits)
|
||||||
|
var maskBuf [4]byte
|
||||||
|
binary.BigEndian.PutUint32(maskBuf[:], mask)
|
||||||
|
n, _ = EncodeOption(optBuf[nopt:], OptSubnetMask, maskBuf[:]...)
|
||||||
|
nopt += n
|
||||||
|
}
|
||||||
|
if sv.dns != [4]byte{} {
|
||||||
|
n, _ = EncodeOption(optBuf[nopt:], OptDNSServers, sv.dns[:]...)
|
||||||
|
nopt += n
|
||||||
|
}
|
||||||
|
if sv.leaseSeconds > 0 {
|
||||||
|
n, _ = EncodeOption32(optBuf[nopt:], OptIPAddressLeaseTime, sv.leaseSeconds)
|
||||||
|
nopt += n
|
||||||
|
n, _ = EncodeOption32(optBuf[nopt:], OptRenewTimeValue, sv.leaseSeconds/2)
|
||||||
|
nopt += n
|
||||||
|
n, _ = EncodeOption32(optBuf[nopt:], OptRebindingTimeValue, sv.leaseSeconds*7/8)
|
||||||
|
nopt += n
|
||||||
|
}
|
||||||
optBuf[nopt] = byte(OptEnd)
|
optBuf[nopt] = byte(OptEnd)
|
||||||
nopt++
|
nopt++
|
||||||
|
|
||||||
@@ -234,6 +310,40 @@ func (sv *Server) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int)
|
|||||||
return OptionsOffset + nopt, nil
|
return OptionsOffset + nopt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// allocAddr allocates the next available address from the pool.
|
||||||
|
// If reqAddr is a valid 4-byte address within the subnet and not already assigned,
|
||||||
|
// it is preferred. Returns false if the pool is exhausted.
|
||||||
|
func (sv *Server) allocAddr(reqAddr []byte) ([4]byte, bool) {
|
||||||
|
if len(reqAddr) == 4 {
|
||||||
|
candidate := netip.AddrFrom4([4]byte(reqAddr))
|
||||||
|
if sv.prefix.Contains(candidate) && candidate.As4() != sv.siaddr && !sv.isAddrAssigned(candidate) {
|
||||||
|
return candidate.As4(), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sv.nextAddr = sv.nextAddr.Next()
|
||||||
|
if !sv.prefix.Contains(sv.nextAddr) {
|
||||||
|
return [4]byte{}, false
|
||||||
|
}
|
||||||
|
// Reject broadcast address (all host bits set).
|
||||||
|
a := sv.nextAddr.As4()
|
||||||
|
hostBits := uint(32 - sv.prefix.Bits())
|
||||||
|
hostMask := ^uint32(0) >> (32 - hostBits)
|
||||||
|
if binary.BigEndian.Uint32(a[:])&hostMask == hostMask {
|
||||||
|
return [4]byte{}, false
|
||||||
|
}
|
||||||
|
return a, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sv *Server) isAddrAssigned(addr netip.Addr) bool {
|
||||||
|
a4 := addr.As4()
|
||||||
|
for _, v := range sv.hosts {
|
||||||
|
if v.addr == a4 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (sv *Server) getClient(clientID [36]byte) (serverEntry, bool) {
|
func (sv *Server) getClient(clientID [36]byte) (serverEntry, bool) {
|
||||||
entry, ok := sv.hosts[clientID]
|
entry, ok := sv.hosts[clientID]
|
||||||
return entry, ok
|
return entry, ok
|
||||||
|
|||||||
@@ -0,0 +1,358 @@
|
|||||||
|
package dhcpv4
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testServerConfig(svAddr [4]byte) ServerConfig {
|
||||||
|
return ServerConfig{
|
||||||
|
ServerAddr: svAddr,
|
||||||
|
Subnet: netip.PrefixFrom(netip.AddrFrom4(svAddr), 24),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestServerMultipleClients verifies the server can handle multiple clients
|
||||||
|
// going through the full DORA flow independently.
|
||||||
|
func TestServerMultipleClients(t *testing.T) {
|
||||||
|
svAddr := [4]byte{192, 168, 1, 1}
|
||||||
|
var sv Server
|
||||||
|
sv.Configure(testServerConfig(svAddr))
|
||||||
|
|
||||||
|
const nClients = 3
|
||||||
|
var clients [nClients]Client
|
||||||
|
var bufs [nClients][1024]byte
|
||||||
|
|
||||||
|
for i := range clients {
|
||||||
|
err := clients[i].BeginRequest(uint32(100+i), RequestConfig{
|
||||||
|
ClientHardwareAddr: [6]byte{0, 0, 0, 0, 0, byte(i + 1)},
|
||||||
|
Hostname: "host",
|
||||||
|
ClientID: string([]byte{byte(i + 1)}),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("client %d BeginRequest: %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 1: All clients send DISCOVER.
|
||||||
|
for i := range clients {
|
||||||
|
n, err := clients[i].Encapsulate(bufs[i][:], -1, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("client %d discover encapsulate: %v", i, err)
|
||||||
|
}
|
||||||
|
err = sv.Demux(bufs[i][:n], 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("client %d discover demux: %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route server responses to the correct client by XID (map iteration is non-deterministic).
|
||||||
|
clientByXID := make(map[uint32]int)
|
||||||
|
for i := range clients {
|
||||||
|
clientByXID[uint32(100+i)] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: Server sends all OFFERs, clients receive.
|
||||||
|
var assignedAddrs [nClients][4]byte
|
||||||
|
for range clients {
|
||||||
|
var buf [1024]byte
|
||||||
|
n, err := sv.Encapsulate(buf[:], -1, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("offer encapsulate: %v", err)
|
||||||
|
} else if n == 0 {
|
||||||
|
t.Fatal("no offer from server")
|
||||||
|
}
|
||||||
|
frm, _ := NewFrame(buf[:n])
|
||||||
|
ci := clientByXID[frm.XID()]
|
||||||
|
assignedAddrs[ci] = *frm.YIAddr()
|
||||||
|
err = clients[ci].Demux(buf[:n], 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("client %d offer demux: %v", ci, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 3: All clients send REQUEST.
|
||||||
|
for i := range clients {
|
||||||
|
n, err := clients[i].Encapsulate(bufs[i][:], -1, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("client %d request encapsulate: %v", i, err)
|
||||||
|
} else if n == 0 {
|
||||||
|
t.Fatalf("client %d: no request data", i)
|
||||||
|
}
|
||||||
|
err = sv.Demux(bufs[i][:n], 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("client %d request demux: %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 4: Server sends all ACKs, clients receive.
|
||||||
|
for range clients {
|
||||||
|
var buf [1024]byte
|
||||||
|
n, err := sv.Encapsulate(buf[:], -1, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ack encapsulate: %v", err)
|
||||||
|
} else if n == 0 {
|
||||||
|
t.Fatal("no ack from server")
|
||||||
|
}
|
||||||
|
frm, _ := NewFrame(buf[:n])
|
||||||
|
ci := clientByXID[frm.XID()]
|
||||||
|
err = clients[ci].Demux(buf[:n], 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("client %d ack demux: %v", ci, err)
|
||||||
|
}
|
||||||
|
if clients[ci].State() != StateBound {
|
||||||
|
t.Errorf("client %d: want StateBound, got %s", ci, clients[ci].State())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All assigned addresses must be unique.
|
||||||
|
for i := 0; i < nClients; i++ {
|
||||||
|
for j := i + 1; j < nClients; j++ {
|
||||||
|
if assignedAddrs[i] == assignedAddrs[j] {
|
||||||
|
t.Errorf("clients %d and %d got same address %v", i, j, assignedAddrs[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestServerSequentialAddressAllocation verifies that the server allocates
|
||||||
|
// addresses sequentially starting from serverAddr+1.
|
||||||
|
func TestServerSequentialAddressAllocation(t *testing.T) {
|
||||||
|
svAddr := [4]byte{192, 168, 1, 1}
|
||||||
|
var sv Server
|
||||||
|
sv.Configure(testServerConfig(svAddr))
|
||||||
|
|
||||||
|
// Build raw DISCOVER frames for two clients.
|
||||||
|
for i := byte(0); i < 2; i++ {
|
||||||
|
var buf [512]byte
|
||||||
|
frm, _ := NewFrame(buf[:])
|
||||||
|
frm.ClearHeader()
|
||||||
|
frm.SetOp(OpRequest)
|
||||||
|
frm.SetHardware(1, 6, 0)
|
||||||
|
frm.SetXID(uint32(200 + i))
|
||||||
|
frm.SetSecs(1)
|
||||||
|
copy(frm.CHAddrAs6()[:], []byte{0, 0, 0, 0, 0, 10 + i})
|
||||||
|
frm.SetMagicCookie(MagicCookie)
|
||||||
|
opts := buf[OptionsOffset:]
|
||||||
|
n := writeOption(opts, OptMessageType, byte(MsgDiscover))
|
||||||
|
n += writeOption(opts[n:], OptClientIdentifier, 10+i)
|
||||||
|
opts[n] = byte(OptEnd)
|
||||||
|
n++
|
||||||
|
|
||||||
|
err := sv.Demux(buf[:OptionsOffset+n], 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("discover %d: %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encapsulate both OFFERs and verify addresses are in expected range.
|
||||||
|
var seen [2][4]byte
|
||||||
|
for i := byte(0); i < 2; i++ {
|
||||||
|
var buf [512]byte
|
||||||
|
n, err := sv.Encapsulate(buf[:], -1, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("offer %d encapsulate: %v", i, err)
|
||||||
|
} else if n == 0 {
|
||||||
|
t.Fatalf("offer %d: no data", i)
|
||||||
|
}
|
||||||
|
frm, _ := NewFrame(buf[:n])
|
||||||
|
seen[i] = *frm.YIAddr()
|
||||||
|
if seen[i][0] != 192 || seen[i][1] != 168 || seen[i][2] != 1 {
|
||||||
|
t.Errorf("offer %d: unexpected subnet in %v", i, seen[i])
|
||||||
|
}
|
||||||
|
if seen[i][3] != 2 && seen[i][3] != 3 {
|
||||||
|
t.Errorf("offer %d: expected .2 or .3, got .%d", i, seen[i][3])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if seen[0] == seen[1] {
|
||||||
|
t.Errorf("both offers got same address %v", seen[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestServerOfferContainsOptions verifies that server OFFER responses
|
||||||
|
// contain the expected DHCP options from the ServerConfig.
|
||||||
|
func TestServerOfferContainsOptions(t *testing.T) {
|
||||||
|
svAddr := [4]byte{192, 168, 1, 1}
|
||||||
|
gwAddr := [4]byte{192, 168, 1, 254}
|
||||||
|
dnsAddr := [4]byte{8, 8, 8, 8}
|
||||||
|
var sv Server
|
||||||
|
sv.Configure(ServerConfig{
|
||||||
|
ServerAddr: svAddr,
|
||||||
|
Gateway: gwAddr,
|
||||||
|
DNS: dnsAddr,
|
||||||
|
Subnet: netip.PrefixFrom(netip.AddrFrom4(svAddr), 24),
|
||||||
|
LeaseSeconds: 7200,
|
||||||
|
})
|
||||||
|
|
||||||
|
var cl Client
|
||||||
|
err := cl.BeginRequest(500, RequestConfig{
|
||||||
|
ClientHardwareAddr: [6]byte{0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf [1024]byte
|
||||||
|
n, err := cl.Encapsulate(buf[:], -1, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
err = sv.Demux(buf[:n], 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err = sv.Encapsulate(buf[:], -1, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
frm, _ := NewFrame(buf[:n])
|
||||||
|
|
||||||
|
var gotServerID, gotRouter, gotSubnet, gotDNS [4]byte
|
||||||
|
var gotLease, gotRenew, gotRebind uint32
|
||||||
|
var foundServerID, foundRouter, foundSubnet, foundDNS, foundLease bool
|
||||||
|
frm.ForEachOption(func(_ int, opt OptNum, data []byte) error {
|
||||||
|
switch opt {
|
||||||
|
case OptServerIdentification:
|
||||||
|
if len(data) == 4 {
|
||||||
|
foundServerID = true
|
||||||
|
copy(gotServerID[:], data)
|
||||||
|
}
|
||||||
|
case OptRouter:
|
||||||
|
if len(data) == 4 {
|
||||||
|
foundRouter = true
|
||||||
|
copy(gotRouter[:], data)
|
||||||
|
}
|
||||||
|
case OptSubnetMask:
|
||||||
|
if len(data) == 4 {
|
||||||
|
foundSubnet = true
|
||||||
|
copy(gotSubnet[:], data)
|
||||||
|
}
|
||||||
|
case OptDNSServers:
|
||||||
|
if len(data) == 4 {
|
||||||
|
foundDNS = true
|
||||||
|
copy(gotDNS[:], data)
|
||||||
|
}
|
||||||
|
case OptIPAddressLeaseTime:
|
||||||
|
if len(data) == 4 {
|
||||||
|
foundLease = true
|
||||||
|
gotLease = maybeU32(data)
|
||||||
|
}
|
||||||
|
case OptRenewTimeValue:
|
||||||
|
gotRenew = maybeU32(data)
|
||||||
|
case OptRebindingTimeValue:
|
||||||
|
gotRebind = maybeU32(data)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if !foundServerID || gotServerID != svAddr {
|
||||||
|
t.Errorf("server ID: found=%v got=%v want=%v", foundServerID, gotServerID, svAddr)
|
||||||
|
}
|
||||||
|
if !foundRouter || gotRouter != gwAddr {
|
||||||
|
t.Errorf("router: found=%v got=%v want=%v", foundRouter, gotRouter, gwAddr)
|
||||||
|
}
|
||||||
|
if !foundSubnet || gotSubnet != [4]byte{255, 255, 255, 0} {
|
||||||
|
t.Errorf("subnet: found=%v got=%v want=255.255.255.0", foundSubnet, gotSubnet)
|
||||||
|
}
|
||||||
|
if !foundDNS || gotDNS != dnsAddr {
|
||||||
|
t.Errorf("DNS: found=%v got=%v want=%v", foundDNS, gotDNS, dnsAddr)
|
||||||
|
}
|
||||||
|
if !foundLease || gotLease != 7200 {
|
||||||
|
t.Errorf("lease: found=%v got=%v want=7200", foundLease, gotLease)
|
||||||
|
}
|
||||||
|
if gotRenew != 3600 {
|
||||||
|
t.Errorf("renew T1: got %d want 3600", gotRenew)
|
||||||
|
}
|
||||||
|
if gotRebind != 6300 {
|
||||||
|
t.Errorf("rebind T2: got %d want 6300", gotRebind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestServerEncapsulateNoPending verifies Encapsulate returns 0 bytes
|
||||||
|
// when there are no pending responses.
|
||||||
|
func TestServerEncapsulateNoPending(t *testing.T) {
|
||||||
|
var sv Server
|
||||||
|
sv.Configure(testServerConfig([4]byte{192, 168, 1, 1}))
|
||||||
|
|
||||||
|
var buf [512]byte
|
||||||
|
n, err := sv.Encapsulate(buf[:], -1, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if n != 0 {
|
||||||
|
t.Errorf("expected 0 bytes from empty server, got %d", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestServerConfigValidation verifies that Configure rejects invalid configurations.
|
||||||
|
func TestServerConfigValidation(t *testing.T) {
|
||||||
|
var sv Server
|
||||||
|
err := sv.Configure(ServerConfig{
|
||||||
|
ServerAddr: [4]byte{192, 168, 1, 1},
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for zero subnet")
|
||||||
|
}
|
||||||
|
err = sv.Configure(ServerConfig{
|
||||||
|
ServerAddr: [4]byte{10, 0, 0, 1},
|
||||||
|
Subnet: netip.PrefixFrom(netip.AddrFrom4([4]byte{192, 168, 1, 0}), 24),
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for server address outside subnet")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestServerRediscover verifies that a client that was previously bound
|
||||||
|
// can send a fresh DISCOVER and get re-served.
|
||||||
|
func TestServerRediscover(t *testing.T) {
|
||||||
|
svAddr := [4]byte{192, 168, 1, 1}
|
||||||
|
var sv Server
|
||||||
|
sv.Configure(testServerConfig(svAddr))
|
||||||
|
|
||||||
|
// First DORA cycle.
|
||||||
|
var cl Client
|
||||||
|
cl.BeginRequest(1, RequestConfig{
|
||||||
|
ClientHardwareAddr: [6]byte{1, 2, 3, 4, 5, 6},
|
||||||
|
ClientID: "rediscover-client",
|
||||||
|
})
|
||||||
|
var buf [1024]byte
|
||||||
|
n, _ := cl.Encapsulate(buf[:], -1, 0)
|
||||||
|
sv.Demux(buf[:n], 0)
|
||||||
|
n, _ = sv.Encapsulate(buf[:], -1, 0)
|
||||||
|
cl.Demux(buf[:n], 0)
|
||||||
|
n, _ = cl.Encapsulate(buf[:], -1, 0)
|
||||||
|
sv.Demux(buf[:n], 0)
|
||||||
|
n, _ = sv.Encapsulate(buf[:], -1, 0)
|
||||||
|
cl.Demux(buf[:n], 0)
|
||||||
|
if cl.State() != StateBound {
|
||||||
|
t.Fatalf("first DORA: want StateBound, got %s", cl.State())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client reboots and sends fresh DISCOVER.
|
||||||
|
cl.Reset()
|
||||||
|
cl.BeginRequest(2, RequestConfig{
|
||||||
|
ClientHardwareAddr: [6]byte{1, 2, 3, 4, 5, 6},
|
||||||
|
ClientID: "rediscover-client",
|
||||||
|
})
|
||||||
|
n, _ = cl.Encapsulate(buf[:], -1, 0)
|
||||||
|
err := sv.Demux(buf[:n], 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("rediscover demux: %v", err)
|
||||||
|
}
|
||||||
|
n, _ = sv.Encapsulate(buf[:], -1, 0)
|
||||||
|
if n == 0 {
|
||||||
|
t.Fatal("no offer after rediscover")
|
||||||
|
}
|
||||||
|
err = cl.Demux(buf[:n], 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("rediscover offer demux: %v", err)
|
||||||
|
}
|
||||||
|
// Complete the second DORA.
|
||||||
|
n, _ = cl.Encapsulate(buf[:], -1, 0)
|
||||||
|
sv.Demux(buf[:n], 0)
|
||||||
|
n, _ = sv.Encapsulate(buf[:], -1, 0)
|
||||||
|
cl.Demux(buf[:n], 0)
|
||||||
|
if cl.State() != StateBound {
|
||||||
|
t.Errorf("second DORA: want StateBound, got %s", cl.State())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -79,6 +79,24 @@ func (efrm Frame) SetEtherType(v Type) {
|
|||||||
// VLANTag returns the VLAN tag field following the TPID=0x8100. See [VLANTag]. Call [Frame.ValidateSize] to ensure this function does not panic.
|
// VLANTag returns the VLAN tag field following the TPID=0x8100. See [VLANTag]. Call [Frame.ValidateSize] to ensure this function does not panic.
|
||||||
func (efrm Frame) VLANTag() VLANTag { return VLANTag(binary.BigEndian.Uint16(efrm.buf[14:16])) }
|
func (efrm Frame) VLANTag() VLANTag { return VLANTag(binary.BigEndian.Uint16(efrm.buf[14:16])) }
|
||||||
|
|
||||||
|
// SetVLAN sets following 3 fields:
|
||||||
|
// - 12:14 ethernet frame type set to constant [TypeVLAN].
|
||||||
|
// - 14:16 set to VLANTag argument value vt
|
||||||
|
// - 16:18 set to the VLAN ether type vlanType.
|
||||||
|
func (efrm Frame) SetVLAN(tag VLANTag, vlanType Type) {
|
||||||
|
efrm.SetEtherType(TypeVLAN)
|
||||||
|
binary.BigEndian.PutUint16(efrm.buf[14:16], uint16(tag))
|
||||||
|
binary.BigEndian.PutUint16(efrm.buf[16:18], uint16(vlanType))
|
||||||
|
}
|
||||||
|
|
||||||
|
// VLAN returns fields 14:16 and 16:18. Does not check field 12:14 for correctness.
|
||||||
|
// VLAN panics if length is insufficient.
|
||||||
|
func (efrm Frame) VLAN() (VLANTag, Type) {
|
||||||
|
vt := binary.BigEndian.Uint16(efrm.buf[14:16])
|
||||||
|
et := binary.BigEndian.Uint16(efrm.buf[16:18])
|
||||||
|
return VLANTag(vt), Type(et)
|
||||||
|
}
|
||||||
|
|
||||||
// SetVLANTag sets the VLAN tag field of the Ethernet Header. See [VLANTag]. Call [Frame.ValidateSize] to ensure this function does not panic.
|
// SetVLANTag sets the VLAN tag field of the Ethernet Header. See [VLANTag]. Call [Frame.ValidateSize] to ensure this function does not panic.
|
||||||
func (efrm Frame) SetVLANTag(vt VLANTag) { binary.BigEndian.PutUint16(efrm.buf[14:16], uint16(vt)) }
|
func (efrm Frame) SetVLANTag(vt VLANTag) { binary.BigEndian.PutUint16(efrm.buf[14:16], uint16(vt)) }
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -61,6 +62,21 @@ func run() error {
|
|||||||
iface = br
|
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)
|
sv, err := ltesto.NewHTTPTapServer(iface, flagMinMTU, flagPacketQueueSize, flagPacketQueueSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -72,6 +88,10 @@ func run() error {
|
|||||||
}
|
}
|
||||||
var pfbuf []byte
|
var pfbuf []byte
|
||||||
sv.OnTransfer(func(channel int, pkt []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()
|
captime := time.Now()
|
||||||
frames, err := cap.CaptureEthernet(nil, pkt, 0)
|
frames, err := cap.CaptureEthernet(nil, pkt, 0)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -79,15 +99,15 @@ func run() error {
|
|||||||
pfbuf, err = pf.FormatFrames(pfbuf, frames, pkt)
|
pfbuf, err = pf.FormatFrames(pfbuf, frames, pkt)
|
||||||
pfbuf = append(pfbuf, ']')
|
pfbuf = append(pfbuf, ']')
|
||||||
if err != nil {
|
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 {
|
} 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 {
|
} 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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -244,10 +244,12 @@ func (pc *PacketBreakdown) CaptureIPv4(dst []Frame, pkt []byte, bitOffset int) (
|
|||||||
println("BAD UDP")
|
println("BAD UDP")
|
||||||
return dst, pc.vld.ErrPop()
|
return dst, pc.vld.ErrPop()
|
||||||
}
|
}
|
||||||
frameLen := ufrm.Length()
|
if ufrm.CRC() != 0 {
|
||||||
ifrm4.CRCWriteUDPPseudo(&crc, frameLen)
|
frameLen := ufrm.Length()
|
||||||
if crc.PayloadSum16(ufrm.RawData()[:frameLen]) != 0 {
|
ifrm4.CRCWriteUDPPseudo(&crc, frameLen)
|
||||||
protoErrs = append(protoErrs, lneto.ErrBadCRC)
|
if crc.PayloadSum16(ufrm.RawData()[:frameLen]) != 0 {
|
||||||
|
protoErrs = append(protoErrs, lneto.ErrBadCRC)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case lneto.IPProtoICMP:
|
case lneto.IPProtoICMP:
|
||||||
|
|||||||
@@ -80,6 +80,16 @@ func (f *Formatter) FormatFrame(dst []byte, frm Frame, pkt []byte) (_ []byte, er
|
|||||||
return dst, err
|
return dst, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(frm.Errors) > 0 {
|
||||||
|
dst = append(dst, " errs=("...)
|
||||||
|
for i, err := range frm.Errors {
|
||||||
|
if i != 0 {
|
||||||
|
dst = append(dst, ';')
|
||||||
|
}
|
||||||
|
dst = append(dst, err.Error()...)
|
||||||
|
}
|
||||||
|
dst = append(dst, ')')
|
||||||
|
}
|
||||||
return dst, nil
|
return dst, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+36
-4
@@ -7,6 +7,14 @@ const (
|
|||||||
// ToS represents the Traffic Class (a.k.a Type of Service). It is 8 bits long. 6 MSB are Differentiated Services; 2 LSB are Explicit Congenstion Notification.
|
// ToS represents the Traffic Class (a.k.a Type of Service). It is 8 bits long. 6 MSB are Differentiated Services; 2 LSB are Explicit Congenstion Notification.
|
||||||
type ToS uint8
|
type ToS uint8
|
||||||
|
|
||||||
|
// NewToS returns a [ToS] from an Explicit Congestion Notification value and a Differentiated Services Field value.
|
||||||
|
func NewToS(ECN, DS uint8) ToS {
|
||||||
|
if ECN > 0b11 || DS > 0b11_1111 {
|
||||||
|
panic("invalid ECN/DS value")
|
||||||
|
}
|
||||||
|
return ToS(ECN | (DS << 2))
|
||||||
|
}
|
||||||
|
|
||||||
// DS returns the top 6 bits of the IPv4 ToS holding the Differentiated Services field
|
// DS returns the top 6 bits of the IPv4 ToS holding the Differentiated Services field
|
||||||
// which is used to classify packets.
|
// which is used to classify packets.
|
||||||
func (tos ToS) DS() uint8 { return uint8(tos) >> 2 }
|
func (tos ToS) DS() uint8 { return uint8(tos) >> 2 }
|
||||||
@@ -17,22 +25,46 @@ func (tos ToS) ECN() uint8 { return uint8(tos & 0b11) }
|
|||||||
// Flags holds fragmentation field data of an IPv4 header. It is 16 bits long.
|
// Flags holds fragmentation field data of an IPv4 header. It is 16 bits long.
|
||||||
type Flags uint16
|
type Flags uint16
|
||||||
|
|
||||||
|
const (
|
||||||
|
flagIsEvilPos = 13
|
||||||
|
flagDontFragPos = 14
|
||||||
|
flagMoreFragPos = 15
|
||||||
|
FlagOffsetMask = (1 << flagIsEvilPos) - 1
|
||||||
|
flagIsEvil Flags = 1 << flagIsEvilPos
|
||||||
|
FlagDontFragment Flags = 1 << flagDontFragPos
|
||||||
|
FlagMoreFragments Flags = 1 << flagMoreFragPos
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewFlags(fragOffset uint16, dontFrag, moreFrag bool) Flags {
|
||||||
|
if fragOffset > FlagOffsetMask {
|
||||||
|
panic("invalid NewFlags arg")
|
||||||
|
}
|
||||||
|
return Flags(fragOffset) | Flags(b2u8(dontFrag))<<flagDontFragPos | Flags(b2u8(moreFrag))<<flagMoreFragPos
|
||||||
|
}
|
||||||
|
|
||||||
// IsEvil returns true if evil bit set as per [RFC3514].
|
// IsEvil returns true if evil bit set as per [RFC3514].
|
||||||
//
|
//
|
||||||
// [RFC3514]: https://datatracker.ietf.org/doc/html/rfc3514
|
// [RFC3514]: https://datatracker.ietf.org/doc/html/rfc3514
|
||||||
func (f Flags) IsEvil() bool { return f&2000 != 0 }
|
func (f Flags) IsEvil() bool { return f&flagIsEvil != 0 }
|
||||||
|
|
||||||
// DontFragment specifies whether the datagram can not be fragmented.
|
// DontFragment specifies whether the datagram can not be fragmented.
|
||||||
// This can be used when sending packets to a host that does not have resources to perform reassembly of fragments.
|
// This can be used when sending packets to a host that does not have resources to perform reassembly of fragments.
|
||||||
// If the DontFragment(DF) flag is set, and fragmentation is required to route the packet, then the packet is dropped.
|
// If the DontFragment(DF) flag is set, and fragmentation is required to route the packet, then the packet is dropped.
|
||||||
func (f Flags) DontFragment() bool { return f&0x4000 != 0 }
|
func (f Flags) DontFragment() bool { return f&FlagDontFragment != 0 }
|
||||||
|
|
||||||
// MoreFragments is cleared for unfragmented packets.
|
// MoreFragments is cleared for unfragmented packets.
|
||||||
// For fragmented packets, all fragments except the last have the MF flag set.
|
// For fragmented packets, all fragments except the last have the MF flag set.
|
||||||
// The last fragment has a non-zero Fragment Offset field, so it can still be differentiated from an unfragmented packet.
|
// The last fragment has a non-zero Fragment Offset field, so it can still be differentiated from an unfragmented packet.
|
||||||
func (f Flags) MoreFragments() bool { return f&0x8000 != 0 }
|
func (f Flags) MoreFragments() bool { return f&FlagMoreFragments != 0 }
|
||||||
|
|
||||||
// FragmentOffset specifies the offset of a particular fragment relative to the beginning of the original unfragmented IP datagram.
|
// FragmentOffset specifies the offset of a particular fragment relative to the beginning of the original unfragmented IP datagram.
|
||||||
// Fragments are specified in units of 8 bytes, which is why fragment lengths are always a multiple of 8; except the last, which may be smaller.
|
// Fragments are specified in units of 8 bytes, which is why fragment lengths are always a multiple of 8; except the last, which may be smaller.
|
||||||
// The fragmentation offset value for the first fragment is always 0.
|
// The fragmentation offset value for the first fragment is always 0.
|
||||||
func (f Flags) FragmentOffset() uint16 { return uint16(f) & 0x1fff }
|
func (f Flags) FragmentOffset() uint16 { return uint16(f) & FlagOffsetMask }
|
||||||
|
|
||||||
|
func b2u8(b bool) uint8 {
|
||||||
|
if b {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user