diff --git a/clash-eth.cabal b/clash-eth.cabal index 4ae08f90..5812d12a 100644 --- a/clash-eth.cabal +++ b/clash-eth.cabal @@ -115,6 +115,7 @@ library Clash.Cores.Ethernet.Mac.PaddingInserter Clash.Cores.Ethernet.Mac.Preamble Clash.Cores.Ethernet.Icmp + Clash.Cores.Ethernet.Udp Clash.Lattice.ECP5.Colorlight.CRG Clash.Lattice.ECP5.Colorlight.TopEntity Clash.Lattice.ECP5.Prims diff --git a/python_tests/test_arp_udp_echo.py b/python_tests/test_arp_udp_echo.py new file mode 100644 index 00000000..1708f8db --- /dev/null +++ b/python_tests/test_arp_udp_echo.py @@ -0,0 +1,17 @@ +import unittest +import os +import socket +import random + +dst_ip = '192.168.1.123' + +class TestArpUdpEcho(unittest.TestCase): + def testArpUdpEcho(self): + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: + s.settimeout(5) + + for _ in range(50): + data = os.urandom(random.randint(0,1000)) + port = random.randint(0, 65535) + s.sendto(data, (dst_ip, port)) + self.assertEqual((data, (dst_ip, port)), s.recvfrom(1500)) \ No newline at end of file diff --git a/python_tests/test_ip_echo.py b/python_tests/test_ip_echo.py deleted file mode 100644 index f54d9e01..00000000 --- a/python_tests/test_ip_echo.py +++ /dev/null @@ -1,66 +0,0 @@ -import unittest -import util -import os -import socket -import struct -import random - -IFNAME = os.environ['IFNAME'] -DEV = os.environ['DEV'] - -def open_socket(): - s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(3)) - s.bind((IFNAME, 0)) - return s - -src_ip = '192.168.1.0' -dst_ip = '192.168.1.123' -src_mac = b"\x8c\x8c\xaa\xc8\x2b\xee" # hardcoded in echo stack -dst_mac = b"\x00\x00\x00\xff\xff\xff" - -class TestEthUartEcho(unittest.TestCase): - def _test(self, dst_ip): - """ - Sends five IP packets and tests if they are sent back - """ - with open_socket() as s: - s.settimeout(5) - - for _ in range(5): - data = os.urandom(random.randint(0,25)) - payload = struct.pack("!HHHH", 1337, 1337, len(data) + 8, 0) + data - ip_header = create_ip_header(src_ip, dst_ip, len(payload)) - packet = dst_mac + src_mac + b"\x08\x00" + ip_header + payload - - s.send(packet) - - response = s.recv(2**16 - 1) - total_length = struct.unpack_from("!H", response, 16)[0] - response = response[:total_length + 14] - - expected_ip_header = create_ip_header(dst_ip, src_ip, len(payload)) - expected = src_mac + dst_mac + b"\x08\x00" + expected_ip_header + payload - - self.assertEqual(expected, response) - - def testWithIp(self): - self._test(dst_ip) - - def testWithBroadcast(self): - self._test("192.168.1.255") - -def create_ip_header(src_ip, dst_ip, payload_length): - header = bytearray(struct.pack('!BBHHHBBH4s4s', - (4 << 4) + 5, # version, ihl - 0, - 20 + payload_length, # total length - 0, - 0, - 64, # TTL. Set to 64 so Wireshark thinks this is a totally normal UDP packet - 0, # Protocol. 0, because we can't set this in ipLitePacketizerC - 0, # checksum - socket.inet_aton(src_ip), - socket.inet_aton(dst_ip))) - checksum = util.internet_checksum(header) - header[10:12] = struct.pack("!H", checksum) - return bytes(header) diff --git a/src/Clash/Cores/Ethernet/Examples/EchoStack.hs b/src/Clash/Cores/Ethernet/Examples/EchoStack.hs index 32298516..277bb8d6 100644 --- a/src/Clash/Cores/Ethernet/Examples/EchoStack.hs +++ b/src/Clash/Cores/Ethernet/Examples/EchoStack.hs @@ -7,25 +7,37 @@ Description : Simple Ethernet echo stack. -} module Clash.Cores.Ethernet.Examples.EchoStack ( ipEchoStackC + , fullStackC + , arpIcmpUdpStackC ) where +import Data.Bifunctor qualified as B + -- import prelude import Clash.Prelude -- import ethernet -import Clash.Cores.Ethernet.Examples.RxStacks ( ipRxStack ) -import Clash.Cores.Ethernet.Examples.TxStacks ( ipTxStack ) -import Clash.Cores.Ethernet.Mac.EthernetTypes ( MacAddress(..) ) +import Clash.Cores.Ethernet.Arp +import Clash.Cores.Ethernet.Examples.RxStacks +import Clash.Cores.Ethernet.Examples.TxStacks +import Clash.Cores.Ethernet.IP.IPPacketizers +import Clash.Cores.Ethernet.Mac.EthernetTypes ( EthernetHeader(..), MacAddress(..) ) + +import Clash.Cores.Ethernet.IP.EthernetStream +import Clash.Cores.Ethernet.IP.IPv4Types -- import protocols import Protocols import Protocols.Extra.PacketStream import Protocols.Extra.PacketStream.PacketBuffer ( packetBufferC ) +import Protocols.Extra.PacketStream.Routing import Clash.Cores.Crc ( HardwareCrc ) import Clash.Cores.Crc.Catalog ( Crc32_ethernet ) -import Clash.Cores.Ethernet.IP.IPv4Types +import Clash.Cores.Ethernet.Icmp ( icmpEchoResponderC ) +import Clash.Cores.Ethernet.Udp + -- | Processes IP packets and echoes them back ipEchoStackC @@ -54,3 +66,71 @@ ipEchoStackC rxClk rxRst rxEn txClk txRst txEn mac ip = ckt |> packetBufferC d10 d4 |> mapMeta swapIp |> ipTxStack @4 txClk txRst txEn mac + +-- | Full stack from ethernet to ethernet. +fullStackC + :: forall + (dom :: Domain) + (domEthRx :: Domain) + (domEthTx :: Domain) + . KnownDomain dom + => KnownDomain domEthRx + => KnownDomain domEthTx + => HardwareCrc Crc32_ethernet 8 4 + => 1 <= DomainPeriod dom + => DomainPeriod dom <= 5 * 10^11 + => KnownNat (DomainPeriod dom) + => HiddenClockResetEnable dom + => Clock domEthRx + -> Reset domEthRx + -> Enable domEthRx + -> Clock domEthTx + -> Reset domEthTx + -> Enable domEthTx + -> Signal dom MacAddress + -- ^ My mac address + -> Signal dom (IPv4Address, IPv4Address) + -- ^ Tuple of my IP and subnet mask + -> Circuit (PacketStream domEthRx 1 ()) (PacketStream domEthTx 1 ()) +fullStackC rxClk rxRst rxEn txClk txRst txEn mac ip = + macRxStack @4 rxClk rxRst rxEn mac + |> arpIcmpUdpStackC mac ip (mapMeta $ B.second swapPorts) + |> macTxStack txClk txRst txEn + where + swapPorts hdr@UdpHeaderLite{..} = hdr + { _udplSrcPort = _udplDstPort + , _udplDstPort = _udplSrcPort + } + +-- | Wraps a circuit that handles UDP packets into a stack that handles IP, ICMP +-- and ARP. +arpIcmpUdpStackC + :: forall (dataWidth :: Nat) (dom :: Domain) + . HiddenClockResetEnable dom + => KnownNat dataWidth + => 1 <= dataWidth + => 1 <= DomainPeriod dom + => DomainPeriod dom <= 5 * 10^11 + => KnownNat (DomainPeriod dom) + => Signal dom MacAddress + -- ^ My MAC Address + -> Signal dom (IPv4Address, IPv4Address) + -- ^ My IP address and the subnet + -> Circuit (PacketStream dom dataWidth (IPv4Address, UdpHeaderLite)) (PacketStream dom dataWidth (IPv4Address, UdpHeaderLite)) + -- ^ UDP handler circuit + -> Circuit (PacketStream dom dataWidth EthernetHeader) (PacketStream dom dataWidth EthernetHeader) +arpIcmpUdpStackC macAddressS ipS udpCkt = circuit $ \ethIn -> do + [arpEthIn, ipEthIn] <- packetDispatcherC (routeBy _etherType $ 0x0806 :> 0x0800 :> Nil) -< ethIn + ipTx <- ipLitePacketizerC <| packetBufferC d10 d4 <| icmpUdpStack <| packetBufferC d10 d4 <| filterMetaS (isForMyIp <$> ipS) <| ipDepacketizerLiteC -< ipEthIn + (ipEthOut, arpLookup) <- toEthernetStreamC macAddressS -< ipTx + arpEthOut <- arpC d10 d5 macAddressS (fst <$> ipS) -< (arpEthIn, arpLookup) + packetArbiterC RoundRobin -< [arpEthOut, ipEthOut] + + where + icmpUdpStack = circuit $ \ipIn -> do + [icmpIn, udpIn] <- packetDispatcherC (routeBy _ipv4lProtocol $ 0x0001 :> 0x0011 :> Nil) -< ipIn + icmpOut <- icmpEchoResponderC @dom @dataWidth (fst <$> ipS) -< icmpIn + udpInParsed <- udpDepacketizerC -< udpIn + udpOutParsed <- udpPacketizerC (fst <$> ipS) <| udpCkt -< udpInParsed + packetArbiterC RoundRobin -< [icmpOut, udpOutParsed] + isForMyIp (ip, subnet) (_ipv4lDestination -> to) = to == ip || to == ipv4Broadcast ip subnet diff --git a/src/Clash/Cores/Ethernet/IP/IPv4Types.hs b/src/Clash/Cores/Ethernet/IP/IPv4Types.hs index 49befb84..4cff5e56 100644 --- a/src/Clash/Cores/Ethernet/IP/IPv4Types.hs +++ b/src/Clash/Cores/Ethernet/IP/IPv4Types.hs @@ -76,6 +76,7 @@ data IPv4Header = IPv4Header data IPv4HeaderLite = IPv4HeaderLite { _ipv4lSource :: IPv4Address , _ipv4lDestination :: IPv4Address + , _ipv4lProtocol :: Unsigned 8 , _ipv4lPayloadLength :: Unsigned 16 } deriving (Show, ShowX, Eq, Generic, BitPack, NFDataX, NFData) @@ -83,6 +84,7 @@ toLite :: IPv4Header -> IPv4HeaderLite toLite IPv4Header {..} = IPv4HeaderLite { _ipv4lSource = _ipv4Source , _ipv4lDestination = _ipv4Destination + , _ipv4lProtocol = _ipv4Protocol , _ipv4lPayloadLength = _ipv4Length - 20 -- We do not support IHLs other than 5 } @@ -104,7 +106,7 @@ fromLite header = IPv4Header { _ipv4Version = 4 , _ipv4FlagMF = False , _ipv4FragmentOffset = 0 , _ipv4Ttl = 64 - , _ipv4Protocol = 0 + , _ipv4Protocol = _ipv4lProtocol header , _ipv4Checksum = 0 , _ipv4Source = _ipv4lSource header , _ipv4Destination = _ipv4lDestination header diff --git a/src/Clash/Cores/Ethernet/Mac/EthernetTypes.hs b/src/Clash/Cores/Ethernet/Mac/EthernetTypes.hs index e471b7ec..adc4d9ac 100644 --- a/src/Clash/Cores/Ethernet/Mac/EthernetTypes.hs +++ b/src/Clash/Cores/Ethernet/Mac/EthernetTypes.hs @@ -71,7 +71,7 @@ toEthernetC macSrc = Circuit (swap . unbundle . helper macSrc . bundle) } hardCodedMac :: MacAddress -hardCodedMac = MacAddress (0x8C :> 0x8C :> 0xAA :> 0xC8 :> 0x2B :> 0xEE :> Nil) +hardCodedMac = MacAddress (0x00 :> 0xe0 :> 0x6c :> 0x38 :> 0xd0:> 0x2c :> Nil) -- | Broadcast MAC address. broadcastMac :: MacAddress diff --git a/src/Clash/Cores/Ethernet/Udp.hs b/src/Clash/Cores/Ethernet/Udp.hs new file mode 100644 index 00000000..37deffd4 --- /dev/null +++ b/src/Clash/Cores/Ethernet/Udp.hs @@ -0,0 +1,89 @@ +{-# language RecordWildCards #-} + +{-| +Module : Clash.Cores.Ethernet.Udp +Description : Circuits and data types to handle the UDP protocol +-} +module Clash.Cores.Ethernet.Udp + ( UdpHeaderLite(..) + , udpDepacketizerC + , udpPacketizerC + ) + where + +import Clash.Cores.Ethernet.IP.IPv4Types +import Clash.Prelude +import Protocols +import Protocols.Extra.PacketStream +import Protocols.Extra.PacketStream.Packetizers ( depacketizerC, packetizerC ) + +-- | The full UDP header +data UdpHeader = UdpHeader + { _udpSrcPort :: Unsigned 16 + -- ^ Source port + , _udpDstPort :: Unsigned 16 + -- ^ Destination port + , _udpLength :: Unsigned 16 + -- ^ length of header + payload + , _udpChecksum :: Unsigned 16 + -- ^ UDP Checksum, We do not validate or generate it + } deriving (Generic, NFDataX, BitPack, Eq, Show, ShowX) + +-- | UDP header +data UdpHeaderLite = UdpHeaderLite + { _udplSrcPort :: Unsigned 16 + -- ^ Source port + , _udplDstPort :: Unsigned 16 + -- ^ Destination port + , _udplPayloadLength :: Unsigned 16 + -- ^ Length of payload + } deriving (Generic, NFDataX, BitPack, Eq, Show, ShowX) + +fromUdpLite :: UdpHeaderLite -> UdpHeader +fromUdpLite UdpHeaderLite{..} = UdpHeader + { _udpSrcPort = _udplSrcPort + , _udpDstPort = _udplDstPort + , _udpLength = _udplPayloadLength + 8 + , _udpChecksum = 0 + } + +toUdpLite :: UdpHeader -> UdpHeaderLite +toUdpLite UdpHeader{..} = UdpHeaderLite + { _udplSrcPort = _udpSrcPort + , _udplDstPort = _udpDstPort + , _udplPayloadLength = _udpLength - 8 + } + +-- | Parses out the UDP header from an IP stream, but ignores the checksum. +-- The first element of the metadata is the source IP of incoming packets. +udpDepacketizerC + :: HiddenClockResetEnable dom + => KnownNat n + => 1 <= n + => Circuit + (PacketStream dom n IPv4HeaderLite) + (PacketStream dom n (IPv4Address, UdpHeaderLite)) +udpDepacketizerC = depacketizerC (\udph ipv4lh -> (_ipv4lSource ipv4lh, toUdpLite udph)) + +-- | Serializes the UDP packet to an IP stream. The first element of the metadata +-- is the destination IP for outgoing packets. No checksum is included in the UDP header. +udpPacketizerC + :: HiddenClockResetEnable dom + => KnownNat n + => 1 <= n + => Signal dom IPv4Address + -- ^ Source IP address + -> Circuit + (PacketStream dom n (IPv4Address, UdpHeaderLite)) + (PacketStream dom n IPv4HeaderLite) +udpPacketizerC myIp = mapMetaS (toIp <$> myIp) |> packetizerC fst snd + where + toIp srcIp (dstIp, udpLite) = (ipLite, udpHeader) + where + udpHeader = fromUdpLite udpLite + ipLite = IPv4HeaderLite + { _ipv4lSource = srcIp + , _ipv4lDestination = dstIp + , _ipv4lProtocol = 0x0011 + , _ipv4lPayloadLength = _udpLength udpHeader + } diff --git a/src/Clash/Lattice/ECP5/Colorlight/TopEntity.hs b/src/Clash/Lattice/ECP5/Colorlight/TopEntity.hs index 66e47646..d37d3cac 100644 --- a/src/Clash/Lattice/ECP5/Colorlight/TopEntity.hs +++ b/src/Clash/Lattice/ECP5/Colorlight/TopEntity.hs @@ -18,7 +18,6 @@ import Clash.Prelude ( exposeClockResetEnable ) import Clash.Cores.Crc ( deriveHardwareCrc ) import Clash.Cores.Crc.Catalog ( Crc32_ethernet ) -import Clash.Cores.Ethernet.Examples.ArpStack import Clash.Cores.Ethernet.IP.IPv4Types ( IPv4Address(IPv4Address) ) import Clash.Cores.Ethernet.Mac.EthernetTypes ( MacAddress(MacAddress) ) import Clash.Lattice.ECP5.Colorlight.CRG @@ -27,7 +26,7 @@ import Clash.Lattice.ECP5.RGMII ( RGMIIRXChannel(..), RGMIITXChannel(..), rgmiiT import Protocols ( toSignals, (|>) ) -import Clash.Cores.Ethernet.Examples.EchoStack ( ipEchoStackC ) +import Clash.Cores.Ethernet.Examples.EchoStack ( fullStackC ) import Data.Proxy ( Proxy(Proxy) ) @@ -93,7 +92,7 @@ topEntity clk25 uartRxBit _dq_in _mdio_in eth0_rx _eth1_rx = phyStack = exposeClockResetEnable (unsafeRgmiiRxC @DomEth0 @DomDDREth0 (delayg d80) iddrx1f) ethRxClk ethRxRst ethRxEn - |> exposeClockResetEnable (ipEchoStackC ethRxClk ethRxRst ethRxEn ethTxClk ethTxRst ethTxEn (pure ourMac) (pure ourIPv4)) clk50 rst50 en50 + |> exposeClockResetEnable (fullStackC ethRxClk ethRxRst ethRxEn ethTxClk ethTxRst ethTxEn (pure ourMac) (pure ourIPv4)) clk50 rst50 en50 |> exposeClockResetEnable (rgmiiTxC @DomEthTx @DomDDREth0 (delayg d0) oddrx1f) ethTxClk ethTxRst ethTxEn uartTxBit = uartRxBit diff --git a/src/Protocols/Extra/PacketStream/Routing.hs b/src/Protocols/Extra/PacketStream/Routing.hs index b52a6852..a50b500c 100644 --- a/src/Protocols/Extra/PacketStream/Routing.hs +++ b/src/Protocols/Extra/PacketStream/Routing.hs @@ -6,6 +6,7 @@ module Protocols.Extra.PacketStream.Routing ( packetArbiterC , ArbiterMode(..) , packetDispatcherC + , routeBy ) where import Clash.Prelude @@ -78,3 +79,12 @@ packetDispatcherC fs = Circuit (second unbundle . unbundle . fmap go . bundle . Just i -> (bwds !! i, replace i (Just x) (repeat Nothing)) _ -> (PacketStreamS2M True, repeat Nothing) go _ = (PacketStreamS2M False, repeat Nothing) + +-- | Routing function for `packetDispatcherC` that matches against values with +-- an `Eq` instance. Useful to route according to a record field. +routeBy + :: Eq b + => (a -> b) + -> Vec p b + -> Vec p (a -> Bool) +routeBy f = fmap $ \x -> (== x) . f diff --git a/tests/Test/Cores/Ethernet/Icmp.hs b/tests/Test/Cores/Ethernet/Icmp.hs index 9c103df0..ddfa17c0 100644 --- a/tests/Test/Cores/Ethernet/Icmp.hs +++ b/tests/Test/Cores/Ethernet/Icmp.hs @@ -47,7 +47,7 @@ genIpAddr :: Gen IPv4Address genIpAddr = IPv4Address <$> C.sequence (C.repeat @4 Gen.enumBounded) genIPv4HeaderLite :: Gen IPv4HeaderLite -genIPv4HeaderLite = IPv4HeaderLite <$> genIpAddr <*> genIpAddr <*> pure 0 +genIPv4HeaderLite = IPv4HeaderLite <$> genIpAddr <*> genIpAddr <*> Gen.enumBounded <*> pure 0 packetize :: 1 C.<= dataWidth diff --git a/tests/Test/Cores/Ethernet/Mac/EthernetTypes.hs b/tests/Test/Cores/Ethernet/Mac/EthernetTypes.hs index a560f080..10b928a8 100644 --- a/tests/Test/Cores/Ethernet/Mac/EthernetTypes.hs +++ b/tests/Test/Cores/Ethernet/Mac/EthernetTypes.hs @@ -60,7 +60,7 @@ toEthernetTest C.SNat macSrc = model src = map $ fmap (toEthernet src) hardCodedMac :: MacAddress - hardCodedMac = MacAddress (0x8C C.:> 0x8C C.:> 0xAA C.:> 0xC8 C.:> 0x2B C.:> 0xEE C.:> C.Nil) + hardCodedMac = MacAddress (0x00 C.:> 0xe0 C.:> 0x6c C.:> 0x38 C.:> 0xd0 C.:> 0x2c C.:> C.Nil) toEthernet :: MacAddress -> IPv4Address -> EthernetHeader toEthernet src _ = EthernetHeader {