diff --git a/cmd/cyrw/main.go b/cmd/cyrw/main.go index 34789d9..6486766 100644 --- a/cmd/cyrw/main.go +++ b/cmd/cyrw/main.go @@ -1,14 +1,13 @@ package main import ( - "encoding/hex" "errors" + "strconv" "time" "github.com/soypat/cyw43439/cyrw" "github.com/soypat/cyw43439/internal/slog" - - "github.com/soypat/cyw43439/internal/tcpctl/eth" + "github.com/soypat/cyw43439/internal/tcpctl" ) var lastRx, lastTx time.Time @@ -33,8 +32,6 @@ func main() { panic(err) } - dev.RecvEthHandle(rcv) - for { // Set ssid/pass in secrets.go err = dev.JoinWPA2(ssid, pass) @@ -45,6 +42,24 @@ func main() { println("wifi join failed:", err.Error()) time.Sleep(5 * time.Second) } + mac := dev.MAC() + println("\n\n\nMAC:", mac.String()) + stack = tcpctl.NewStack(tcpctl.StackConfig{ + MAC: nil, + MaxUDPConns: 2, + }) + + dev.RecvEthHandle(stack.RecvEth) + for { + println("Trying DoDHCP") + err = DoDHCP(stack, dev) + if err == nil { + println("========\nDHCP done, your IP: ", stack.IP.String(), "\n========") + break + } + println(err.Error()) + time.Sleep(8 * time.Second) + } println("finished init OK") @@ -63,44 +78,42 @@ func main() { } var ( - errNotTCP = errors.New("packet not TCP") - errNotIPv4 = errors.New("packet not IPv4") - errPacketSmol = errors.New("packet too small") + stack *tcpctl.Stack + txbuf [1500]byte ) -func rcv(pkt []byte) error { - // Note: rcv is called from a locked Device context. - // No calls to device I/O should be performed here. - lastRx = time.Now() - if len(pkt) < 14 { - return errPacketSmol +func DoDHCP(s *tcpctl.Stack, dev *cyrw.Device) error { + var dc tcpctl.DHCPClient + copy(dc.MAC[:], dev.MAC()) + err := s.OpenUDP(68, dc.HandleUDP) + if err != nil { + return err } - ethHdr := eth.DecodeEthernetHeader(pkt) - if ethHdr.AssertType() != eth.EtherTypeIPv4 { - return errNotIPv4 + defer s.CloseUDP(68) + err = s.FlagUDPPending(68) // Force a DHCP discovery. + if err != nil { + return err } - ipHdr := eth.DecodeIPv4Header(pkt[eth.SizeEthernetHeaderNoVLAN:]) - println("ETH:", ethHdr.String()) - println("IPv4:", ipHdr.String()) - println("Rx:", len(pkt)) - println(hex.Dump(pkt)) - if ipHdr.Protocol == 17 { - // We got an UDP packet and we validate it. - udpHdr := eth.DecodeUDPHeader(pkt[eth.SizeEthernetHeaderNoVLAN+eth.SizeIPv4Header:]) - gotChecksum := udpHdr.CalculateChecksumIPv4(&ipHdr, pkt[eth.SizeEthernetHeaderNoVLAN+eth.SizeIPv4Header+eth.SizeUDPHeader:]) - println("UDP:", udpHdr.String()) - if gotChecksum == 0 || gotChecksum == udpHdr.Checksum { - println("checksum match!") - } else { - println("checksum mismatch! Received ", udpHdr.Checksum, " but calculated ", gotChecksum) + for retry := 0; retry < 20 && dc.State < 3; retry++ { + n, err := stack.HandleEth(txbuf[:]) + if err != nil { + return err + } + if n == 0 { + time.Sleep(200 * time.Millisecond) + continue + } + err = dev.SendEth(txbuf[:n]) + if err != nil { + return err } - return nil } - if ipHdr.Protocol != 6 { - return errNotTCP + if dc.State != 3 { // TODO: find way to make this value more self descriptive. + return errors.New("DHCP did not complete, state=" + strconv.Itoa(int(dc.State))) } - tcpHdr := eth.DecodeTCPHeader(pkt[eth.SizeEthernetHeaderNoVLAN+eth.SizeIPv4Header:]) - println("TCP:", tcpHdr.String()) - + if len(s.IP) == 0 { + s.IP = make([]byte, 4) + } + copy(s.IP, dc.YourIP[:]) return nil } diff --git a/cmd/cyweth/cyweth.go b/cmd/cyweth/cyweth.go index d04a7a6..904ad5b 100644 --- a/cmd/cyweth/cyweth.go +++ b/cmd/cyweth/cyweth.go @@ -21,15 +21,15 @@ func rx(pkt []byte) error { if ethHdr.AssertType() != eth.EtherTypeIPv4 { return errNotIPv4 } - ipHdr := eth.DecodeIPv4Header(pkt[eth.SizeEthernetHeaderNoVLAN:]) + ipHdr := eth.DecodeIPv4Header(pkt[eth.SizeEthernetHeader:]) println("ETH:", ethHdr.String()) println("IPv4:", ipHdr.String()) println("Rx:", len(pkt)) println(hex.Dump(pkt)) if ipHdr.Protocol == 17 { // We got an UDP packet and we validate it. - udpHdr := eth.DecodeUDPHeader(pkt[eth.SizeEthernetHeaderNoVLAN+eth.SizeIPv4Header:]) - gotChecksum := udpHdr.CalculateChecksumIPv4(&ipHdr, pkt[eth.SizeEthernetHeaderNoVLAN+eth.SizeIPv4Header+eth.SizeUDPHeader:]) + udpHdr := eth.DecodeUDPHeader(pkt[eth.SizeEthernetHeader+eth.SizeIPv4Header:]) + gotChecksum := udpHdr.CalculateChecksumIPv4(&ipHdr, pkt[eth.SizeEthernetHeader+eth.SizeIPv4Header+eth.SizeUDPHeader:]) println("UDP:", udpHdr.String()) if gotChecksum == 0 || gotChecksum == udpHdr.Checksum { println("checksum match!") @@ -41,7 +41,7 @@ func rx(pkt []byte) error { if ipHdr.Protocol != 6 { return errNotTCP } - tcpHdr := eth.DecodeTCPHeader(pkt[eth.SizeEthernetHeaderNoVLAN+eth.SizeIPv4Header:]) + tcpHdr := eth.DecodeTCPHeader(pkt[eth.SizeEthernetHeader+eth.SizeIPv4Header:]) println("TCP:", tcpHdr.String()) return nil diff --git a/cyrw/ioctl.go b/cyrw/ioctl.go index f16d309..d4d7212 100644 --- a/cyrw/ioctl.go +++ b/cyrw/ioctl.go @@ -225,7 +225,6 @@ func (d *Device) sendIoctl(kind uint8, cmd whd.SDPCMCommand, iface whd.IoctlInte buf := d._sendIoctlBuf[:] buf8 := u32AsU8(buf) - totalLen := uint32(whd.SDPCM_HEADER_LEN + whd.CDC_HEADER_LEN + len(data)) if int(totalLen) > len(buf8) { return errors.New("ioctl data too large " + strconv.Itoa(len(data))) @@ -442,8 +441,14 @@ func (d *Device) rxControl(packet []byte) (offset, plen uint16, err error) { return offset, plen, nil } +var errPacketSmol = errors.New("asyncEvent packet too small for parsing") + func (d *Device) rxEvent(packet []byte) error { // Split packet into BDC header:payload. + if len(packet) < whd.BDC_HEADER_LEN+72 { + d.logerr("rxEvent", slog.Int("plen", len(packet)), slog.String("err", errPacketSmol.Error())) + return errPacketSmol + } bdcHdr := whd.DecodeBDCHeader(packet) packetStart := whd.BDC_HEADER_LEN + 4*int(bdcHdr.DataOffset) if packetStart > len(packet) { diff --git a/internal/tcpctl/dhcp.go b/internal/tcpctl/dhcp.go new file mode 100644 index 0000000..09179aa --- /dev/null +++ b/internal/tcpctl/dhcp.go @@ -0,0 +1,181 @@ +package tcpctl + +import ( + "encoding/binary" + "errors" + "fmt" + "net" + + "github.com/soypat/cyw43439/internal/tcpctl/eth" +) + +type DHCPClient struct { + ourHeader eth.DHCPHeader + State uint8 + MAC [6]byte + // The result IP of the DHCP transaction (our new IP). + YourIP [4]byte + // DHCP server IP + ServerIP [4]byte +} + +const ( + dhcpStateNone = iota + dhcpStateWaitOffer + dhcpStateWaitAck + dhcpStateDone +) + +func (d *DHCPClient) HandleUDP(resp []byte, packet *UDPPacket) (_ int, err error) { + const ( + xid = 0x12345678 + + sizeSName = 64 // Server name, part of BOOTP too. + sizeFILE = 128 // Boot file name, Legacy. + sizeOptions = 312 + dhcpOffset = eth.SizeEthernetHeader + eth.SizeIPv4Header + eth.SizeUDPHeader + optionsStart = dhcpOffset + eth.SizeDHCPHeader + sizeSName + sizeFILE + sizeDHCPTotal = eth.SizeDHCPHeader + sizeSName + sizeFILE + sizeOptions + ) + // First action is used to send data without having received a packet + // so hasPacket will be false. + hasPacket := packet.HasPacket() + incpayload := packet.Payload() + switch { + case len(resp) < sizeDHCPTotal: + return 0, errors.New("short payload to marshall DHCP") + case hasPacket && len(incpayload) < eth.SizeDHCPHeader: + return 0, errors.New("short payload to parse DHCP") + } + + var rcvHdr eth.DHCPHeader + if hasPacket { + rcvHdr = eth.DecodeDHCPHeader(incpayload) + ptr := eth.SizeDHCPHeader + sizeSName + sizeFILE + 4 + for ptr+1 < len(incpayload) && int(incpayload[ptr+1]) < len(incpayload) { + if incpayload[ptr] == 0xff { + break + } + option := eth.DHCPOption(incpayload[ptr]) + optlen := incpayload[ptr+1] + // optionData := incpayload[ptr+2 : ptr+2+int(optlen)] + + // print("DHCP Option received ", option.String()) + optionData := incpayload[ptr+2 : ptr+2+int(optlen)] + if d.State == dhcpStateWaitAck && option == eth.DHCP_MessageType && len(optionData) > 0 && optionData[0] == 5 { + d.State = dhcpStateDone + return 0, nil + } + println() + ptr += int(optlen) + 2 + } + } + + // Switch statement prepares DHCP response depending on whether we're waiting + // for offer, ack or if we still need to send a discover (StateNone). + type option struct { + code byte + data []byte + } + var Options []option + switch { + case !hasPacket && d.State == dhcpStateNone: + d.initOurHeader(xid) + // DHCP options. + Options = []option{ + {53, []byte{1}}, // DHCP Message Type: Discover + {50, []byte{192, 168, 1, 69}}, // Requested IP + {55, []byte{1, 3, 15, 6}}, // Parameter request list + } + d.State = dhcpStateWaitOffer + + case hasPacket && d.State == dhcpStateWaitOffer: + offer := net.IP(rcvHdr.YIAddr[:]) + Options = []option{ + {53, []byte{3}}, // DHCP Message Type: Request + {50, offer}, // Requested IP + {54, rcvHdr.SIAddr[:]}, // DHCP server IP + } + // Accept this server's offer. + copy(d.ourHeader.SIAddr[:], rcvHdr.SIAddr[:]) + copy(d.YourIP[:], offer) // Store our new IP. + d.State = dhcpStateWaitAck + default: + err = fmt.Errorf("UNHANDLED CASE %v %+v", hasPacket, d) + } + if err != nil { + return 0, nil + } + for i := dhcpOffset + 14; i < len(resp); i++ { + resp[i] = 0 // Zero out BOOTP and options fields. + } + // Encode DHCP header + options. + d.ourHeader.Put(resp[dhcpOffset:]) + + ptr := optionsStart + binary.BigEndian.PutUint32(resp[ptr:], 0x63825363) // Magic cookie. + ptr += 4 + for _, opt := range Options { + ptr += encodeDHCPOption(resp[ptr:], opt.code, opt.data) + } + resp[ptr] = 0xff // endmark + // Set Ethernet+IP+UDP headers. + payload := resp[dhcpOffset : dhcpOffset+sizeDHCPTotal] + d.setResponseUDP(packet, payload) + packet.PutHeaders(resp) + return dhcpOffset + sizeDHCPTotal, nil +} + +// initOurHeader zero's out most of header and sets the xid and MAC address along with OP=1. +func (d *DHCPClient) initOurHeader(xid uint32) { + dhdr := &d.ourHeader + dhdr.OP = 1 + dhdr.HType = 1 + dhdr.HLen = 6 + dhdr.HOps = 0 + dhdr.Secs = 0 + dhdr.Flags = 0 + dhdr.Xid = xid + dhdr.CIAddr = [4]byte{} + dhdr.YIAddr = [4]byte{} + dhdr.SIAddr = [4]byte{} + dhdr.GIAddr = [4]byte{} + copy(dhdr.CHAddr[:], d.MAC[:]) +} + +func (d *DHCPClient) setResponseUDP(packet *UDPPacket, payload []byte) { + const ipWordLen = 5 + // Ethernet frame. + copy(packet.Eth.Destination[:], eth.BroadcastHW()) + copy(packet.Eth.Source[:], d.MAC[:]) + packet.Eth.SizeOrEtherType = uint16(eth.EtherTypeIPv4) + + // IPv4 frame. + copy(packet.IP.Destination[:], eth.BroadcastHW()) + packet.IP.Source = [4]byte{} // Source IP is always zeroed when client sends. + packet.IP.Protocol = 17 // UDP + packet.IP.TTL = 64 + // 16bit Xorshift for prandom IP packet ID. https://en.wikipedia.org/wiki/Xorshift + packet.IP.ID ^= packet.IP.ID << 7 + packet.IP.ID ^= packet.IP.ID >> 9 + packet.IP.ID ^= packet.IP.ID << 8 + packet.IP.VersionAndIHL = ipWordLen // Sets IHL: No IP options. Version set automatically. + packet.IP.TotalLength = 4*ipWordLen + eth.SizeUDPHeader + uint16(len(payload)) + packet.IP.Checksum = packet.IP.CalculateChecksum() + + // UDP frame. + packet.UDP.DestinationPort = 67 + packet.UDP.SourcePort = 68 + packet.UDP.Length = packet.IP.TotalLength - 4*ipWordLen + packet.UDP.Checksum = packet.UDP.CalculateChecksumIPv4(&packet.IP, payload) +} + +func encodeDHCPOption(dst []byte, code byte, data []byte) int { + if len(data)+2 > len(dst) { + panic("small dst size for DHCP encoding") + } + dst[0] = code + dst[1] = byte(len(data)) + copy(dst[2:], data) + return 2 + len(data) +} diff --git a/internal/tcpctl/eth/definitions.go b/internal/tcpctl/eth/definitions.go new file mode 100644 index 0000000..06fba44 --- /dev/null +++ b/internal/tcpctl/eth/definitions.go @@ -0,0 +1,121 @@ +package eth + +// Ethertype values. From: http://en.wikipedia.org/wiki/Ethertype +// +//go:generate stringer -type=EtherType -trimprefix=EtherType +const ( + EtherTypeIPv4 EtherType = 0x0800 + EtherTypeARP EtherType = 0x0806 + EtherTypeWakeOnLAN EtherType = 0x0842 + EtherTypeTRILL EtherType = 0x22F3 + EtherTypeDECnetPhase4 EtherType = 0x6003 + EtherTypeRARP EtherType = 0x8035 + EtherTypeAppleTalk EtherType = 0x809B + EtherTypeAARP EtherType = 0x80F3 + EtherTypeIPX1 EtherType = 0x8137 + EtherTypeIPX2 EtherType = 0x8138 + EtherTypeQNXQnet EtherType = 0x8204 + EtherTypeIPv6 EtherType = 0x86DD + EtherTypeEthernetFlowControl EtherType = 0x8808 + EtherTypeIEEE802_3 EtherType = 0x8809 + EtherTypeCobraNet EtherType = 0x8819 + EtherTypeMPLSUnicast EtherType = 0x8847 + EtherTypeMPLSMulticast EtherType = 0x8848 + EtherTypePPPoEDiscovery EtherType = 0x8863 + EtherTypePPPoESession EtherType = 0x8864 + EtherTypeJumboFrames EtherType = 0x8870 + EtherTypeHomePlug1_0MME EtherType = 0x887B + EtherTypeIEEE802_1X EtherType = 0x888E + EtherTypePROFINET EtherType = 0x8892 + EtherTypeHyperSCSI EtherType = 0x889A + EtherTypeAoE EtherType = 0x88A2 + EtherTypeEtherCAT EtherType = 0x88A4 + EtherTypeEthernetPowerlink EtherType = 0x88AB + EtherTypeLLDP EtherType = 0x88CC + EtherTypeSERCOS3 EtherType = 0x88CD + EtherTypeHomePlugAVMME EtherType = 0x88E1 + EtherTypeMRP EtherType = 0x88E3 + EtherTypeIEEE802_1AE EtherType = 0x88E5 + EtherTypeIEEE1588 EtherType = 0x88F7 + EtherTypeIEEE802_1ag EtherType = 0x8902 + EtherTypeFCoE EtherType = 0x8906 + EtherTypeFCoEInit EtherType = 0x8914 + EtherTypeRoCE EtherType = 0x8915 + EtherTypeCTP EtherType = 0x9000 + EtherTypeVeritasLLT EtherType = 0xCAFE + EtherTypeVLAN EtherType = 0x8100 + EtherTypeServiceVLAN EtherType = 0x88a8 + // minEthPayload is the minimum payload size for an Ethernet frame, assuming + // that no 802.1Q VLAN tags are present. + minEthPayload = 46 +) + +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_ +const ( + 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 new file mode 100644 index 0000000..f6af653 --- /dev/null +++ b/internal/tcpctl/eth/dhcpoption_string.go @@ -0,0 +1,84 @@ +// Code generated by "stringer -type=DHCPOption -trimprefix=DHCP_"; DO NOT EDIT. + +package eth + +import "strconv" + +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[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" + +var _DHCPOption_index = [...]uint16{0, 11, 21, 31, 37, 48, 59, 69, 79, 92, 102, 116, 126, 134, 146, 159, 169, 179, 187, 200, 217, 232, 244, 267, 279, 298, 308, 324, 342, 358, 378, 397, 419, 444, 462, 482, 497, 518, 538, 558, 577, 590, 608, 627, 652, 669, 696, 711, 723, 740, 761, 779, 797, 811, 822, 842, 862, 869, 887, 901, 919, 935, 952} + +func (i DHCPOption) String() string { + if i >= DHCPOption(len(_DHCPOption_index)-1) { + return "DHCPOption(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _DHCPOption_name[_DHCPOption_index[i]:_DHCPOption_index[i+1]] +} diff --git a/internal/tcpctl/eth/headers.go b/internal/tcpctl/eth/headers.go index 25f2fe3..93e7d32 100644 --- a/internal/tcpctl/eth/headers.go +++ b/internal/tcpctl/eth/headers.go @@ -77,15 +77,31 @@ type ARPv4Header struct { // IPv4Header is the Internet Protocol header. 20 bytes in size. Does not include options. type IPv4Header struct { - Version uint8 // 0:1 + // VersionAndIHL contains union of both IP Version and IHL data. + // + // Version must be 4 for IPv4. It is force-set to its valid value in a call to Put. + // // Internet Header Length (IHL) The IPv4 header is variable in size due to the // optional 14th field (options). The IHL field contains the size of the IPv4 header; // it has 4 bits that specify the number of 32-bit words in the header. - // // The minimum value for this field is 5, which indicates a length of // 5 × 32 bits = 160 bits = 20 bytes. As a 4-bit field, the maximum value is 15; // this means that the maximum size of the IPv4 header is 15 × 32 bits = 480 bits = 60 bytes. - IHL uint8 // 1:2 + VersionAndIHL uint8 // 0:1 (first 4 bits are version, last 4 bits are IHL) + + // Type of Service contains Differential Services Code Point (DSCP) and + // Explicit Congestion Notification (ECN) union data. + // + // DSCP originally defined as the type of service (ToS), this field specifies + // differentiated services (DiffServ) per RFC 2474. Real-time data streaming + // makes use of the DSCP field. An example is Voice over IP (VoIP), which is + // used for interactive voice services. + // + // ECN is defined in RFC 3168 and allows end-to-end notification of + // network congestion without dropping packets. ECN is an optional feature available + // when both endpoints support it and effective when also supported by the underlying network. + ToS uint8 // 1:2 (first 6 bits are DSCP, last 2 bits are ECN) + // This 16-bit field defines the entire packet size in bytes, including header and data. // The minimum size is 20 bytes (header without data) and the maximum is 65,535 bytes. // All hosts are required to be able to reassemble datagrams of size up to 576 bytes, @@ -95,19 +111,24 @@ type IPv4Header struct { // must be fragmented. Fragmentation in IPv4 is performed in either the // sending host or in routers. Reassembly is performed at the receiving host. TotalLength uint16 // 2:4 + // This field is an identification field and is primarily used for uniquely // identifying the group of fragments of a single IP datagram. ID uint16 // 4:6 + // A three-bit field follows and is used to control or identify fragments. // - If the DF flag is set (bit 1), and fragmentation is required to route the packet, then the packet is dropped. // - For fragmented packets, all fragments except the last have the MF flag set (bit 2). // - Bit 0 is reserved and must be set to zero. Flags IPFlags // 6:8 + // An eight-bit time to live field limits a datagram's lifetime to prevent - // network failure in the event of a routing loop. When the datagram arrives - // at a router, the router decrements the TTL field by one. It is specified - // in seconds, but time intervals less than 1 second are rounded up to 1. + // network failure in the event of a routing loop. In practice, the field + // is used as a hop count—when the datagram arrives at a router, + // the router decrements the TTL field by one. When the TTL field hits zero, + // the router discards the packet and typically sends an ICMP time exceeded message to the sender. TTL uint8 // 8:9 + // This field defines the protocol used in the data portion of the IP datagram. TCP is 6, UDP is 17. Protocol uint8 // 9:10 Checksum uint16 // 10:12 @@ -131,8 +152,37 @@ type TCPHeader struct { type UDPHeader struct { SourcePort uint16 // 0:2 DestinationPort uint16 // 2:4 - Length uint16 // 4:6 - Checksum uint16 // 6:8 + // Length specifies length in bytes of UDP header and UDP payload. The minimum length + // is 8 bytes (UDP header length). This field should match the result of the IP header + // TotalLength field minus the IP header size: udp.Length == ip.TotalLength - 4*ip.IHL + Length uint16 // 4:6 + Checksum uint16 // 6:8 +} + +// DHCPHeader specifies the first 44 bytes of a DHCP packet payload. It does +// not include BOOTP, magic cookie and options. +// Reference: https://lists.gnu.org/archive/html/lwip-users/2012-12/msg00016.html +type DHCPHeader struct { + OP byte // 0:1 + HType byte // 1:2 + HLen byte // 2:3 + HOps byte // 3:4 + Xid uint32 // 4:8 + Secs uint16 // 8:10 + Flags uint16 // 10:12 + // CIAddr is the client IP address. If the client has not obtained an IP + // address yet, this field is set to 0. + CIAddr [4]byte // 12:16 + YIAddr [4]byte // 16:20 + SIAddr [4]byte // 20:24 + GIAddr [4]byte // 24:28 + // CHAddr is the client hardware address. Can be up to 16 bytes in length but + // is usually 6 bytes for Ethernet. + CHAddr [16]byte // 28:44 + // BOOTP, Magic Cookie, and DHCP Options not included. + // LegacyBOOTP [192]byte + // Magic [4]byte // 0x63,0x82,0x53,0x63 + // Options [275...]byte // as of RFC2131 it is variable length } // There are 9 flags, bits 100 thru 103 are reserved @@ -154,77 +204,40 @@ const ( FlagTCP_NS ) +// These are minimum sizes that do not take into consideration the presence of +// options or special tags (i.e: VLAN, IP/TCP Options). const ( - SizeEthernetHeaderNoVLAN = 14 - SizeIPv4Header = 20 - SizeUDPHeader = 8 - SizeARPv4Header = 28 - SizeTCPHeaderNoOptions = 20 - ipflagDontFrag = 0x4000 - ipFlagMoreFrag = 0x8000 - ipVersion4 = 0x45 - ipProtocolTCP = 6 + SizeEthernetHeader = 14 + SizeIPv4Header = 20 + SizeUDPHeader = 8 + SizeARPv4Header = 28 + SizeTCPHeader = 20 + SizeDHCPHeader = 44 + ipflagDontFrag = 0x4000 + ipFlagMoreFrag = 0x8000 + ipVersion4 = 0x45 + ipProtocolTCP = 6 + ipProtocolUDP = 17 ) +func IsBroadcastHW(hwaddr net.HardwareAddr) bool { + // This comparison should be optimized by compiler to not allocate. + // See bytes.Equal. + return string(hwaddr) == broadcast +} + +func BroadcastHW() net.HardwareAddr { return net.HardwareAddr(broadcast) } + +// Broadcast is a special hardware address which indicates a Frame should +// be sent to every device on a given LAN segment. +const broadcast = "\xff\xff\xff" + "\xff\xff\xff" + var ( - // Broadcast is a special hardware address which indicates a Frame should - // be sent to every device on a given LAN segment. - Broadcast = net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff} - None = net.HardwareAddr{0, 0, 0, 0, 0, 0} + none = net.HardwareAddr{0, 0, 0, 0, 0, 0} ) type EtherType uint16 -// Ethertype values. From: http://en.wikipedia.org/wiki/Ethertype -// -//go:generate stringer -type=EtherType -trimprefix=EtherType -const ( - EtherTypeIPv4 EtherType = 0x0800 - EtherTypeARP EtherType = 0x0806 - EtherTypeWakeOnLAN EtherType = 0x0842 - EtherTypeTRILL EtherType = 0x22F3 - EtherTypeDECnetPhase4 EtherType = 0x6003 - EtherTypeRARP EtherType = 0x8035 - EtherTypeAppleTalk EtherType = 0x809B - EtherTypeAARP EtherType = 0x80F3 - EtherTypeIPX1 EtherType = 0x8137 - EtherTypeIPX2 EtherType = 0x8138 - EtherTypeQNXQnet EtherType = 0x8204 - EtherTypeIPv6 EtherType = 0x86DD - EtherTypeEthernetFlowControl EtherType = 0x8808 - EtherTypeIEEE802_3 EtherType = 0x8809 - EtherTypeCobraNet EtherType = 0x8819 - EtherTypeMPLSUnicast EtherType = 0x8847 - EtherTypeMPLSMulticast EtherType = 0x8848 - EtherTypePPPoEDiscovery EtherType = 0x8863 - EtherTypePPPoESession EtherType = 0x8864 - EtherTypeJumboFrames EtherType = 0x8870 - EtherTypeHomePlug1_0MME EtherType = 0x887B - EtherTypeIEEE802_1X EtherType = 0x888E - EtherTypePROFINET EtherType = 0x8892 - EtherTypeHyperSCSI EtherType = 0x889A - EtherTypeAoE EtherType = 0x88A2 - EtherTypeEtherCAT EtherType = 0x88A4 - EtherTypeEthernetPowerlink EtherType = 0x88AB - EtherTypeLLDP EtherType = 0x88CC - EtherTypeSERCOS3 EtherType = 0x88CD - EtherTypeHomePlugAVMME EtherType = 0x88E1 - EtherTypeMRP EtherType = 0x88E3 - EtherTypeIEEE802_1AE EtherType = 0x88E5 - EtherTypeIEEE1588 EtherType = 0x88F7 - EtherTypeIEEE802_1ag EtherType = 0x8902 - EtherTypeFCoE EtherType = 0x8906 - EtherTypeFCoEInit EtherType = 0x8914 - EtherTypeRoCE EtherType = 0x8915 - EtherTypeCTP EtherType = 0x9000 - EtherTypeVeritasLLT EtherType = 0xCAFE - EtherTypeVLAN EtherType = 0x8100 - EtherTypeServiceVLAN EtherType = 0x88a8 - // minEthPayload is the minimum payload size for an Ethernet frame, assuming - // that no 802.1Q VLAN tags are present. - minEthPayload = 46 -) - // AssertType returns the Size or EtherType field of the Ethernet frame as EtherType. func (e EthernetHeader) AssertType() EtherType { return EtherType(e.SizeOrEtherType) } @@ -275,6 +288,13 @@ func (f *EthernetHeader) String() string { "etype: ", ethertpStr, vlanstr) } +// IHL returns the internet header length in 32bit words and is guaranteed to be within 0..15. +// Valid values for IHL are 5..15. When multiplied by 4 this yields number of bytes of the header, 20..60. +func (iphdr *IPv4Header) IHL() uint8 { return iphdr.VersionAndIHL & 0xf } +func (iphdr *IPv4Header) Version() uint8 { return iphdr.VersionAndIHL >> 4 } +func (iphdr *IPv4Header) DSCP() uint8 { return iphdr.ToS >> 2 } +func (iphdr *IPv4Header) ECN() uint8 { return iphdr.ToS & 0b11 } + func (iphdr *IPv4Header) FrameLength() int { return int(iphdr.TotalLength) } @@ -293,8 +313,8 @@ func (ip *IPv4Header) String() string { // DecodeIPv4Header decodes a 20 byte IPv4 header from buf. func DecodeIPv4Header(buf []byte) (iphdr IPv4Header) { _ = buf[19] - iphdr.Version = buf[0] - iphdr.IHL = buf[1] + iphdr.VersionAndIHL = buf[0] + iphdr.ToS = buf[1] iphdr.TotalLength = binary.BigEndian.Uint16(buf[2:]) iphdr.ID = binary.BigEndian.Uint16(buf[4:]) iphdr.Flags = IPFlags(binary.BigEndian.Uint16(buf[6:])) @@ -309,8 +329,8 @@ func DecodeIPv4Header(buf []byte) (iphdr IPv4Header) { // Put marshals the IPv4 frame onto buf. buf needs to be 20 bytes in length or Put panics. func (iphdr *IPv4Header) Put(buf []byte) { _ = buf[19] - buf[0] = iphdr.Version - buf[1] = iphdr.IHL + buf[0] = (4 << 4) | (iphdr.VersionAndIHL & 0xf) // ignore set version. + buf[1] = iphdr.ToS binary.BigEndian.PutUint16(buf[2:], iphdr.TotalLength) binary.BigEndian.PutUint16(buf[4:], iphdr.ID) binary.BigEndian.PutUint16(buf[6:], uint16(iphdr.Flags)) @@ -334,6 +354,15 @@ func (iphdr *IPv4Header) PutPseudo(buf []byte) { copy(buf[8:12], iphdr.Destination[:]) } +func (iphdr *IPv4Header) CalculateChecksum() uint16 { + crc := CRC791{} + var buf [SizeIPv4Header]byte + iphdr.Put(buf[:]) + binary.BigEndian.PutUint16(buf[10:], 0) // Zero out checksum field. + crc.Write(buf[:]) + return crc.Sum16() +} + type IPFlags uint16 func (f IPFlags) DontFragment() bool { return f&ipflagDontFrag != 0 } @@ -441,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 { @@ -466,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 @@ -560,3 +585,70 @@ func hexascii(b byte) [2]byte { const hexstr = "0123456789abcdef" return [2]byte{hexstr[b>>4], hexstr[b&0b1111]} } + +func (d *DHCPHeader) Put(dst []byte) { + _ = dst[43] + dst[0] = d.OP + dst[1] = d.HType + dst[2] = d.HLen + dst[3] = d.HOps + binary.BigEndian.PutUint32(dst[4:8], d.Xid) + binary.BigEndian.PutUint16(dst[8:10], d.Secs) + binary.BigEndian.PutUint16(dst[10:12], d.Flags) + copy(dst[12:16], d.CIAddr[:]) + copy(dst[16:20], d.YIAddr[:]) + copy(dst[20:24], d.SIAddr[:]) + copy(dst[24:28], d.GIAddr[:]) + copy(dst[28:44], d.CHAddr[:]) +} + +func DecodeDHCPHeader(src []byte) (d DHCPHeader) { + _ = src[43] + d.OP = src[0] + d.HType = src[1] + d.HLen = src[2] + d.HOps = src[3] + d.Xid = binary.BigEndian.Uint32(src[4:8]) + d.Secs = binary.BigEndian.Uint16(src[8:10]) + d.Flags = binary.BigEndian.Uint16(src[10:12]) + copy(d.CIAddr[:], src[12:16]) + copy(d.YIAddr[:], src[16:20]) + copy(d.SIAddr[:], src[20:24]) + copy(d.GIAddr[:], src[24:28]) + copy(d.CHAddr[:], src[28:44]) + return d +} + +func (d *DHCPHeader) String() (s string) { + s = "DHCP op=" + strconv.Itoa(int(d.OP)) + " " + if d.CIAddr != [4]byte{} { + s += "ciaddr=" + net.IP(d.CIAddr[:]).String() + " " + } + if d.YIAddr != [4]byte{} { + s += "yiaddr=" + net.IP(d.YIAddr[:]).String() + " " + } + if d.SIAddr != [4]byte{} { + s += "siaddr=" + net.IP(d.SIAddr[:]).String() + " " + } + if d.GIAddr != [4]byte{} { + s += "giaddr=" + net.IP(d.GIAddr[:]).String() + " " + } + if d.CHAddr != [16]byte{} && d.HLen < 16 && d.HLen > 0 { + s += "chaddr=" + net.HardwareAddr(d.CHAddr[:d.HLen]).String() + " " + } + return s +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func min(a, b byte) byte { + if a < b { + return a + } + return b +} diff --git a/internal/tcpctl/eth/headers_test.go b/internal/tcpctl/eth/headers_test.go index 2905db1..3614220 100644 --- a/internal/tcpctl/eth/headers_test.go +++ b/internal/tcpctl/eth/headers_test.go @@ -23,31 +23,44 @@ func TestUDPChecksum(t *testing.T) { 0x22, 0x3a, 0x20, 0x5b, 0x38, 0x31, 0x35, 0x32, 0x34, 0x36, 0x32, 0x30, 0x30, 0x30, 0x5d, 0x7d, // |": [8152462000]}| } // Process Ethernet header. - e := DecodeEthernetHeader(testUDPPacket[:14]) - if !bytes.Equal(e.Destination[:], testUDPPacket[0:6]) { + ethData := testUDPPacket[:14] + e := DecodeEthernetHeader(ethData) + if !bytes.Equal(e.Destination[:], ethData[0:6]) { t.Errorf("incorrect ethernet destination: %v", e.String()) } - if !bytes.Equal(e.Source[:], testUDPPacket[6:12]) { + if !bytes.Equal(e.Source[:], ethData[6:12]) { t.Errorf("incorrect ethernet source: %v", e.String()) } if e.AssertType() != EtherTypeIPv4 { t.Errorf("incorrect ethertype: %v", e.String()) } - + ethDataGot := make([]byte, len(ethData)) + e.Put(ethDataGot) + if !bytes.Equal(ethData, ethDataGot) { + got := DecodeEthernetHeader(ethDataGot) + t.Error("ethernet marshal does not match original data", e.String(), got.String()) + } // Process IP header. - ip := DecodeIPv4Header(testUDPPacket[14:34]) + ipData := testUDPPacket[14:34] + ip := DecodeIPv4Header(ipData) if ip.Protocol != 17 { - t.Errorf("incorrect ip protocol: %v", ip.String()) + t.Errorf("incorrect IP protocol: %v", ip.String()) } if !bytes.Equal(ip.Source[:], testUDPPacket[26:30]) { - t.Errorf("incorrect ip source: %v", ip.String()) + t.Errorf("incorrect IP source: %v", ip.String()) } if !bytes.Equal(ip.Destination[:], testUDPPacket[30:34]) { - t.Errorf("incorrect ip destination: %v", ip.String()) + t.Errorf("incorrect IP destination: %v", ip.String()) + } + ipDataGot := make([]byte, len(ipData)) + ip.Put(ipDataGot) + if !bytes.Equal(ipData, ipDataGot) { + got := DecodeIPv4Header(ipDataGot) + t.Error("IP marshal does not match original data", ip.String(), got.String()) } - // Process UDP header. - udp := DecodeUDPHeader(testUDPPacket[34 : 34+8]) + udpData := testUDPPacket[34 : 34+8] + udp := DecodeUDPHeader(udpData) if udp.SourcePort != 17500 { t.Errorf("incorrect udp source port: %v", udp.String()) } @@ -57,6 +70,12 @@ func TestUDPChecksum(t *testing.T) { if udp.Length != 142 { t.Errorf("incorrect udp length: %v", udp.String()) } + udpDataGot := make([]byte, len(udpData)) + udp.Put(udpDataGot) + if !bytes.Equal(udpData, udpDataGot) { + got := DecodeUDPHeader(udpDataGot) + t.Error("UDP marshal does not match original data", udp.String(), got.String()) + } } func TestCRC791_oneshot(t *testing.T) { 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 new file mode 100644 index 0000000..995e09a --- /dev/null +++ b/internal/tcpctl/stack.go @@ -0,0 +1,445 @@ +package tcpctl + +import ( + "bytes" + "errors" + "fmt" + "io" + "net" + "time" + + "github.com/soypat/cyw43439/internal/slog" + "github.com/soypat/cyw43439/internal/tcpctl/eth" +) + +const ( + _MTU = 1500 +) + +type StackConfig struct { + MAC net.HardwareAddr + IP net.IP + MaxUDPConns int +} + +// NewStack creates a ready to use TCP/UDP Stack instance. +func NewStack(cfg StackConfig) *Stack { + var s Stack + s.MAC = cfg.MAC + s.IP = cfg.IP + s.UDPv4 = make([]udpSocket, cfg.MaxUDPConns) + return &s +} + +// Stack is a TCP/UDP netlink implementation for muxing packets received into +// their respective sockets with [Stack.RcvEth]. +type Stack struct { + lastRx time.Time + lastRxSuccess time.Time + MAC net.HardwareAddr + // 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 + droppedPackets uint32 + processedPackets uint32 +} + +// Common errors. +var ( + 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 +// defers response handling of the packets during a call to [Stack.HandleEth]. +// +// If [Stack.HandleEth] is not called often enough prevent packet queue from +// filling up on a socket RecvEth will start to return [ErrDroppedPacket]. +func (s *Stack) RecvEth(ethernetFrame []byte) (err error) { + var ehdr eth.EthernetHeader + var ihdr eth.IPv4Header + 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.debug("Stack.RecvEth:start", slog.Int("plen", len(payload))) + s.lastRx = time.Now() + + // Ethernet parsing block + ehdr = eth.DecodeEthernetHeader(payload) + if s.MAC != nil && !eth.IsBroadcastHW(ehdr.Destination[:]) && !bytes.Equal(ehdr.Destination[:], s.MAC) { + return nil // Ignore packet, is not for us. + } else if ehdr.AssertType() != eth.EtherTypeIPv4 { + return nil // Ignore Non-IPv4 packets. + } + + // IP parsing block. + ihdr = eth.DecodeIPv4Header(payload[eth.SizeEthernetHeader:]) + ihl := ihdr.IHL() + offset := eth.SizeEthernetHeader + 4*ihl // Can be at most 14+60=74, so no overflow risk. + end := eth.SizeEthernetHeader + ihdr.TotalLength + 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 + } + + payload = payload[offset:end] + switch ihdr.Protocol { + case 17: + // UDP (User Datagram Protocol). + if len(s.UDPv4) == 0 { + return nil // No sockets. + } else if len(payload) < eth.SizeUDPHeader { + return errTooShortTCPOrUDP + } + uhdr := eth.DecodeUDPHeader(payload) + 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 errChecksumTCPorUDP + } + + 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). + 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 +} + +// 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 value 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. +func (s *Stack) HandleEth(dst []byte) (n int, err error) { + switch { + case len(dst) < _MTU: + return 0, io.ErrShortBuffer + 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 { + socket := &s.UDPv4[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.pendingUDPv4-- + if err != nil { + socket.Close() + return 0, err + } + if n == 0 { + continue // Nothing done or io.ErrNoProgress flag. + } + break // If we got here our packet has been processed. + } + } + + if n == 0 && s.pendingTCPv4 > 0 { + 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 { + s.processedPackets++ + } + 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...) +} + +func (s *Stack) error(msg string, attrs ...slog.Attr) { + logAttrsPrint(slog.LevelError, msg, attrs...) +} + +func (s *Stack) debug(msg string, attrs ...slog.Attr) { + logAttrsPrint(slog.LevelDebug, msg, attrs...) +} + +func logAttrsPrint(level slog.Level, msg string, attrs ...slog.Attr) { + var levelStr string = level.String() + + print(levelStr) + print(" ") + print(msg) + + for _, a := range attrs { + print(" ") + print(a.Key) + print("=") + if a.Value.Kind() == slog.KindAny { + fmt.Printf("%+v", a.Value.Any()) + } else { + print(a.Value.String()) + } + } + println() +} diff --git a/internal/tcpctl/tcpctl.go b/internal/tcpctl/tcpctl.go index 2a3915a..aa4696a 100644 --- a/internal/tcpctl/tcpctl.go +++ b/internal/tcpctl/tcpctl.go @@ -7,6 +7,7 @@ import ( "math" "net" + "github.com/soypat/cyw43439/internal/netlink" "github.com/soypat/cyw43439/internal/tcpctl/eth" ) @@ -50,23 +51,23 @@ const ( StateLastAck ) -type Socket struct { +type TCPSocket struct { cs connState us net.TCPAddr them net.TCPAddr staticBuf [1504]byte } -func (s *Socket) Listen() { +func (s *TCPSocket) Listen() { s.cs.SetState(StateListen) } -func (s *Socket) RecvEthernet(buf []byte) (payloadStart, payloadEnd uint16, err error) { +func (s *TCPSocket) RecvEthernet(buf []byte) (payloadStart, payloadEnd uint16, err error) { buflen := uint16(len(buf)) switch { case len(buf) > math.MaxUint16: err = errors.New("buffer too long") - case buflen < eth.SizeEthernetHeaderNoVLAN+eth.SizeIPv4Header+eth.SizeTCPHeaderNoOptions: + case buflen < eth.SizeEthernetHeader+eth.SizeIPv4Header+eth.SizeTCPHeader: err = errors.New("buffer too short to contain TCP") } @@ -80,14 +81,14 @@ func (s *Socket) RecvEthernet(buf []byte) (payloadStart, payloadEnd uint16, err if ethhdr.SizeOrEtherType != uint16(eth.EtherTypeIPv4) { return 0, 0, errors.New("support only IPv4") } - payloadStart, payloadEnd, err = s.RecvTCP(buf[eth.SizeEthernetHeaderNoVLAN:]) + payloadStart, payloadEnd, err = s.RecvTCP(buf[eth.SizeEthernetHeader:]) if err != nil { return 0, 0, err } - return payloadStart + eth.SizeEthernetHeaderNoVLAN, payloadEnd + eth.SizeEthernetHeaderNoVLAN, nil + return payloadStart + eth.SizeEthernetHeader, payloadEnd + eth.SizeEthernetHeader, nil } -func (s *Socket) RecvTCP(buf []byte) (payloadStart, payloadEnd uint16, err error) { +func (s *TCPSocket) RecvTCP(buf []byte) (payloadStart, payloadEnd uint16, err error) { buflen := uint16(len(buf)) ip := eth.DecodeIPv4Header(buf[:]) payloadEnd = ip.TotalLength @@ -98,15 +99,15 @@ func (s *Socket) RecvTCP(buf []byte) (payloadStart, payloadEnd uint16, err error // fmt.Printf("%+v\n%s\n", ip, ip.String()) return 0, 0, fmt.Errorf("expected TCP protocol (6) in IP.Proto field; got %d", ip.Protocol) } - if ip.IHL != 0 { - return 0, 0, errors.New("expected IP.IHL to be zero") + if ip.ToS != 0 { + return 0, 0, errors.New("expected IP.ToS to be zero") } tcp := eth.DecodeTCPHeader(buf[eth.SizeIPv4Header:]) nb := tcp.OffsetInBytes() 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) } @@ -117,7 +118,7 @@ func (s *Socket) RecvTCP(buf []byte) (payloadStart, payloadEnd uint16, err error if s.cs.pendingCtlFrame == 0 { return payloadStart, payloadEnd, nil } - tcpOptions := buf[eth.SizeIPv4Header+eth.SizeTCPHeaderNoOptions : payloadStart] + tcpOptions := buf[eth.SizeIPv4Header+eth.SizeTCPHeader : payloadStart] gotSum := tcp.CalculateChecksumIPv4(&ip, tcpOptions, buf[payloadStart:payloadEnd]) if gotSum != tcp.Checksum { fmt.Println("Checksum mismatch!") @@ -130,7 +131,7 @@ func (s *Socket) RecvTCP(buf []byte) (payloadStart, payloadEnd uint16, err error return payloadStart, payloadEnd, err } -func (s *Socket) rx(hdr *eth.TCPHeader) (err error) { +func (s *TCPSocket) rx(hdr *eth.TCPHeader) (err error) { s.cs.mu.Lock() defer s.cs.mu.Unlock() switch s.cs.state { @@ -171,19 +172,19 @@ func (s *Socket) rx(hdr *eth.TCPHeader) (err error) { } // writeTCPIPv4 writes a TCP+IPv4 packet to dst, returning the number of bytes written. -func (s *Socket) writeTCPIPv4(dst, tcpOpts, payload []byte) (n int, err error) { +func (s *TCPSocket) writeTCPIPv4(dst, tcpOpts, payload []byte) (n int, err error) { if len(dst) > math.MaxUint16 { return 0, errors.New("buffer too long for TCP/IP") } // Exclude Ethernet header and CRC in frame size. - payloadOffset := len(tcpOpts) + eth.SizeIPv4Header + eth.SizeTCPHeaderNoOptions + payloadOffset := len(tcpOpts) + eth.SizeIPv4Header + eth.SizeTCPHeader if len(dst) < payloadOffset+len(payload) { return 0, io.ErrShortBuffer } // Limit dst to the size of the frame. dst = dst[:payloadOffset+len(payload)] - offsetBytes := len(tcpOpts) + eth.SizeTCPHeaderNoOptions + offsetBytes := len(tcpOpts) + eth.SizeTCPHeader offset := offsetBytes / 4 if offsetBytes%4 != 0 { offset++ @@ -193,13 +194,12 @@ func (s *Socket) writeTCPIPv4(dst, tcpOpts, payload []byte) (n int, err error) { } payloadOffset = eth.SizeIPv4Header + offset*4 ip := eth.IPv4Header{ - Version: 4, - IHL: eth.SizeIPv4Header / 4, - TotalLength: uint16(offsetBytes+len(payload)) + eth.SizeIPv4Header + eth.SizeTCPHeaderNoOptions, - ID: 0, - Flags: 0, - TTL: 255, - Protocol: 6, // 6 == TCP. + VersionAndIHL: 4 & ((eth.SizeIPv4Header / 4) << 4), + TotalLength: uint16(offsetBytes+len(payload)) + eth.SizeIPv4Header + eth.SizeTCPHeader, + ID: 0, + Flags: 0, + TTL: 255, + Protocol: 6, // 6 == TCP. } copy(ip.Destination[:], s.them.IP) copy(ip.Source[:], s.us.IP) @@ -216,7 +216,7 @@ func (s *Socket) writeTCPIPv4(dst, tcpOpts, payload []byte) (n int, err error) { tcp.Checksum = tcp.CalculateChecksumIPv4(&ip, tcpOpts, payload) // Copy TCP header+options and payload into buffer. tcp.Put(dst[eth.SizeIPv4Header:]) - nopt := copy(dst[eth.SizeIPv4Header+eth.SizeTCPHeaderNoOptions:payloadOffset], tcpOpts) + nopt := copy(dst[eth.SizeIPv4Header+eth.SizeTCPHeader:payloadOffset], tcpOpts) if nopt != len(tcpOpts) { panic("tcp options copy failed") } @@ -224,7 +224,31 @@ func (s *Socket) writeTCPIPv4(dst, tcpOpts, payload []byte) (n int, err error) { // Calculate IP checksum and copy IP header into buffer. crc := eth.CRC791{} crc.Write(dst[eth.SizeIPv4Header:]) // We limited dst size above. - ip.Checksum = crc.Sum() + ip.Checksum = crc.Sum16() ip.Put(dst) return len(dst), nil } + +func ResolveDHCPv4(dev netlink.Netlinker) (net.IP, error) { + // TODO + var ( + broadcast = []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff} + rawbuf [1500]byte + ) + { // Ethernet header. + hdr := eth.EthernetHeader{SizeOrEtherType: uint16(eth.EtherTypeIPv4)} + copy(hdr.Destination[:], broadcast) + hwaddr, err := dev.GetHardwareAddr() + if err != nil { + return nil, err + } + if n := copy(hdr.Source[:], hwaddr); n != 6 { + return nil, errors.New("MAC shorter than 6") + } + hdr.Put(rawbuf[:]) + } + { // IP header. + // hdr := eth.IPv4Header{} + } + return nil, nil +} diff --git a/internal/tcpctl/tcpctl_test.go b/internal/tcpctl/tcpctl_test.go index fb98b24..35d7481 100644 --- a/internal/tcpctl/tcpctl_test.go +++ b/internal/tcpctl/tcpctl_test.go @@ -16,7 +16,7 @@ var ( ) func TestSynReceive(t *testing.T) { - s := tcpctl.Socket{} + s := tcpctl.TCPSocket{} s.Listen() pStart, pEnd, err := s.RecvEthernet(packetSyn) _, _ = pStart, pEnd