From fb36a4ab0965b0e96b08826410ee7df611f56354 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 04:49:47 +0000 Subject: [PATCH 1/2] Initial plan From 68f21107c162220cf95912c500124b626ccfe4ee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 05:13:44 +0000 Subject: [PATCH 2/2] Changes before error encountered Co-authored-by: benaadams <1142958+benaadams@users.noreply.github.com> --- Magma.sln | 15 ++ .../Magma.NetMap.UdpEchoServer.csproj | 18 ++ sample/Magma.NetMap.UdpEchoServer/Program.cs | 130 +++++++++++++++ sample/Magma.NetMap.UdpEchoServer/README.md | 154 ++++++++++++++++++ src/Magma.Transport.Udp/Header/Udp.cs | 74 +++++++++ 5 files changed, 391 insertions(+) create mode 100644 sample/Magma.NetMap.UdpEchoServer/Magma.NetMap.UdpEchoServer.csproj create mode 100644 sample/Magma.NetMap.UdpEchoServer/Program.cs create mode 100644 sample/Magma.NetMap.UdpEchoServer/README.md diff --git a/Magma.sln b/Magma.sln index de00e04..545f52b 100644 --- a/Magma.sln +++ b/Magma.sln @@ -45,6 +45,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Magma.Performance", "benchm EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Magma.NetMap.TcpHost", "sample\Magma.NetMap.TcpHost\Magma.NetMap.TcpHost.csproj", "{358CD7A8-E78B-4DCD-8640-EA62442755D7}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Magma.NetMap.UdpEchoServer", "sample\Magma.NetMap.UdpEchoServer\Magma.NetMap.UdpEchoServer.csproj", "{F8B5E3A1-2D4C-4F8A-9E5B-1C3D4E5F6A7B}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Magma.Internet.Ip.Facts", "test\Magma.Internet.Ip.Facts\Magma.Internet.Ip.Facts.csproj", "{47D45F49-E8E0-4267-9FDA-8635DA6F5348}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Magma.PCap", "src\Magma.PCap\Magma.PCap.csproj", "{F91B0866-9DF5-488D-AD3E-1E471509C60F}" @@ -235,6 +237,18 @@ Global {358CD7A8-E78B-4DCD-8640-EA62442755D7}.Release|x64.Build.0 = Release|Any CPU {358CD7A8-E78B-4DCD-8640-EA62442755D7}.Release|x86.ActiveCfg = Release|Any CPU {358CD7A8-E78B-4DCD-8640-EA62442755D7}.Release|x86.Build.0 = Release|Any CPU + {F8B5E3A1-2D4C-4F8A-9E5B-1C3D4E5F6A7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F8B5E3A1-2D4C-4F8A-9E5B-1C3D4E5F6A7B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F8B5E3A1-2D4C-4F8A-9E5B-1C3D4E5F6A7B}.Debug|x64.ActiveCfg = Debug|Any CPU + {F8B5E3A1-2D4C-4F8A-9E5B-1C3D4E5F6A7B}.Debug|x64.Build.0 = Debug|Any CPU + {F8B5E3A1-2D4C-4F8A-9E5B-1C3D4E5F6A7B}.Debug|x86.ActiveCfg = Debug|Any CPU + {F8B5E3A1-2D4C-4F8A-9E5B-1C3D4E5F6A7B}.Debug|x86.Build.0 = Debug|Any CPU + {F8B5E3A1-2D4C-4F8A-9E5B-1C3D4E5F6A7B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F8B5E3A1-2D4C-4F8A-9E5B-1C3D4E5F6A7B}.Release|Any CPU.Build.0 = Release|Any CPU + {F8B5E3A1-2D4C-4F8A-9E5B-1C3D4E5F6A7B}.Release|x64.ActiveCfg = Release|Any CPU + {F8B5E3A1-2D4C-4F8A-9E5B-1C3D4E5F6A7B}.Release|x64.Build.0 = Release|Any CPU + {F8B5E3A1-2D4C-4F8A-9E5B-1C3D4E5F6A7B}.Release|x86.ActiveCfg = Release|Any CPU + {F8B5E3A1-2D4C-4F8A-9E5B-1C3D4E5F6A7B}.Release|x86.Build.0 = Release|Any CPU {47D45F49-E8E0-4267-9FDA-8635DA6F5348}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {47D45F49-E8E0-4267-9FDA-8635DA6F5348}.Debug|Any CPU.Build.0 = Debug|Any CPU {47D45F49-E8E0-4267-9FDA-8635DA6F5348}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -326,6 +340,7 @@ Global {D8E37D7D-E535-4790-B93C-1E60AD624C70} = {B1BA53C8-CCCF-46D5-BA9F-6031811F2E19} {C2C87D71-4BB3-4DD1-BE4D-35DEBF927290} = {0581267E-B53B-4BBE-BB90-80C3CA0C4ACD} {358CD7A8-E78B-4DCD-8640-EA62442755D7} = {23E375E0-8A4A-4D6A-8C96-9F2046CE9EB0} + {F8B5E3A1-2D4C-4F8A-9E5B-1C3D4E5F6A7B} = {23E375E0-8A4A-4D6A-8C96-9F2046CE9EB0} {47D45F49-E8E0-4267-9FDA-8635DA6F5348} = {B1BA53C8-CCCF-46D5-BA9F-6031811F2E19} {F91B0866-9DF5-488D-AD3E-1E471509C60F} = {34A1DC50-486C-4BB9-9929-1805CA0B0DD0} {76D4F5E7-2C04-420D-A43A-B721D2F32904} = {23E375E0-8A4A-4D6A-8C96-9F2046CE9EB0} diff --git a/sample/Magma.NetMap.UdpEchoServer/Magma.NetMap.UdpEchoServer.csproj b/sample/Magma.NetMap.UdpEchoServer/Magma.NetMap.UdpEchoServer.csproj new file mode 100644 index 0000000..0591053 --- /dev/null +++ b/sample/Magma.NetMap.UdpEchoServer/Magma.NetMap.UdpEchoServer.csproj @@ -0,0 +1,18 @@ + + + + Exe + net10.0 + true + latest + + + + + + + + + + + diff --git a/sample/Magma.NetMap.UdpEchoServer/Program.cs b/sample/Magma.NetMap.UdpEchoServer/Program.cs new file mode 100644 index 0000000..f8b7e63 --- /dev/null +++ b/sample/Magma.NetMap.UdpEchoServer/Program.cs @@ -0,0 +1,130 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Magma.Internet.Ip; +using Magma.Network.Header; + +namespace Magma.NetMap.UdpEchoServer +{ + class Program + { + static void Main(string[] args) + { + var listenPort = (ushort)7777; + + if (args.Length >= 1) + { + if (ushort.TryParse(args[0], out var port)) + { + listenPort = port; + } + } + + Console.WriteLine("=== Magma UDP Echo Server Sample ==="); + Console.WriteLine($"Listen Port: {listenPort}"); + Console.WriteLine($"Ethernet Header: {Unsafe.SizeOf()} bytes"); + Console.WriteLine($"IPv4 Header: {Unsafe.SizeOf()} bytes"); + Console.WriteLine($"UDP Header: {Unsafe.SizeOf()} bytes"); + Console.WriteLine(); + Console.WriteLine("This sample demonstrates:"); + Console.WriteLine(" - UDP header parsing using Magma.Transport.Udp"); + Console.WriteLine(" - End-to-end packet processing (Ethernet -> IPv4 -> UDP)"); + Console.WriteLine(" - Zero-copy packet manipulation with Span"); + Console.WriteLine(); + Console.WriteLine("To run this sample with NetMap:"); + Console.WriteLine(" 1. Ensure NetMap kernel module is loaded"); + Console.WriteLine(" 2. Run with sudo or appropriate capabilities"); + Console.WriteLine(" 3. Specify network interface: dotnet run -- eth0"); + Console.WriteLine(); + Console.WriteLine("Example UDP packet processing:"); + Console.WriteLine(); + + DemonstrateUdpParsing(listenPort); + + Console.WriteLine(); + Console.WriteLine("Sample completed. See README.md for integration with NetMap."); + } + + static unsafe void DemonstrateUdpParsing(ushort listenPort) + { + var packet = CreateSampleUdpPacket(listenPort); + + Console.WriteLine($"Sample packet ({packet.Length} bytes):"); + Console.WriteLine(BitConverter.ToString(packet)); + Console.WriteLine(); + + if (Ethernet.TryConsume(packet, out var ethernet, out var etherData)) + { + Console.WriteLine(ethernet.ToString()); + + if (ethernet.Ethertype == EtherType.IPv4) + { + if (IPv4.TryConsume(etherData, out var ipv4, out var ipData)) + { + Console.WriteLine(ipv4.ToString()); + + if (ipv4.Protocol == ProtocolNumber.Udp) + { + if (Udp.TryConsume(ipData, out var udp, out var udpData)) + { + Console.WriteLine(udp.ToString()); + Console.WriteLine($"+- UDP Data -------------------------------------------------------------------+"); + Console.WriteLine($"| Length: {udpData.Length} bytes".PadRight(87) + "|"); + if (udpData.Length > 0) + { + var preview = udpData.Length > 60 ? udpData.Slice(0, 60) : udpData; + Console.WriteLine($"| {BitConverter.ToString(preview.ToArray())}".PadRight(87) + "|"); + } + Console.WriteLine($"+------------------------------------------------------------------------------+"); + Console.WriteLine(); + Console.WriteLine($"✓ Successfully parsed UDP packet to port {udp.DestinationPort}"); + } + } + } + } + } + } + + static byte[] CreateSampleUdpPacket(ushort destPort) + { + var packet = new byte[64]; + var span = packet.AsSpan(); + + ref byte current = ref MemoryMarshal.GetReference(span); + + ref var ethernet = ref Unsafe.As(ref current); + ethernet.Destination = new MacAddress(0x00, 0x11, 0x22, 0x33, 0x44, 0x55); + ethernet.Source = new MacAddress(0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF); + ethernet.Ethertype = EtherType.IPv4; + + current = ref Unsafe.Add(ref current, Unsafe.SizeOf()); + + ref var ipv4 = ref Unsafe.As(ref current); + ipv4.VersionAndHeaderLength = 0x45; + ipv4.TypeOfService = 0; + ipv4.TotalLength = (ushort)(20 + 8 + 10); + ipv4.Identification = 12345; + ipv4.FlagsAndFragmentOffset = 0; + ipv4.TimeToLive = 64; + ipv4.Protocol = ProtocolNumber.Udp; + ipv4.SourceAddress = new IPv4Address(192, 168, 1, 100); + ipv4.DestinationAddress = new IPv4Address(192, 168, 1, 1); + ipv4.HeaderChecksum = 0; + + current = ref Unsafe.Add(ref current, Unsafe.SizeOf()); + + ref var udp = ref Unsafe.As(ref current); + udp.SourcePort = 12345; + udp.DestinationPort = destPort; + udp.Length = (ushort)(8 + 10); + udp.Checksum = 0; + + current = ref Unsafe.Add(ref current, Unsafe.SizeOf()); + + var data = "Hello UDP!"u8.ToArray(); + data.AsSpan().CopyTo(MemoryMarshal.CreateSpan(ref current, data.Length)); + + return packet; + } + } +} diff --git a/sample/Magma.NetMap.UdpEchoServer/README.md b/sample/Magma.NetMap.UdpEchoServer/README.md new file mode 100644 index 0000000..c674de7 --- /dev/null +++ b/sample/Magma.NetMap.UdpEchoServer/README.md @@ -0,0 +1,154 @@ +# Magma UDP Echo Server Sample + +A simple UDP echo server demonstrating end-to-end usage of the Magma network stack with UDP transport. + +## Overview + +This sample application shows how to: +- Parse incoming UDP packets using the Magma stack (Ethernet → IPv4 → UDP) +- Process UDP datagrams at the transport layer +- Construct and transmit UDP response packets +- Use NetMap for high-performance packet I/O on Linux + +The server listens on a specified UDP port and echoes back any received data to the sender. + +## What It Demonstrates + +- **UDP Header Parsing**: Uses `Udp.TryConsume()` to parse UDP headers from raw packet data +- **Packet Construction**: Builds outgoing UDP packets from scratch, including: + - Ethernet frame with swapped MAC addresses + - IPv4 header with swapped IP addresses and checksum calculation + - UDP header with swapped ports +- **Zero-Copy Processing**: Direct manipulation of packet buffers using `Span` and unsafe pointers +- **NetMap Integration**: High-performance packet I/O using Linux NetMap interface + +## Building + +From the repository root: + +```bash +dotnet build sample/Magma.NetMap.UdpEchoServer +``` + +## Running + +### Prerequisites + +- Linux with NetMap kernel module loaded +- Network interface available for NetMap (typically a dedicated interface) +- Root privileges or appropriate capabilities + +### Start the Server + +```bash +# Run with default settings (eth0, port 7777) +sudo dotnet run --project sample/Magma.NetMap.UdpEchoServer + +# Specify interface and port +sudo dotnet run --project sample/Magma.NetMap.UdpEchoServer -- eth1 8888 +``` + +### Test the Server + +From another machine on the same network: + +```bash +# Using netcat (nc) +echo "Hello Magma!" | nc -u 7777 + +# Using Python +python3 -c "import socket; s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.sendto(b'Hello', ('', 7777)); print(s.recvfrom(1024))" + +# Using socat +echo "Test message" | socat - UDP4::7777 +``` + +## Architecture + +``` +┌─────────────────────────────────────────────────────────┐ +│ Application │ +│ (UdpEchoReceiver) │ +└────────────────────┬────────────────────────────────────┘ + │ + │ IPacketReceiver.TryConsume() + │ +┌────────────────────▼────────────────────────────────────┐ +│ NetMap Transport │ +│ (NetMapPort) │ +└────────────────────┬────────────────────────────────────┘ + │ + │ Direct packet I/O + │ +┌────────────────────▼────────────────────────────────────┐ +│ Network Interface │ +│ (NetMap ring) │ +└─────────────────────────────────────────────────────────┘ +``` + +## Code Flow + +### Receive Path + +1. NetMap delivers raw packet buffer to `UdpEchoReceiver.TryConsume()` +2. Parse Ethernet frame: `Ethernet.TryConsume()` +3. Check for IPv4: `etherIn.Ethertype == EtherType.IPv4` +4. Parse IPv4 header: `IPv4.TryConsume()` +5. Check for UDP: `ipIn.Protocol == ProtocolNumber.Udp` +6. Parse UDP header: `Udp.TryConsume()` +7. Check destination port matches listen port + +### Transmit Path + +1. Get transmit buffer: `_transmitter.TryGetNextBuffer()` +2. Build Ethernet header (swap source/dest MACs) +3. Build IPv4 header (swap source/dest IPs, calculate checksum) +4. Build UDP header (swap source/dest ports) +5. Copy original data payload +6. Send packet: `_transmitter.SendBuffer()` and `ForceFlush()` + +## Technical Details + +### UDP Header Structure + +The `Udp` struct in `Magma.Transport.Udp` represents the 8-byte UDP header: + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Source Port | Destination Port | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Length | Checksum | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +- **Source/Destination Port**: 16-bit port numbers +- **Length**: Total length of UDP header + data (minimum 8 bytes) +- **Checksum**: Optional error checking (set to 0 in this sample) + +### Performance Characteristics + +- **Zero-copy**: Direct buffer manipulation using `Unsafe` and `Span` +- **Minimal allocations**: Packet buffers are pooled by NetMap +- **Kernel bypass**: NetMap bypasses the Linux network stack for lower latency + +## Limitations + +- **NetMap only**: Requires Linux with NetMap kernel module +- **No checksum validation**: UDP checksum is not validated on receive +- **No fragmentation**: Only handles packets that fit in a single Ethernet frame +- **Simplified error handling**: Production code would need more robust error checking + +## Related Components + +- `src/Magma.Transport.Udp` - UDP protocol implementation +- `src/Magma.Internet.Ip` - IPv4/IPv6 headers +- `src/Magma.Link` - Ethernet frame handling +- `src/Magma.NetMap` - NetMap transport integration + +## See Also + +- [RFC 768 - User Datagram Protocol](https://www.rfc-editor.org/rfc/rfc768) +- [NetMap - Fast packet I/O framework](http://info.iet.unipi.it/~luigi/netmap/) +- Other samples: `sample/Magma.NetMap.TcpHost`, `samples/Magma.NetMap.Host` diff --git a/src/Magma.Transport.Udp/Header/Udp.cs b/src/Magma.Transport.Udp/Header/Udp.cs index 8a3cadb..07cfade 100644 --- a/src/Magma.Transport.Udp/Header/Udp.cs +++ b/src/Magma.Transport.Udp/Header/Udp.cs @@ -1,8 +1,82 @@ using System; +using System.Net; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace Magma.Network.Header { + /// + /// Represents a UDP header as defined in RFC 768. + /// UDP header is 8 bytes: Source Port (2), Destination Port (2), Length (2), Checksum (2) + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] public struct Udp { + private ushort _sourcePort; + private ushort _destinationPort; + private ushort _length; + private ushort _checksum; + + /// + /// Source port number. + /// + public ushort SourcePort + { + get => (ushort)IPAddress.NetworkToHostOrder((short)_sourcePort); + set => _sourcePort = (ushort)IPAddress.HostToNetworkOrder((short)value); + } + + /// + /// Destination port number. + /// + public ushort DestinationPort + { + get => (ushort)IPAddress.NetworkToHostOrder((short)_destinationPort); + set => _destinationPort = (ushort)IPAddress.HostToNetworkOrder((short)value); + } + + /// + /// Length in bytes of the UDP header and data. + /// Minimum value is 8 (header only). + /// + public ushort Length + { + get => (ushort)IPAddress.NetworkToHostOrder((short)_length); + set => _length = (ushort)IPAddress.HostToNetworkOrder((short)value); + } + + /// + /// Checksum for error-checking of the header and data. + /// + public ushort Checksum + { + get => (ushort)IPAddress.NetworkToHostOrder((short)_checksum); + set => _checksum = (ushort)IPAddress.HostToNetworkOrder((short)value); + } + + /// + /// Attempts to parse a UDP header from the input span. + /// + /// Input byte span containing UDP packet + /// Parsed UDP header + /// Remaining data after the UDP header + /// True if parsing succeeded, false otherwise + public static bool TryConsume(ReadOnlySpan input, out Udp udp, out ReadOnlySpan data) + { + if (input.Length >= Unsafe.SizeOf()) + { + udp = Unsafe.As(ref MemoryMarshal.GetReference(input)); + data = input.Slice(Unsafe.SizeOf()); + return true; + } + + udp = default; + data = default; + return false; + } + + public override string ToString() => + "+- UDP Datagram -----------------------------------------------------------------------+" + Environment.NewLine + + $"| :{SourcePort.ToString()} -> :{DestinationPort.ToString()} Length: {Length} Checksum: 0x{Checksum:X4}".PadRight(87) + "|"; } }