diff --git a/.gitignore b/.gitignore index 1091f8b..d5dbfd8 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,8 @@ vendor/ /xcurl /xnet /httpclient +/httpserver +**/local* /stack **__debug_bin* # `__debug_bin` Debug binary generated in VSCode when using the built-in debugger. @@ -33,6 +35,7 @@ vendor/ /bridge # IDE .vscode/ +agents.md # For local development and testing create `local` directories. local diff --git a/dhcpv4/dhcp_test.go b/dhcpv4/dhcp_test.go index cb59901..ebcb51d 100644 --- a/dhcpv4/dhcp_test.go +++ b/dhcpv4/dhcp_test.go @@ -2,6 +2,7 @@ package dhcpv4 import ( "bytes" + "net/netip" "testing" ) @@ -25,7 +26,10 @@ func TestClientServer(t *testing.T) { 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. assertClState(StateInit) var buf [1024]byte diff --git a/dhcpv4/server.go b/dhcpv4/server.go index 88bceb0..d82067c 100644 --- a/dhcpv4/server.go +++ b/dhcpv4/server.go @@ -11,15 +11,33 @@ import ( ) type Server struct { - connID uint64 - nextAddr netip.Addr - prefix netip.Prefix - hosts map[[36]byte]serverEntry - vld lneto.Validator - pending int - port uint16 - siaddr [4]byte - gwaddr [4]byte + connID uint64 + nextAddr netip.Addr + prefix netip.Prefix + hosts map[[36]byte]serverEntry + vld lneto.Validator + pending int + leaseSeconds uint32 + port uint16 + 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 { @@ -35,30 +53,53 @@ type serverEntry struct { // - Init: Server received discover, pending Offer sent out. // - Selecting: Server sent out offer, request not received. // - 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 } -func (sv *Server) Reset(serverAddr [4]byte, port uint16) { - *sv = Server{ - connID: sv.connID + 1, - siaddr: serverAddr, - port: port, - hosts: sv.hosts, - nextAddr: netip.AddrFrom4(serverAddr), +// Configure resets and configures the server with the given configuration. +// The connection ID is incremented on each call to invalidate existing connections. +// The hosts map is reused across calls to avoid reallocation. +func (sv *Server) Configure(cfg ServerConfig) error { + svAddr := netip.AddrFrom4(cfg.ServerAddr) + if !cfg.Subnet.IsValid() { + 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 { - sv.hosts = make(map[[36]byte]serverEntry) + port := cfg.Port + 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 { - for k := range sv.hosts { - delete(sv.hosts, k) + for k := range hosts { + 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) 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 { isIPLayer := frameOffset >= 28 @@ -103,6 +144,9 @@ func (sv *Server) Demux(carrierData []byte, frameOffset int) error { } return nil }) + if err != nil { + return err + } var clientIDRaw [36]byte var client serverEntry var clientExists bool @@ -115,16 +159,17 @@ func (sv *Server) Demux(carrierData []byte, frameOffset int) error { switch msgType { case MsgDiscover: - if clientExists { - err = errors.New("DHCP Discover on initialized client") - break + if clientExists && (client.state == StateInit || client.state == StateRequesting) { + sv.pending-- // Cancel unfulfilled pending response. } - if len(reqAddr) == 4 { - println("requested", reqAddr[0], reqAddr[1], reqAddr[2], reqAddr[3]) + if !clientExists { + 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) - client.addr = sv.nextAddr.As4() client.state = StateInit client.hostname = string(hostname) client.xid = dfrm.XID() @@ -137,7 +182,7 @@ func (sv *Server) Demux(carrierData []byte, frameOffset int) error { case MsgRequest: if !clientExists { - err = errors.New("request for non existing client?") + err = errors.New("request for non existing client") } else if dfrm.XID() != client.xid { err = errors.New("unexpected XID for client") } else if client.state != StateSelecting && client.state != StateRequesting { @@ -146,11 +191,22 @@ func (sv *Server) Demux(carrierData []byte, frameOffset int) error { if err != nil { break } - client.state = StateRequesting - sv.pending++ + if client.state == StateSelecting { + 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: - err = errors.New("unhandled message type") + err = fmt.Errorf("unhandled message type %s", msgType.String()) } if err != nil { 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 } if sv.pending == 0 { - return 0, nil // No pending outgoing frames.a + return 0, nil // No pending outgoing frames. } var client serverEntry @@ -205,6 +261,26 @@ func (sv *Server) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int) n, _ = EncodeOption(optBuf[nopt:], OptRouter, sv.gwaddr[:]...) 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) nopt++ @@ -234,6 +310,40 @@ func (sv *Server) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int) 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) { entry, ok := sv.hosts[clientID] return entry, ok @@ -257,4 +367,4 @@ func getSrcIPPort(ipCarrier []byte) (srcaddr []byte, port uint16, err error) { } port = binary.BigEndian.Uint16(ipCarrier[off:]) // TCP and UDP share same port offsets. return srcaddr, port, nil -} +} \ No newline at end of file diff --git a/dhcpv4/server_test.go b/dhcpv4/server_test.go new file mode 100644 index 0000000..cf85bac --- /dev/null +++ b/dhcpv4/server_test.go @@ -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()) + } +} \ No newline at end of file diff --git a/ethernet/frame.go b/ethernet/frame.go index 22e6bb0..392855e 100644 --- a/ethernet/frame.go +++ b/ethernet/frame.go @@ -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. 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. func (efrm Frame) SetVLANTag(vt VLANTag) { binary.BigEndian.PutUint16(efrm.buf[14:16], uint16(vt)) } diff --git a/examples/httpserver/index.html b/examples/httpserver/index.html new file mode 100644 index 0000000..9d5cfd3 --- /dev/null +++ b/examples/httpserver/index.html @@ -0,0 +1,92 @@ + + + + Arbitrary Waveform Generator Control + + + + +

AD9850 DDS Control

+
+ +

+ Whose woods these are I think I know.
+ His house is in the village though;
+ He will not see me stopping here
+ To watch his woods fill up with snow.
+
+ My little horse must think it queer
+ To stop without a farmhouse near
+ Between the woods and frozen lake
+ The darkest evening of the year.
+
+ He gives his harness bells a shake
+ To ask if there is some mistake.
+ The only other sound’s the sweep
+ Of easy wind and downy flake.
+
+ The woods are lovely, dark and deep,
+ But I have promises to keep,
+ And miles to go before I sleep,
+

+ \ No newline at end of file diff --git a/examples/httpserver/main.go b/examples/httpserver/main.go new file mode 100644 index 0000000..3ad6fe0 --- /dev/null +++ b/examples/httpserver/main.go @@ -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 +} diff --git a/examples/httptap/dhcp.go b/examples/httptap/dhcp.go new file mode 100644 index 0000000..b1da3a5 --- /dev/null +++ b/examples/httptap/dhcp.go @@ -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() } diff --git a/examples/httptap/main.go b/examples/httptap/main.go index 5c38af2..f3c59ae 100644 --- a/examples/httptap/main.go +++ b/examples/httptap/main.go @@ -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 } diff --git a/internet/pcap/capture.go b/internet/pcap/capture.go index a7e0c6b..185f197 100644 --- a/internet/pcap/capture.go +++ b/internet/pcap/capture.go @@ -244,10 +244,12 @@ func (pc *PacketBreakdown) CaptureIPv4(dst []Frame, pkt []byte, bitOffset int) ( println("BAD UDP") return dst, pc.vld.ErrPop() } - frameLen := ufrm.Length() - ifrm4.CRCWriteUDPPseudo(&crc, frameLen) - if crc.PayloadSum16(ufrm.RawData()[:frameLen]) != 0 { - protoErrs = append(protoErrs, lneto.ErrBadCRC) + if ufrm.CRC() != 0 { + frameLen := ufrm.Length() + ifrm4.CRCWriteUDPPseudo(&crc, frameLen) + if crc.PayloadSum16(ufrm.RawData()[:frameLen]) != 0 { + protoErrs = append(protoErrs, lneto.ErrBadCRC) + } } } case lneto.IPProtoICMP: diff --git a/internet/pcap/format.go b/internet/pcap/format.go index 35e036b..ea0d5c2 100644 --- a/internet/pcap/format.go +++ b/internet/pcap/format.go @@ -80,6 +80,16 @@ func (f *Formatter) FormatFrame(dst []byte, frm Frame, pkt []byte) (_ []byte, er 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 } diff --git a/ipv4/definitions.go b/ipv4/definitions.go index 4097e58..d0bf309 100644 --- a/ipv4/definitions.go +++ b/ipv4/definitions.go @@ -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. 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 // which is used to classify packets. 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. 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))<