From b999e61d61f6aa581808a94b7032b27116755125 Mon Sep 17 00:00:00 2001 From: soypat Date: Sat, 16 Sep 2023 18:43:53 -0300 Subject: [PATCH] add tcp sockets --- cmd/cyrw/main.go | 4 +- internal/tcpctl/dhcp.go | 2 +- internal/tcpctl/eth/definitions.go | 126 +++--- internal/tcpctl/eth/dhcpoption_string.go | 126 +++--- internal/tcpctl/eth/headers.go | 22 +- internal/tcpctl/socket_tcp.go | 113 ++++++ internal/tcpctl/socket_udp.go | 115 ++++++ internal/tcpctl/stack.go | 490 ++++++++++++----------- internal/tcpctl/tcpctl.go | 2 +- 9 files changed, 627 insertions(+), 373 deletions(-) create mode 100644 internal/tcpctl/socket_tcp.go create mode 100644 internal/tcpctl/socket_udp.go diff --git a/cmd/cyrw/main.go b/cmd/cyrw/main.go index a932bc6..153cc77 100644 --- a/cmd/cyrw/main.go +++ b/cmd/cyrw/main.go @@ -47,9 +47,7 @@ func main() { MAC: nil, MaxUDPConns: 2, }) - // stack.GlobalHandler = func(b []byte) { - // println("NEW payload:\n", hex.Dump(b)) - // } + dev.RecvEthHandle(stack.RecvEth) for { println("Trying DoDHCP") diff --git a/internal/tcpctl/dhcp.go b/internal/tcpctl/dhcp.go index 12bca5a..09179aa 100644 --- a/internal/tcpctl/dhcp.go +++ b/internal/tcpctl/dhcp.go @@ -62,7 +62,7 @@ func (d *DHCPClient) HandleUDP(resp []byte, packet *UDPPacket) (_ int, err error // print("DHCP Option received ", option.String()) optionData := incpayload[ptr+2 : ptr+2+int(optlen)] - if d.State == dhcpStateWaitAck && option == eth.DHCPMessageType && len(optionData) > 0 && optionData[0] == 5 { + if d.State == dhcpStateWaitAck && option == eth.DHCP_MessageType && len(optionData) > 0 && optionData[0] == 5 { d.State = dhcpStateDone return 0, nil } diff --git a/internal/tcpctl/eth/definitions.go b/internal/tcpctl/eth/definitions.go index 9f0e4f7..06fba44 100644 --- a/internal/tcpctl/eth/definitions.go +++ b/internal/tcpctl/eth/definitions.go @@ -54,68 +54,68 @@ type DHCPOption uint8 // DHCP options. Taken from https://help.sonicwall.com/help/sw/eng/6800/26/2/3/content/Network_DHCP_Server.042.12.htm. // -//go:generate stringer -type=DHCPOption -trimprefix=DHCP +//go:generate stringer -type=DHCPOption -trimprefix=DHCP_ const ( - DHCPWordAligned DHCPOption = 0 - DHCPSubnetMask DHCPOption = 1 - DHCPTimeOffset DHCPOption = 2 - DHCPRouter DHCPOption = 3 - DHCPTimeServers DHCPOption = 4 - DHCPNameServers DHCPOption = 5 - DHCPDNSServers DHCPOption = 6 - DHCPLogServers DHCPOption = 7 - DHCPCookieServers DHCPOption = 8 - DHCPLPRServers DHCPOption = 9 - DHCPImpressServers DHCPOption = 10 - DHCPRLPServers DHCPOption = 11 - DHCPHostName DHCPOption = 12 - DHCPBootFileSize DHCPOption = 13 - DHCPMeritDumpFile DHCPOption = 14 - DHCPDomainName DHCPOption = 15 - DHCPSwapServer DHCPOption = 16 - DHCPRootPath DHCPOption = 17 - DHCPExtensionFile DHCPOption = 18 - DHCPIPLayerForwarding DHCPOption = 19 - DHCPSrcrouteenabler DHCPOption = 20 - DHCPPolicyFilter DHCPOption = 21 - DHCPMaximumDGReassemblySize DHCPOption = 22 - DHCPDefaultIPTTL DHCPOption = 23 - DHCPPathMTUAgingTimeout DHCPOption = 24 - DHCPMTUPlateau DHCPOption = 25 - DHCPInterfaceMTUSize DHCPOption = 26 - DHCPAllSubnetsAreLocal DHCPOption = 27 - DHCPBroadcastAddress DHCPOption = 28 - DHCPPerformMaskDiscovery DHCPOption = 29 - DHCPProvideMasktoOthers DHCPOption = 30 - DHCPPerformRouterDiscovery DHCPOption = 31 - DHCPRouterSolicitationAddress DHCPOption = 32 - DHCPStaticRoutingTable DHCPOption = 33 - DHCPTrailerEncapsulation DHCPOption = 34 - DHCPARPCacheTimeout DHCPOption = 35 - DHCPEthernetEncapsulation DHCPOption = 36 - DHCPDefaultTCPTimetoLive DHCPOption = 37 - DHCPTCPKeepaliveInterval DHCPOption = 38 - DHCPTCPKeepaliveGarbage DHCPOption = 39 - DHCPNISDomainName DHCPOption = 40 - DHCPNISServerAddresses DHCPOption = 41 - DHCPNTPServersAddresses DHCPOption = 42 - DHCPVendorSpecificInformation DHCPOption = 43 - DHCPNetBIOSNameServer DHCPOption = 44 - DHCPNetBIOSDatagramDistribution DHCPOption = 45 - DHCPNetBIOSNodeType DHCPOption = 46 - DHCPNetBIOSScope DHCPOption = 47 - DHCPXWindowFontServer DHCPOption = 48 - DHCPXWindowDisplayManager DHCPOption = 49 - DHCPRequestedIPaddress DHCPOption = 50 - DHCPIPAddressLeaseTime DHCPOption = 51 - DHCPOptionOverload DHCPOption = 52 - DHCPMessageType DHCPOption = 53 - DHCPServerIdentification DHCPOption = 54 - DHCPParameterRequestList DHCPOption = 55 - DHCPMessage DHCPOption = 56 - DHCPMaximumMessageSize DHCPOption = 57 - DHCPRenewTimeValue DHCPOption = 58 - DHCPRebindingTimeValue DHCPOption = 59 - DHCPClientIdentifier DHCPOption = 60 - DHCPClientIdentifier1 DHCPOption = 61 + DHCP_WordAligned DHCPOption = 0 + DHCP_SubnetMask DHCPOption = 1 + DHCP_TimeOffset DHCPOption = 2 // Time offset in seconds from UTC + DHCP_Router DHCPOption = 3 // N/4 router addresses + DHCP_TimeServers DHCPOption = 4 // N/4 time server addresses + DHCP_NameServers DHCPOption = 5 // N/4 IEN-116 server addresses + DHCP_DNSServers DHCPOption = 6 // N/4 DNS server addresses + DHCP_LogServers DHCPOption = 7 // N/4 logging server addresses + DHCP_CookieServers DHCPOption = 8 // N/4 quote server addresses + DHCP_LPRServers DHCPOption = 9 // N/4 printer server addresses + DHCP_ImpressServers DHCPOption = 10 // N/4 impress server addresses + DHCP_RLPServers DHCPOption = 11 // N/4 RLP server addresses + DHCP_HostName DHCPOption = 12 // Hostname string + DHCP_BootFileSize DHCPOption = 13 // Size of boot file in 512 byte chunks + DHCP_MeritDumpFile DHCPOption = 14 // Client to dump and name of file to dump to + DHCP_DomainName DHCPOption = 15 // The DNS domain name of the client + DHCP_SwapServer DHCPOption = 16 // Swap server addresses + DHCP_RootPath DHCPOption = 17 // Path name for root disk + DHCP_ExtensionFile DHCPOption = 18 // Patch name for more BOOTP info + DHCP_IPLayerForwarding DHCPOption = 19 // Enable or disable IP forwarding + DHCP_Srcrouteenabler DHCPOption = 20 // Enable or disable source routing + DHCP_PolicyFilter DHCPOption = 21 // Routing policy filters + DHCP_MaximumDGReassemblySize DHCPOption = 22 // Maximum datagram reassembly size + DHCP_DefaultIPTTL DHCPOption = 23 // Default IP time-to-live + DHCP_PathMTUAgingTimeout DHCPOption = 24 // Path MTU aging timeout + DHCP_MTUPlateau DHCPOption = 25 // Path MTU plateau table + DHCP_InterfaceMTUSize DHCPOption = 26 // Interface MTU size + DHCP_AllSubnetsAreLocal DHCPOption = 27 // All subnets are local + DHCP_BroadcastAddress DHCPOption = 28 // Broadcast address + DHCP_PerformMaskDiscovery DHCPOption = 29 // Perform mask discovery + DHCP_ProvideMasktoOthers DHCPOption = 30 // Provide mask to others + DHCP_PerformRouterDiscovery DHCPOption = 31 // Perform router discovery + DHCP_RouterSolicitationAddress DHCPOption = 32 // Router solicitation address + DHCP_StaticRoutingTable DHCPOption = 33 // Static routing table + DHCP_TrailerEncapsulation DHCPOption = 34 // Trailer encapsulation + DHCP_ARPCacheTimeout DHCPOption = 35 // ARP cache timeout + DHCP_EthernetEncapsulation DHCPOption = 36 // Ethernet encapsulation + DHCP_DefaultTCPTimetoLive DHCPOption = 37 // Default TCP time to live + DHCP_TCPKeepaliveInterval DHCPOption = 38 // TCP keepalive interval + DHCP_TCPKeepaliveGarbage DHCPOption = 39 // TCP keepalive garbage + DHCP_NISDomainName DHCPOption = 40 // NIS domain name + DHCP_NISServerAddresses DHCPOption = 41 // NIS server addresses + DHCP_NTPServersAddresses DHCPOption = 42 // NTP servers addresses + DHCP_VendorSpecificInformation DHCPOption = 43 // Vendor specific information + DHCP_NetBIOSNameServer DHCPOption = 44 // NetBIOS name server + DHCP_NetBIOSDatagramDistribution DHCPOption = 45 // NetBIOS datagram distribution + DHCP_NetBIOSNodeType DHCPOption = 46 // NetBIOS node type + DHCP_NetBIOSScope DHCPOption = 47 // NetBIOS scope + DHCP_XWindowFontServer DHCPOption = 48 // X window font server + DHCP_XWindowDisplayManager DHCPOption = 49 // X window display manager + DHCP_RequestedIPaddress DHCPOption = 50 // Requested IP address + DHCP_IPAddressLeaseTime DHCPOption = 51 // IP address lease time + DHCP_OptionOverload DHCPOption = 52 // Overload “sname” or “file” + DHCP_MessageType DHCPOption = 53 // DHCP message type + DHCP_ServerIdentification DHCPOption = 54 // DHCP server identification + DHCP_ParameterRequestList DHCPOption = 55 // Parameter request list + DHCP_Message DHCPOption = 56 // DHCP error message + DHCP_MaximumMessageSize DHCPOption = 57 // DHCP maximum message size + DHCP_RenewTimeValue DHCPOption = 58 // DHCP renewal (T1) time + DHCP_RebindingTimeValue DHCPOption = 59 // DHCP rebinding (T2) time + DHCP_ClientIdentifier DHCPOption = 60 // Client identifier + DHCP_ClientIdentifier1 DHCPOption = 61 // Client identifier ) diff --git a/internal/tcpctl/eth/dhcpoption_string.go b/internal/tcpctl/eth/dhcpoption_string.go index c8ab25e..f6af653 100644 --- a/internal/tcpctl/eth/dhcpoption_string.go +++ b/internal/tcpctl/eth/dhcpoption_string.go @@ -1,4 +1,4 @@ -// Code generated by "stringer -type=DHCPOption -trimprefix=DHCP"; DO NOT EDIT. +// Code generated by "stringer -type=DHCPOption -trimprefix=DHCP_"; DO NOT EDIT. package eth @@ -8,68 +8,68 @@ func _() { // An "invalid array index" compiler error signifies that the constant values have changed. // Re-run the stringer command to generate them again. var x [1]struct{} - _ = x[DHCPWordAligned-0] - _ = x[DHCPSubnetMask-1] - _ = x[DHCPTimeOffset-2] - _ = x[DHCPRouter-3] - _ = x[DHCPTimeServers-4] - _ = x[DHCPNameServers-5] - _ = x[DHCPDNSServers-6] - _ = x[DHCPLogServers-7] - _ = x[DHCPCookieServers-8] - _ = x[DHCPLPRServers-9] - _ = x[DHCPImpressServers-10] - _ = x[DHCPRLPServers-11] - _ = x[DHCPHostName-12] - _ = x[DHCPBootFileSize-13] - _ = x[DHCPMeritDumpFile-14] - _ = x[DHCPDomainName-15] - _ = x[DHCPSwapServer-16] - _ = x[DHCPRootPath-17] - _ = x[DHCPExtensionFile-18] - _ = x[DHCPIPLayerForwarding-19] - _ = x[DHCPSrcrouteenabler-20] - _ = x[DHCPPolicyFilter-21] - _ = x[DHCPMaximumDGReassemblySize-22] - _ = x[DHCPDefaultIPTTL-23] - _ = x[DHCPPathMTUAgingTimeout-24] - _ = x[DHCPMTUPlateau-25] - _ = x[DHCPInterfaceMTUSize-26] - _ = x[DHCPAllSubnetsAreLocal-27] - _ = x[DHCPBroadcastAddress-28] - _ = x[DHCPPerformMaskDiscovery-29] - _ = x[DHCPProvideMasktoOthers-30] - _ = x[DHCPPerformRouterDiscovery-31] - _ = x[DHCPRouterSolicitationAddress-32] - _ = x[DHCPStaticRoutingTable-33] - _ = x[DHCPTrailerEncapsulation-34] - _ = x[DHCPARPCacheTimeout-35] - _ = x[DHCPEthernetEncapsulation-36] - _ = x[DHCPDefaultTCPTimetoLive-37] - _ = x[DHCPTCPKeepaliveInterval-38] - _ = x[DHCPTCPKeepaliveGarbage-39] - _ = x[DHCPNISDomainName-40] - _ = x[DHCPNISServerAddresses-41] - _ = x[DHCPNTPServersAddresses-42] - _ = x[DHCPVendorSpecificInformation-43] - _ = x[DHCPNetBIOSNameServer-44] - _ = x[DHCPNetBIOSDatagramDistribution-45] - _ = x[DHCPNetBIOSNodeType-46] - _ = x[DHCPNetBIOSScope-47] - _ = x[DHCPXWindowFontServer-48] - _ = x[DHCPXWindowDisplayManager-49] - _ = x[DHCPRequestedIPaddress-50] - _ = x[DHCPIPAddressLeaseTime-51] - _ = x[DHCPOptionOverload-52] - _ = x[DHCPMessageType-53] - _ = x[DHCPServerIdentification-54] - _ = x[DHCPParameterRequestList-55] - _ = x[DHCPMessage-56] - _ = x[DHCPMaximumMessageSize-57] - _ = x[DHCPRenewTimeValue-58] - _ = x[DHCPRebindingTimeValue-59] - _ = x[DHCPClientIdentifier-60] - _ = x[DHCPClientIdentifier1-61] + _ = x[DHCP_WordAligned-0] + _ = x[DHCP_SubnetMask-1] + _ = x[DHCP_TimeOffset-2] + _ = x[DHCP_Router-3] + _ = x[DHCP_TimeServers-4] + _ = x[DHCP_NameServers-5] + _ = x[DHCP_DNSServers-6] + _ = x[DHCP_LogServers-7] + _ = x[DHCP_CookieServers-8] + _ = x[DHCP_LPRServers-9] + _ = x[DHCP_ImpressServers-10] + _ = x[DHCP_RLPServers-11] + _ = x[DHCP_HostName-12] + _ = x[DHCP_BootFileSize-13] + _ = x[DHCP_MeritDumpFile-14] + _ = x[DHCP_DomainName-15] + _ = x[DHCP_SwapServer-16] + _ = x[DHCP_RootPath-17] + _ = x[DHCP_ExtensionFile-18] + _ = x[DHCP_IPLayerForwarding-19] + _ = x[DHCP_Srcrouteenabler-20] + _ = x[DHCP_PolicyFilter-21] + _ = x[DHCP_MaximumDGReassemblySize-22] + _ = x[DHCP_DefaultIPTTL-23] + _ = x[DHCP_PathMTUAgingTimeout-24] + _ = x[DHCP_MTUPlateau-25] + _ = x[DHCP_InterfaceMTUSize-26] + _ = x[DHCP_AllSubnetsAreLocal-27] + _ = x[DHCP_BroadcastAddress-28] + _ = x[DHCP_PerformMaskDiscovery-29] + _ = x[DHCP_ProvideMasktoOthers-30] + _ = x[DHCP_PerformRouterDiscovery-31] + _ = x[DHCP_RouterSolicitationAddress-32] + _ = x[DHCP_StaticRoutingTable-33] + _ = x[DHCP_TrailerEncapsulation-34] + _ = x[DHCP_ARPCacheTimeout-35] + _ = x[DHCP_EthernetEncapsulation-36] + _ = x[DHCP_DefaultTCPTimetoLive-37] + _ = x[DHCP_TCPKeepaliveInterval-38] + _ = x[DHCP_TCPKeepaliveGarbage-39] + _ = x[DHCP_NISDomainName-40] + _ = x[DHCP_NISServerAddresses-41] + _ = x[DHCP_NTPServersAddresses-42] + _ = x[DHCP_VendorSpecificInformation-43] + _ = x[DHCP_NetBIOSNameServer-44] + _ = x[DHCP_NetBIOSDatagramDistribution-45] + _ = x[DHCP_NetBIOSNodeType-46] + _ = x[DHCP_NetBIOSScope-47] + _ = x[DHCP_XWindowFontServer-48] + _ = x[DHCP_XWindowDisplayManager-49] + _ = x[DHCP_RequestedIPaddress-50] + _ = x[DHCP_IPAddressLeaseTime-51] + _ = x[DHCP_OptionOverload-52] + _ = x[DHCP_MessageType-53] + _ = x[DHCP_ServerIdentification-54] + _ = x[DHCP_ParameterRequestList-55] + _ = x[DHCP_Message-56] + _ = x[DHCP_MaximumMessageSize-57] + _ = x[DHCP_RenewTimeValue-58] + _ = x[DHCP_RebindingTimeValue-59] + _ = x[DHCP_ClientIdentifier-60] + _ = x[DHCP_ClientIdentifier1-61] } const _DHCPOption_name = "WordAlignedSubnetMaskTimeOffsetRouterTimeServersNameServersDNSServersLogServersCookieServersLPRServersImpressServersRLPServersHostNameBootFileSizeMeritDumpFileDomainNameSwapServerRootPathExtensionFileIPLayerForwardingSrcrouteenablerPolicyFilterMaximumDGReassemblySizeDefaultIPTTLPathMTUAgingTimeoutMTUPlateauInterfaceMTUSizeAllSubnetsAreLocalBroadcastAddressPerformMaskDiscoveryProvideMasktoOthersPerformRouterDiscoveryRouterSolicitationAddressStaticRoutingTableTrailerEncapsulationARPCacheTimeoutEthernetEncapsulationDefaultTCPTimetoLiveTCPKeepaliveIntervalTCPKeepaliveGarbageNISDomainNameNISServerAddressesNTPServersAddressesVendorSpecificInformationNetBIOSNameServerNetBIOSDatagramDistributionNetBIOSNodeTypeNetBIOSScopeXWindowFontServerXWindowDisplayManagerRequestedIPaddressIPAddressLeaseTimeOptionOverloadMessageTypeServerIdentificationParameterRequestListMessageMaximumMessageSizeRenewTimeValueRebindingTimeValueClientIdentifierClientIdentifier1" diff --git a/internal/tcpctl/eth/headers.go b/internal/tcpctl/eth/headers.go index eed1657..93e7d32 100644 --- a/internal/tcpctl/eth/headers.go +++ b/internal/tcpctl/eth/headers.go @@ -470,12 +470,19 @@ func (tcphdr *TCPHeader) Put(buf []byte) { binary.BigEndian.PutUint16(buf[18:], tcphdr.UrgentPtr) } +// Offset specifies the size of the TCP header in 32-bit words. The minimum size +// header is 5 words and the maximum is 15 words thus giving the minimum size of +// 20 bytes and maximum of 60 bytes, allowing for up to 40 bytes of options in +// the header. This field gets its name from the fact that it is also the offset +// from the start of the TCP segment to the actual data. func (tcphdr *TCPHeader) Offset() (tcpWords uint8) { return uint8(tcphdr.OffsetAndFlags[0] >> (8 + 4)) } -func (tcphdr *TCPHeader) OffsetInBytes() (offsetInBytes uint16) { - return uint16(tcphdr.Offset()) * tcpWordlen +// OffsetInBytes returns the size of the TCP header in bytes, including options. +// See [TCPHeader.Offset] for more information. +func (tcphdr *TCPHeader) OffsetInBytes() uint8 { + return tcphdr.Offset() * tcpWordlen } func (tcphdr *TCPHeader) Flags() TCPFlags { @@ -495,17 +502,6 @@ func (tcphdr *TCPHeader) SetOffset(tcpWords uint8) { tcphdr.OffsetAndFlags[0] |= onlyFlags | (uint16(tcpWords) << 12) } -// FrameLength returns the size of the TCP frame as described by tcphdr and -// payloadLength, which is the size of the TCP payload not including the TCP options. -func (tcphdr *TCPHeader) FrameLength(payloadLength uint16) uint16 { - return tcphdr.OffsetInBytes() + payloadLength -} - -// OptionsLength returns the length of the options section -func (tcphdr *TCPHeader) OptionsLength() uint16 { - return tcphdr.OffsetInBytes()*tcpWordlen - 20 -} - // CalculateChecksumIPv4 calculates the checksum of the TCP header, options and payload. func (tcphdr *TCPHeader) CalculateChecksumIPv4(pseudoHeader *IPv4Header, tcpOptions, payload []byte) uint16 { const sizePseudo = 12 diff --git a/internal/tcpctl/socket_tcp.go b/internal/tcpctl/socket_tcp.go new file mode 100644 index 0000000..e074912 --- /dev/null +++ b/internal/tcpctl/socket_tcp.go @@ -0,0 +1,113 @@ +package tcpctl + +import ( + "strconv" + "time" + + "github.com/soypat/cyw43439/internal/tcpctl/eth" +) + +type tcpSocket struct { + LastRx time.Time + handler func(response []byte, self *TCPPacket) (int, error) + Port uint16 + packets [1]TCPPacket +} + +type TCPPacket struct { + Rx time.Time + Eth eth.EthernetHeader + IP eth.IPv4Header + TCP eth.TCPHeader + payload [_MTU - eth.SizeEthernetHeader - eth.SizeIPv4Header - eth.SizeTCPHeader]byte +} + +// NeedsHandling returns true if the socket needs handling before it can +// admit more pending packets. +func (u *tcpSocket) NeedsHandling() bool { + // As of now socket has space for 1 packet so if packet is pending, queue is full. + // Compile time check to ensure this is fulfilled: + _ = u.packets[1-len(u.packets)] + return u.IsPendingHandling() +} + +// IsPendingHandling returns true if there are packet(s) pending handling. +func (u *tcpSocket) IsPendingHandling() bool { + return u.Port != 0 && !u.packets[0].Rx.IsZero() +} + +// HandleEth writes the socket's response into dst to be sent over an ethernet interface. +// HandleEth can return 0 bytes written and a nil error to indicate no action must be taken. +// If +func (u *tcpSocket) HandleEth(dst []byte) (int, error) { + if u.handler == nil { + panic("nil udp handler on port " + strconv.Itoa(int(u.Port))) + } + packet := &u.packets[0] + + n, err := u.handler(dst, &u.packets[0]) + packet.Rx = time.Time{} // Invalidate packet. + return n, err +} + +// Open sets the UDP handler and opens the port. +func (u *tcpSocket) Open(port uint16, h func([]byte, *TCPPacket) (int, error)) { + if port == 0 || h == nil { + panic("invalid port or nil handler" + strconv.Itoa(int(u.Port))) + } + u.handler = h + u.Port = port + for i := range u.packets { + u.packets[i].Rx = time.Time{} // Invalidate packets. + } +} + +func (s *tcpSocket) pending() (p uint32) { + for i := range s.packets { + if s.packets[i].HasPacket() { + p++ + } + } + return p +} + +func (u *tcpSocket) Close() { + u.handler = nil + u.Port = 0 // Port 0 flags the port is inactive. +} + +func (u *tcpSocket) forceResponse() (added bool) { + if !u.IsPendingHandling() { + added = true + u.packets[0].Rx = forcedTime + } + return added +} + +func (u *TCPPacket) HasPacket() bool { + return u.Rx != forcedTime && !u.Rx.IsZero() +} + +func (p *TCPPacket) PutHeaders(b []byte) { + if len(b) < eth.SizeEthernetHeader+eth.SizeIPv4Header+eth.SizeTCPHeader { + panic("short tcpPacket buffer") + } + p.Eth.Put(b) + p.IP.Put(b[eth.SizeEthernetHeader:]) + p.TCP.Put(b[eth.SizeEthernetHeader+eth.SizeIPv4Header:]) +} + +// Payload returns the UDP payload. If UDP or IPv4 header data is incorrect/bad it returns nil. +// If the response is "forced" then payload will be nil. +func (p *TCPPacket) Payload() []byte { + if !p.HasPacket() { + return nil + } + // TODO(soypat): store TCP options in payload. + // options := p.payload[:p.TCP.OffsetInBytes()-eth.SizeTCPHeader] + ipLen := int(p.IP.TotalLength) - int(p.IP.IHL()*4) - eth.SizeTCPHeader // Total length(including header) - header length = payload length + if ipLen > len(p.payload) { + return nil // Mismatching IP and UDP data or bad length. + } + return p.payload[:ipLen] +} diff --git a/internal/tcpctl/socket_udp.go b/internal/tcpctl/socket_udp.go new file mode 100644 index 0000000..c0ea155 --- /dev/null +++ b/internal/tcpctl/socket_udp.go @@ -0,0 +1,115 @@ +package tcpctl + +import ( + "strconv" + "time" + + "github.com/soypat/cyw43439/internal/tcpctl/eth" +) + +type udpSocket struct { + LastRx time.Time + handler func(response []byte, self *UDPPacket) (int, error) + Port uint16 + packets [1]UDPPacket +} + +type UDPPacket struct { + Rx time.Time + Eth eth.EthernetHeader + IP eth.IPv4Header + UDP eth.UDPHeader + payload [_MTU - eth.SizeEthernetHeader - eth.SizeIPv4Header - eth.SizeUDPHeader]byte +} + +// NeedsHandling returns true if the socket needs handling before it can +// admit more pending packets. +func (u *udpSocket) NeedsHandling() bool { + // As of now socket has space for 1 packet so if packet is pending, queue is full. + // Compile time check to ensure this is fulfilled: + _ = u.packets[1-len(u.packets)] + return u.IsPendingHandling() +} + +// IsPendingHandling returns true if there are packet(s) pending handling. +func (u *udpSocket) IsPendingHandling() bool { + return u.Port != 0 && !u.packets[0].Rx.IsZero() +} + +// HandleEth writes the socket's response into dst to be sent over an ethernet interface. +// HandleEth can return 0 bytes written and a nil error to indicate no action must be taken. +// If +func (u *udpSocket) HandleEth(dst []byte) (int, error) { + if u.handler == nil { + panic("nil udp handler on port " + strconv.Itoa(int(u.Port))) + } + packet := &u.packets[0] + + n, err := u.handler(dst, &u.packets[0]) + packet.Rx = time.Time{} // Invalidate packet. + return n, err +} + +// Open sets the UDP handler and opens the port. +func (u *udpSocket) Open(port uint16, h func([]byte, *UDPPacket) (int, error)) { + if port == 0 || h == nil { + panic("invalid port or nil handler" + strconv.Itoa(int(u.Port))) + } + u.handler = h + u.Port = port +} + +func (s *udpSocket) pending() (p int) { + for i := range s.packets { + if s.packets[i].HasPacket() { + p++ + } + } + return p +} + +func (u *udpSocket) Close() { + u.Port = 0 // Port 0 flags the port is inactive. + for i := range u.packets { + u.packets[i].Rx = time.Time{} // Invalidate packets. + } +} + +// UDP socket can be forced to respond even if no packet has been received +// by flagging the packet's Rx time with non-zero value. +var forcedTime = (time.Time{}).Add(1) + +func (u *udpSocket) forceResponse() (added bool) { + if !u.IsPendingHandling() { + added = true + u.packets[0].Rx = forcedTime + } + return added +} + +func (u *UDPPacket) HasPacket() bool { + return u.Rx != forcedTime && !u.Rx.IsZero() // TODO simplify this to just IsZero +} + +func (p *UDPPacket) PutHeaders(b []byte) { + if len(b) < eth.SizeEthernetHeader+eth.SizeIPv4Header+eth.SizeUDPHeader { + panic("short UDPPacket buffer") + } + p.Eth.Put(b) + p.IP.Put(b[eth.SizeEthernetHeader:]) + p.UDP.Put(b[eth.SizeEthernetHeader+eth.SizeIPv4Header:]) +} + +// Payload returns the UDP payload. If UDP or IPv4 header data is incorrect/bad it returns nil. +// If the response is "forced" then payload will be nil. +func (p *UDPPacket) Payload() []byte { + if !p.HasPacket() { + return nil + } + ipLen := int(p.IP.TotalLength) - int(p.IP.IHL()*4) - eth.SizeUDPHeader // Total length(including header) - header length = payload length + uLen := int(p.UDP.Length) - eth.SizeUDPHeader + if ipLen != uLen || uLen > len(p.payload) { + return nil // Mismatching IP and UDP data or bad length. + } + return p.payload[:uLen] +} diff --git a/internal/tcpctl/stack.go b/internal/tcpctl/stack.go index 26ab24d..995e09a 100644 --- a/internal/tcpctl/stack.go +++ b/internal/tcpctl/stack.go @@ -6,7 +6,6 @@ import ( "fmt" "io" "net" - "strconv" "time" "github.com/soypat/cyw43439/internal/slog" @@ -17,116 +16,6 @@ const ( _MTU = 1500 ) -type socketEth interface { - Close() - IsPendingHandling() bool - // HandleEth searches for a socket with a pending packet and writes the response - // into the dst argument. The length written to dst is returned. - // [io.ErrNoProgress] can be returned by a handler to indicate the packet was - // not processed and that a future call to HandleEth is required to complete. - // - // If a handler returns any other error the port is closed. - HandleEth(dst []byte) (int, error) -} - -// NeedsHandling returns true if the socket needs handling before it can -// admit more pending packets. -func (u *udpSocket) NeedsHandling() bool { - // As of now socket has space for 1 packet so if packet is pending, queue is full. - // Compile time check to ensure this is fulfilled: - _ = u.packets[1-len(u.packets)] - return u.IsPendingHandling() -} - -// IsPendingHandling returns true if there are packet(s) pending handling. -func (u *udpSocket) IsPendingHandling() bool { - return u.Port != 0 && !u.packets[0].Rx.IsZero() -} - -// HandleEth writes the socket's response into dst to be sent over an ethernet interface. -// HandleEth can return 0 bytes written and a nil error to indicate no action must be taken. -// If -func (u *udpSocket) HandleEth(dst []byte) (int, error) { - if u.handler == nil { - panic("nil udp handler on port " + strconv.Itoa(int(u.Port))) - } - packet := &u.packets[0] - - n, err := u.handler(dst, &u.packets[0]) - packet.Rx = time.Time{} // Invalidate packet. - return n, err -} - -// Open sets the UDP handler and opens the port. -func (u *udpSocket) Open(port uint16, h func([]byte, *UDPPacket) (int, error)) { - if port == 0 || h == nil { - panic("invalid port or nil handler" + strconv.Itoa(int(u.Port))) - } - u.handler = h - u.Port = port -} - -func (u *udpSocket) Close() { - u.Port = 0 // Port 0 flags the port is inactive. - for i := range u.packets { - u.packets[i].Rx = time.Time{} // Invalidate packets. - } -} - -// UDP socket can be forced to respond even if no packet has been received -// by flagging the packet's Rx time with non-zero value. -var forcedTime = (time.Time{}).Add(1) - -func (u *udpSocket) forceResponse() (added bool) { - if !u.IsPendingHandling() { - added = true - u.packets[0].Rx = forcedTime - } - return added -} - -func (u *UDPPacket) HasPacket() bool { - return u.Rx != forcedTime && !u.Rx.IsZero() -} - -type udpSocket struct { - LastRx time.Time - handler func(response []byte, self *UDPPacket) (int, error) - Port uint16 - packets [1]UDPPacket -} - -type UDPPacket struct { - Rx time.Time - Eth eth.EthernetHeader - IP eth.IPv4Header - UDP eth.UDPHeader - payload [_MTU - eth.SizeEthernetHeader - eth.SizeIPv4Header - eth.SizeUDPHeader]byte -} - -func (p *UDPPacket) PutHeaders(b []byte) { - if len(b) < eth.SizeEthernetHeader+eth.SizeIPv4Header+eth.SizeUDPHeader { - panic("short UDPPacket buffer") - } - p.Eth.Put(b) - p.IP.Put(b[eth.SizeEthernetHeader:]) - p.UDP.Put(b[eth.SizeEthernetHeader+eth.SizeIPv4Header:]) -} - -// Payload returns the UDP payload. If UDP or IPv4 header data is incorrect/bad it returns nil. -// If the response is "forced" then payload will be nil. -func (p *UDPPacket) Payload() []byte { - if !p.HasPacket() { - return nil - } - ipLen := int(p.IP.TotalLength) - int(p.IP.IHL()*4) - eth.SizeUDPHeader // Total length(including header) - header length = payload length - uLen := int(p.UDP.Length) - eth.SizeUDPHeader - if ipLen != uLen || uLen > len(p.payload) { - return nil // Mismatching IP and UDP data or bad length. - } - return p.payload[:uLen] -} - type StackConfig struct { MAC net.HardwareAddr IP net.IP @@ -151,6 +40,7 @@ type Stack struct { // Set IP to non-nil to ignore packets not meant for us. IP net.IP UDPv4 []udpSocket + TCPv4 []tcpSocket GlobalHandler func([]byte) pendingUDPv4 uint32 pendingTCPv4 uint32 @@ -158,71 +48,21 @@ type Stack struct { processedPackets uint32 } -// OpenUDP opens a UDP port and sets the handler. If the port is already open -// or if there is no socket available it returns an error. -func (s *Stack) OpenUDP(port uint16, handler func([]byte, *UDPPacket) (int, error)) error { - if port == 0 { - panic("invalid port number") - } - availIdx := -1 - for i := range s.UDPv4 { - socket := &s.UDPv4[i] - if socket.Port == port { - availIdx = -1 - break - } else if availIdx == -1 && socket.Port == 0 { - availIdx = i - } - } - if availIdx == -1 { - return errNoSocketAvail - } - s.UDPv4[availIdx].Open(port, handler) - return nil -} - -func (s *Stack) FlagUDPPending(port uint16) error { - if port == 0 { - panic("invalid port number") - } - socket := s.getUDP(port) - if socket == nil { - return errNoSocketAvail - } - if socket.forceResponse() { - s.pendingUDPv4++ - } - return nil -} - -func (s *Stack) CloseUDP(port uint16) error { - if port == 0 { - panic("invalid port number") - } - socket := s.getUDP(port) - if socket == nil { - return errNoSocketAvail - } - socket.Close() - return nil -} - -func (s *Stack) getUDP(port uint16) *udpSocket { - for i := range s.UDPv4 { - socket := &s.UDPv4[i] - if socket.Port == port { - return socket - } - } - return nil -} - // Common errors. var ( - ErrDroppedPacket = errors.New("dropped packet") - errNotIPv4 = errors.New("require IPv4") - errPacketSmol = errors.New("packet too small") - errNoSocketAvail = errors.New("no available socket") + ErrDroppedPacket = errors.New("dropped packet") + errPacketExceedsMTU = errors.New("packet exceeds MTU") + errNotIPv4 = errors.New("require IPv4") + errPacketSmol = errors.New("packet too small") + errNoSocketAvail = errors.New("no available socket") + errTooShortTCPOrUDP = errors.New("packet too short to be TCP/UDP") + errZeroPort = errors.New("zero port in TCP/UDP") + errBadTCPOffset = errors.New("invalid TCP offset") + errNilHandler = errors.New("nil handler") + errChecksumTCPorUDP = errors.New("invalid TCP/UDP checksum") + errBadUDPLength = errors.New("invalid UDP length") + errInvalidIHL = errors.New("invalid IP IHL") + errIPVersion = errors.New("IP version not supported") ) // RecvEth validates an ethernet+ipv4 frame in payload. If it is OK then it @@ -236,13 +76,15 @@ func (s *Stack) RecvEth(ethernetFrame []byte) (err error) { defer func() { if err != nil { s.error("Stack.RecvEth", slog.String("err", err.Error()), slog.Any("IP", ihdr)) + } else { + s.lastRxSuccess = s.lastRx } }() payload := ethernetFrame if len(payload) < eth.SizeEthernetHeader+eth.SizeIPv4Header { return errPacketSmol } - s.info("Stack.RecvEth:start", slog.Int("plen", len(payload))) + s.debug("Stack.RecvEth:start", slog.Int("plen", len(payload))) s.lastRx = time.Now() // Ethernet parsing block @@ -256,78 +98,109 @@ func (s *Stack) RecvEth(ethernetFrame []byte) (err error) { // IP parsing block. ihdr = eth.DecodeIPv4Header(payload[eth.SizeEthernetHeader:]) ihl := ihdr.IHL() - if ihdr.Version() != 4 { - return errors.New("IP version not supported") - } else if ihl < 5 { - return errors.New("bad IHL") - } else if s.IP != nil && string(ihdr.Destination[:]) != string(s.IP) { - return nil // Not for us. - } - - // Handle UDP/TCP packets. offset := eth.SizeEthernetHeader + 4*ihl // Can be at most 14+60=74, so no overflow risk. end := eth.SizeEthernetHeader + ihdr.TotalLength - if len(payload) < int(end) || end < uint16(offset) { - return errors.New("short payload buffer or bad IP TotalLength") - } else if end > _MTU { - return errors.New("packet size exceeds MTU") + switch { + case ihdr.Version() != 4: + return errIPVersion + case ihl < 5: + return errInvalidIHL + case s.IP != nil && string(ihdr.Destination[:]) != string(s.IP): + return nil // Not for us. + case uint16(offset) > end || int(offset) > len(payload): + return errors.New("bad IP TotalLength/IHL") + case end > _MTU: + return errPacketExceedsMTU } - s.lastRxSuccess = s.lastRx payload = payload[offset:end] switch ihdr.Protocol { case 17: // UDP (User Datagram Protocol). if len(s.UDPv4) == 0 { - println("no sockets") return nil // No sockets. + } else if len(payload) < eth.SizeUDPHeader { + return errTooShortTCPOrUDP } uhdr := eth.DecodeUDPHeader(payload) - if uhdr.DestinationPort == 0 || uhdr.SourcePort == 0 { - // Ignore port 0. Is invalid and we use it to flag a closed/inactive port. - s.debug("UDP packet with 0 port", slog.Any("hdr", uhdr)) - return nil - } else if uhdr.Length < 8 { - return errors.New("bad UDP length field") + switch { + case uhdr.DestinationPort == 0 || uhdr.SourcePort == 0: + return errZeroPort + case uhdr.Length < 8: + return errBadUDPLength } + payload = payload[eth.SizeUDPHeader:] gotsum := uhdr.CalculateChecksumIPv4(&ihdr, payload) if gotsum != uhdr.Checksum { - return errors.New("UDP checksum mismatch") + return errChecksumTCPorUDP } - for i := range s.UDPv4 { - socket := &s.UDPv4[i] - if socket.Port != uhdr.DestinationPort { - continue - } - // The packet is meant for us. We handle it. - if socket.NeedsHandling() { - s.error("UDP packet dropped") - s.droppedPackets++ - return ErrDroppedPacket // Our socket needs handling before admitting more packets. - } - s.info("UDP packet stored", slog.Int("plen", len(payload))) - // Flag packets as needing processing. - s.pendingUDPv4++ - socket.LastRx = s.lastRxSuccess // set as unhandled here. - - socket.packets[0].Rx = s.lastRxSuccess - socket.packets[0].Eth = ehdr - socket.packets[0].IP = ihdr - socket.packets[0].UDP = uhdr - - copy(socket.packets[0].payload[:], payload) - break // Packet succesfully processed, missing + socket := s.getUDP(uhdr.DestinationPort) + if socket == nil { + break // No socket listening on this port. + } else if socket.NeedsHandling() { + s.error("UDP packet dropped") + s.droppedPackets++ + return ErrDroppedPacket // Our socket needs handling before admitting more packets. } + // The packet is meant for us. We handle it. + s.info("UDP packet stored", slog.Int("plen", len(payload))) + // Flag packets as needing processing. + s.pendingUDPv4++ + socket.LastRx = s.lastRx // set as unhandled here. + + socket.packets[0].Rx = s.lastRx + socket.packets[0].Eth = ehdr + socket.packets[0].IP = ihdr + socket.packets[0].UDP = uhdr + + copy(socket.packets[0].payload[:], payload) + case 6: s.info("TCP packet received", slog.Int("plen", len(payload))) // TCP (Transport Control Protocol). - // TODO - } - s.debug("Stack.RecvEth:success") - if s.GlobalHandler != nil { - s.GlobalHandler(ethernetFrame) + switch { + case len(s.TCPv4) == 0: + return nil + case len(payload) < eth.SizeTCPHeader: + return errTooShortTCPOrUDP + } + + thdr := eth.DecodeTCPHeader(payload) + offset := thdr.Offset() + switch { + case thdr.DestinationPort == 0 || thdr.SourcePort == 0: + return errZeroPort + case offset < 5 || int(offset*4) > len(payload): + return errBadTCPOffset + } + options := payload[:offset*4] + payload = payload[offset*4:] + gotsum := thdr.CalculateChecksumIPv4(&ihdr, options, payload) + if gotsum != thdr.Checksum { + return errChecksumTCPorUDP + } + + socket := s.getTCP(thdr.DestinationPort) + if socket == nil { + break // No socket listening on this port. + } else if socket.NeedsHandling() { + s.error("TCP packet dropped") + s.droppedPackets++ + return ErrDroppedPacket // Our socket needs handling before admitting more packets. + } + s.info("TCP packet stored", slog.Int("plen", len(payload))) + // Flag packets as needing processing. + s.pendingTCPv4++ + socket.LastRx = s.lastRx // set as unhandled here. + + socket.packets[0].Rx = s.lastRx + socket.packets[0].Eth = ehdr + socket.packets[0].IP = ihdr + socket.packets[0].TCP = thdr + + copy(socket.packets[0].payload[:], payload) // TODO: add options to payload. } return nil } @@ -339,12 +212,13 @@ func (s *Stack) RecvEth(ethernetFrame []byte) (err error) { // // If a handler returns any other error the port is closed. func (s *Stack) HandleEth(dst []byte) (n int, err error) { - if len(dst) < _MTU { + switch { + case len(dst) < _MTU: return 0, io.ErrShortBuffer - } - if s.pendingUDPv4 == 0 && s.pendingTCPv4 == 0 { + case s.pendingUDPv4 == 0 && s.pendingTCPv4 == 0: return 0, nil // No packets to handle } + s.info("HandleEth", slog.Int("dstlen", len(dst))) if s.pendingUDPv4 > 0 { for i := range s.UDPv4 { @@ -370,8 +244,31 @@ func (s *Stack) HandleEth(dst []byte) (n int, err error) { break // If we got here our packet has been processed. } } + if n == 0 && s.pendingTCPv4 > 0 { - // TODO + socketList := s.TCPv4 + for i := range socketList { + socket := &socketList[i] + if !socket.IsPendingHandling() { + return 0, nil + } + // Socket has an unhandled packet. + n, err = socket.HandleEth(dst) + if err == io.ErrNoProgress { + n = 0 + err = nil + continue + } + s.pendingTCPv4-- + if err != nil { + socket.Close() + return 0, err + } + if n == 0 { + continue + } + break // If we got here our packet has been processed. + } } if n != 0 && err == nil { @@ -380,6 +277,141 @@ func (s *Stack) HandleEth(dst []byte) (n int, err error) { return n, err } +// OpenUDP opens a UDP port and sets the handler. If the port is already open +// or if there is no socket available it returns an error. +func (s *Stack) OpenUDP(port uint16, handler func([]byte, *UDPPacket) (int, error)) error { + switch { + case port == 0: + return errZeroPort + case handler == nil: + return errNilHandler + } + availIdx := -1 + socketList := s.UDPv4 + for i := range socketList { + socket := &socketList[i] + if socket.Port == port { + availIdx = -1 + break + } else if availIdx == -1 && socket.Port == 0 { + availIdx = i + } + } + if availIdx == -1 { + return errNoSocketAvail + } + socketList[availIdx].Open(port, handler) + return nil +} + +// FlagUDPPending flags the socket listening on a given port as having a pending +// packet. This is useful to force a response even if no packet has been received. +func (s *Stack) FlagUDPPending(port uint16) error { + if port == 0 { + return errZeroPort + } + socket := s.getUDP(port) + if socket == nil { + return errNoSocketAvail + } + if socket.forceResponse() { + s.pendingUDPv4++ + } + return nil +} + +// CloseUDP closes a UDP socket. +func (s *Stack) CloseUDP(port uint16) error { + if port == 0 { + return errZeroPort + } + socket := s.getUDP(port) + if socket == nil { + return errNoSocketAvail + } + s.pendingUDPv4 -= uint32(socket.pending()) + socket.Close() + return nil +} + +func (s *Stack) getUDP(port uint16) *udpSocket { + for i := range s.UDPv4 { + socket := &s.UDPv4[i] + if socket.Port == port { + return socket + } + } + return nil +} + +// OpenTCP opens a TCP port and sets the handler. If the port is already open +// or if there is no socket available it returns an error. +func (s *Stack) OpenTCP(port uint16, handler func([]byte, *TCPPacket) (int, error)) error { + switch { + case port == 0: + return errZeroPort + case handler == nil: + return errNilHandler + } + + availIdx := -1 + socketList := s.TCPv4 + for i := range socketList { + socket := &socketList[i] + if socket.Port == port { + availIdx = -1 + break + } else if availIdx == -1 && socket.Port == 0 { + availIdx = i + } + } + if availIdx == -1 { + return errNoSocketAvail + } + socketList[availIdx].Open(port, handler) + return nil +} + +// FlagTCPPending flags the socket listening on a given port as having a pending +// packet. This is useful to force a response even if no packet has been received. +func (s *Stack) FlagTCPPending(port uint16) error { + if port == 0 { + return errZeroPort + } + socket := s.getTCP(port) + if socket == nil { + return errNoSocketAvail + } + if socket.forceResponse() { + s.pendingTCPv4++ + } + return nil +} + +// CloseTCP closes a TCP socket. +func (s *Stack) CloseTCP(port uint16) error { + if port == 0 { + return errZeroPort + } + socket := s.getTCP(port) + if socket == nil { + return errNoSocketAvail + } + s.pendingTCPv4 -= socket.pending() + socket.Close() + return nil +} + +func (s *Stack) getTCP(port uint16) *tcpSocket { + for i := range s.UDPv4 { + socket := &s.TCPv4[i] + if socket.Port == port { + return socket + } + } + return nil +} + func (s *Stack) info(msg string, attrs ...slog.Attr) { logAttrsPrint(slog.LevelInfo, msg, attrs...) } diff --git a/internal/tcpctl/tcpctl.go b/internal/tcpctl/tcpctl.go index 41039bd..aa4696a 100644 --- a/internal/tcpctl/tcpctl.go +++ b/internal/tcpctl/tcpctl.go @@ -107,7 +107,7 @@ func (s *TCPSocket) RecvTCP(buf []byte) (payloadStart, payloadEnd uint16, err er if nb < 20 { return 0, 0, errors.New("garbage TCP.Offset") } - payloadStart = nb + eth.SizeIPv4Header + payloadStart = uint16(nb + eth.SizeIPv4Header) if payloadStart > buflen { return 0, 0, fmt.Errorf("malformed packet, got payload offset %d/%d", payloadStart, buflen) }