diff options
Diffstat (limited to 'metis/ccnx/forwarder/metis/platforms/darwin/metis_GenericEther.c')
-rw-r--r-- | metis/ccnx/forwarder/metis/platforms/darwin/metis_GenericEther.c | 618 |
1 files changed, 618 insertions, 0 deletions
diff --git a/metis/ccnx/forwarder/metis/platforms/darwin/metis_GenericEther.c b/metis/ccnx/forwarder/metis/platforms/darwin/metis_GenericEther.c new file mode 100644 index 00000000..b49b6579 --- /dev/null +++ b/metis/ccnx/forwarder/metis/platforms/darwin/metis_GenericEther.c @@ -0,0 +1,618 @@ +/* + * Copyright (c) 2017 Cisco and/or its affiliates. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Implements the platform-specific code for working with an Ethernet interface. + * + * Uses the Berkeley Packet Filter (BPF) approach to reading the Ethernet device. + */ + +#include <config.h> +#include <stdio.h> +#include <net/bpf.h> +#include <sys/ioctl.h> +#include <net/if.h> +#include <fcntl.h> +#include <errno.h> +#include <string.h> +#include <unistd.h> +#include <net/if_arp.h> +#include <net/if.h> +#include <net/if_dl.h> +#include <net/if_types.h> +#include <ifaddrs.h> + +#include <net/ethernet.h> + +#include <LongBow/runtime.h> + +#include <parc/algol/parc_Memory.h> +#include <ccnx/forwarder/metis/io/metis_GenericEther.h> +#include <ccnx/forwarder/metis/core/metis_System.h> +#include <parc/algol/parc_Object.h> +#include <ccnx/forwarder/metis/io/metis_Ethernet.h> + +struct metis_generic_ether { + uint16_t ethertype; + int etherSocket; + + // what size do the read buffers need to be? ioctl BIOCGBLEN will tell us. + unsigned etherBufferLength; + + // MTU set on interface when we are created + unsigned mtu; + + PARCEventBuffer *workBuffer; + + PARCBuffer *macAddress; + MetisLogger *logger; +}; + +static void +_metisGenericEther_Destroy(MetisGenericEther **etherPtr) +{ + MetisGenericEther *ether = *etherPtr; + + if (metisLogger_IsLoggable(ether->logger, MetisLoggerFacility_IO, PARCLogLevel_Debug)) { + metisLogger_Log(ether->logger, MetisLoggerFacility_IO, PARCLogLevel_Debug, __func__, + "GenericEther %p destroyed", (void *) ether); + } + + if (ether->etherSocket > 0) { + close(ether->etherSocket); + } + + if (ether->macAddress) { + parcBuffer_Release(ðer->macAddress); + } + + metisLogger_Release(ðer->logger); + parcEventBuffer_Destroy(ðer->workBuffer); +} + + +// ========================= +// PRIVATE API +// ========================= + +/* + * Returns the total legth of the good data: the ethernet header plus ccnx packet. + * If there is a FCS, it will be excluded. + * + * PRECONDITION: You have drained off any BPF headers and the first byte + * of the work buffer points to the first byte of the Ethernet header + */ +static uint16_t +_getFrameLengthFromWorkBuffer(MetisGenericEther *ether) +{ + uint16_t frameLength = 0; + + // read the fixed header + uint8_t *etherHeader = parcEventBuffer_Pullup(ether->workBuffer, ETHER_HDR_LEN + metisTlv_FixedHeaderLength()); + + if (etherHeader) { + uint8_t *fixedHeader = etherHeader + ETHER_HDR_LEN; + frameLength = metisTlv_TotalPacketLength(fixedHeader) + ETHER_HDR_LEN; + } + + return frameLength; +} + +/* + * An attempt to read from the workbuffer to the readbuffer can succeed (ok), fail because + * the work buffer does not have enough bytes (empty), or an error causes a frame to be + * discarded (tryagain). + */ +typedef enum { + ReadWorkBufferResult_Ok, + ReadWorkBufferResult_Empty, + ReadWorkBufferResult_TryAgain +} _ReadWorkBufferResult; + +/** + * Parses the work buffer to extract packets + * + * The work buffer should be filled in with a set of tuples (bh_hdrlen, frame, pad). + * The pad extends each packet out to BPF_WORDALIGN. + * + * If the CCNxMessage PacketLength says it is larger than the read capture length (caplen), + * then this is an invalid packet and it will be discarded. This error will result in a + * ReadWorkBufferResult_TryAgain condition. + * + * struct bpf_hdr { + * struct BPF_TIMEVAL bh_tstamp; // time stamp + * bpf_u_int32 bh_caplen; // length of captured portion + * bpf_u_int32 bh_datalen; // original length of packet + * u_short bh_hdrlen; // length of bpf header (this struct plus alignment padding) + * } + * + * @param [in] ether An allocated Darwin ethernet. + * @param [in] readbuffer A user-provided read buffer. + * + * @retval ReadWorkBufferResult_Ok A frame was moved from workbuffer to readbuffer + * @retval ReadWorkBufferResult_Empty There's not enough bytes in the workbuffer + * @retval ReadWorkBufferResult_TryAgain (likely discard) caused this call to fail, but you should try again + * + * Example: + * @code + * <#example#> + * @endcode + */ +static _ReadWorkBufferResult +_darwinEthernet_ReadWorkBuffer(MetisGenericEther *ether, PARCEventBuffer *readbuffer) +{ + _ReadWorkBufferResult result = ReadWorkBufferResult_Empty; + + // Make sure we have linear memory for the BPF header + struct bpf_hdr *bpfHeader = (struct bpf_hdr *) parcEventBuffer_Pullup(ether->workBuffer, sizeof(struct bpf_hdr)); + + // make sure we have enough bytes to process the frame + // bpfHeader may be NULL if there are not sizeof(struct bpf_hdr) bytes available. + if (bpfHeader && parcEventBuffer_GetLength(ether->workBuffer) >= bpfHeader->bh_hdrlen + bpfHeader->bh_caplen) { + // (0) Save the needed fields from the bpf header + // (1) pop off the bpf header + // (2) move the iovec from work buffer to readBuffer. + // (3) remove any BPF_WORDALIGN padding to the start of the next packet + + // (0) Save the needed fields from the bpf header + uint16_t hdrlen = bpfHeader->bh_hdrlen; + uint32_t caplen = bpfHeader->bh_caplen; + + // (1) pop off the bpf header + parcEventBuffer_Read(ether->workBuffer, NULL, hdrlen); + + // (1a) Determine the packet length from the fixed header and only transfer that many bytes + uint16_t packetlen = _getFrameLengthFromWorkBuffer(ether); + + if (packetlen <= caplen) { + // (2) move the iovec from work buffer to readBuffer. + parcEventBuffer_ReadIntoBuffer(ether->workBuffer, readbuffer, packetlen); + + // (2a) drain off any trailer (i.e. FCS) + parcEventBuffer_Read(ether->workBuffer, NULL, caplen - packetlen); + + result = ReadWorkBufferResult_Ok; + } else { + if (metisLogger_IsLoggable(ether->logger, MetisLoggerFacility_IO, PARCLogLevel_Warning)) { + metisLogger_Log(ether->logger, MetisLoggerFacility_IO, PARCLogLevel_Warning, __func__, + "%s reading fd %d discard packetlen %u greater than caplen %u", + __func__, ether->etherSocket, packetlen, caplen); + } + + // discard all of caplen + parcEventBuffer_Read(ether->workBuffer, NULL, caplen); + + // tell the caller that this read failed, but they could try again. + result = ReadWorkBufferResult_TryAgain; + } + + // (3) remove any BPF_WORDALIGN padding to the start of the next packet + size_t alignedLength = BPF_WORDALIGN(hdrlen + caplen); + size_t pad = alignedLength - hdrlen - caplen; + parcEventBuffer_Read(ether->workBuffer, NULL, pad); + } + return result; +} + +/** + * Reads at most a single frame from the BPF queue in the workbuffer + * + * Reads at most a single frame from the BPF queue in the workbuffer to the user-provided + * readbuffer. Returns true if one frame moved to readbuffer or false otherwise. + * + * The frame will be appended to the readbuffer. If return failure, the readbuffer will + * be unmodified. + * + * The returned frame will not contain a FCS, even if one was read. + * + * @param [in] ether An allocated Darwin ethernet. + * @param [in] readbuffer A user-provided read buffer. + * + * @retval true A frame was added to the readbuffer + * @retval false No frame is available at this time + * + * Example: + * @code + * <#example#> + * @endcode + */ +static bool +_darwinEthernet_WorkBufferToReadBuffer(MetisGenericEther *ether, PARCEventBuffer *readbuffer) +{ + _ReadWorkBufferResult result = ReadWorkBufferResult_Empty; + do { + result = _darwinEthernet_ReadWorkBuffer(ether, readbuffer); + } while (result == ReadWorkBufferResult_TryAgain); + + return (result == ReadWorkBufferResult_Ok); +} + +/** + * Reads from the socket to fill in the work buffer + * + * Reads one or more packets from the socket to the work buffer It will append to the work buffer. + * The BFP socket is non-blocking. The BPF interface may return multiple packets in one read + * that need to be parsed as in _darwinEthernet_ReadWorkBuffer(). + * + * @param [in] ether The Darwin ethernet interface + * + * @retval true We added something to the work buffer + * @retval false Nothing was read + * + * Example: + * @code + * <#example#> + * @endcode + */ +static bool +_darwinEthernet_ReadSocket(MetisGenericEther *ether) +{ + bool success = false; + + if (metisLogger_IsLoggable(ether->logger, MetisLoggerFacility_IO, PARCLogLevel_Debug)) { + metisLogger_Log(ether->logger, MetisLoggerFacility_IO, PARCLogLevel_Debug, __func__, + "%s reading fd %d bufferLength %u", __func__, ether->etherSocket, ether->etherBufferLength); + } + + // The buffer we're reading must be exactly ether->etherBufferLength + // TODO: Fix the parcEventBuffer_ReadFromFileDescriptor call to reserve that space so it's all there. + + uint8_t tempBuffer[ether->etherBufferLength]; + ssize_t read_length = read(ether->etherSocket, tempBuffer, ether->etherBufferLength); + if (read_length > 0) { + parcEventBuffer_Append(ether->workBuffer, tempBuffer, read_length); + if (read_length > 0) { + if (metisLogger_IsLoggable(ether->logger, MetisLoggerFacility_IO, PARCLogLevel_Debug)) { + metisLogger_Log(ether->logger, MetisLoggerFacility_IO, PARCLogLevel_Debug, __func__, + "%s read %zd bytes from fd %d", + __func__, + read_length, + ether->etherSocket); + } + success = true; + } + } + + return success; +} + + +// open the first available /dev/bpf. Returns +// the fd or -1 on none found +static int +_darwinEthernet_OpenBfpDevice(void) +{ + for (int i = 0; i < 255; i++) { + char bpfstr[255]; + snprintf(bpfstr, sizeof(bpfstr), "/dev/bpf%d", i); + + int fd = open(bpfstr, O_RDWR); + if (fd > -1) { + return fd; + } + + if (errno == EBUSY) { + continue; + } + + if (errno == EBADF) { + continue; + } + + // its an error + return -1; + } + + errno = ENOENT; + return -1; +} + +/** + * Set the ioctrl for the BPF socket + * + * (a) bind to a specific name + * (b) The kernel will fill in the source MAC address + * (c) We will see out-going packets [turn off?] + * (d) immediate read() [don't wait to queue multiple packets] + * (e) Include the buffer length + * + * Settings BIOCIMMEDIATE means that read() calls will not wait for several packets + * to accumulate. It does not, however, guarantee only one packet per read(). + * + * @param [in] fd BPF socket + * @param [in] devstr The device name + * + * @retval true Set all needed ioctrl + * @retval false An error + * + * Example: + * @code + * <#example#> + * @endcode + */ +static bool +_darwinEthernet_SetDeviceOptions(int fd, const char *devstr) +{ + struct ifreq ifr; + uint32_t on = 1; + + bool success = false; + + // Always null terminate. If devstr did not fit in ifr_name, then + // the ioctl call will fail anyway. + if (devstr != NULL) { + (void) strncpy(ifr.ifr_name, devstr, IF_NAMESIZE - 1); + ifr.ifr_name[IF_NAMESIZE - 1] = 0; + + if (ioctl(fd, BIOCSETIF, &ifr)) { + return false; + } + } + + // on = immediate read on packet reception + if (!ioctl(fd, BIOCIMMEDIATE, &on)) { + // on = request the buffer length prepended to each packet + if (!ioctl(fd, BIOCGBLEN, &on)) { + // set non-blocking + if (!ioctl(fd, FIONBIO, &on)) { + success = true; + } + } + } + + return success; +} + +/** + * Create the berkeley packet filter for our ethertype + * + * Creates a BPF for our ether type. + * + * @param [in,out] ether The MetisGenericEther to modify + * + * @retval true success + * @retval false failure + * + * Example: + * @code + * <#example#> + * @endcode + */ +static bool +_darwinEthernet_SetFilter(MetisGenericEther *ether) +{ + struct bpf_program filterCode = { 0 }; + + // BPF instructions: + // Load 12 to accumulator (offset of ethertype) + // Jump Equal to netbyteorder_ethertype 0 instructions, otherwise 1 instruction + // 0: return a length of -1 (meaning whole packet) + // 1: return a length of 0 (meaning skip packet) + struct bpf_insn instructions[] = { + BPF_STMT(BPF_LD + BPF_H + BPF_ABS, 12), + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, 0x0801, 0, 1), + BPF_STMT(BPF_RET + BPF_K, (u_int) - 1), + BPF_STMT(BPF_RET + BPF_K, 0), + }; + // BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, ether->ethertype, 0, 1), + + /* Set the filter */ + filterCode.bf_len = sizeof(instructions) / sizeof(struct bpf_insn); + filterCode.bf_insns = &instructions[0]; + + if (ioctl(ether->etherSocket, BIOCSETF, &filterCode) < 0) { + return false; + } + + return true; +} + +static bool +_darwinEthernet_SetupReceive(MetisGenericEther *ether, const char *devstr) +{ + // If we cannot open the Ethernet BPF (likely due to permissions), return + // a soft error so we can move past this failure. + if ((ether->etherSocket = _darwinEthernet_OpenBfpDevice()) < 0) { + return false; + } + + if (!_darwinEthernet_SetDeviceOptions(ether->etherSocket, devstr)) { + trapUnrecoverableState("error setting options: %s", strerror(errno)); + } + + + if (!_darwinEthernet_SetFilter(ether)) { + trapUnrecoverableState("error setting filter: %s", strerror(errno)); + } + + if (ioctl(ether->etherSocket, BIOCGBLEN, ðer->etherBufferLength)) { + trapUnrecoverableState("error getting buffer length: %s", strerror(errno)); + } + + return true; +} + +/** + * If the user specified a device name, set the MAC address in ether->macAddress + * + * <#Paragraphs Of Explanation#> + * + * @param [in] ether An allocated MetisGenericEther + * @param [in] devstr A C-String of the device name + * + * Example: + * @code + * <#example#> + * @endcode + */ +static void +_darwinEthernet_SetInterfaceAddress(MetisGenericEther *ether, const char *devstr) +{ + if (devstr) { + struct ifaddrs *ifaddr; + int failure = getifaddrs(&ifaddr); + assertFalse(failure, "Error getifaddrs: (%d) %s", errno, strerror(errno)); + + struct ifaddrs *next; + for (next = ifaddr; next != NULL; next = next->ifa_next) { + if (strcmp(next->ifa_name, devstr) == 0) { + if (next->ifa_addr->sa_family == AF_LINK) { + struct sockaddr_dl *addr_dl = (struct sockaddr_dl *) next->ifa_addr; + + // addr_dl->sdl_data[12] contains the interface name followed by the MAC address, so + // need to offset in to the array past the interface name. + PARCBuffer *addr = parcBuffer_Allocate(addr_dl->sdl_alen); + parcBuffer_PutArray(addr, addr_dl->sdl_alen, (uint8_t *) &addr_dl->sdl_data[ addr_dl->sdl_nlen]); + parcBuffer_Flip(addr); + ether->macAddress = addr; + + // break out of loop and freeifaddrs + break; + } + } + } + freeifaddrs(ifaddr); + } +} + +parcObject_ExtendPARCObject(MetisGenericEther, _metisGenericEther_Destroy, NULL, NULL, NULL, NULL, NULL, NULL); + +parcObject_ImplementAcquire(metisGenericEther, MetisGenericEther); + +parcObject_ImplementRelease(metisGenericEther, MetisGenericEther); + +// ========================= +// PUBLIC API +// ========================= + +MetisGenericEther * +metisGenericEther_Create(MetisForwarder *metis, const char *deviceName, uint16_t etherType) +{ + assertNotNull(metis, "Parameter metis must be non-null"); + + // The Darwin generic ether allows a NULL device name, it is used in the unit tests. + + MetisGenericEther *ether = NULL; + + if (metisEthernet_IsValidEthertype(etherType)) { + ether = parcObject_CreateInstance(MetisGenericEther); + ether->ethertype = etherType; + ether->logger = metisLogger_Acquire(metisForwarder_GetLogger(metis)); + ether->etherSocket = -1; // invalid valid + ether->workBuffer = parcEventBuffer_Create(); + ether->macAddress = NULL; + ether->mtu = metisSystem_InterfaceMtu(metis, deviceName); + + _darwinEthernet_SetInterfaceAddress(ether, deviceName); + + bool success = _darwinEthernet_SetupReceive(ether, deviceName); + + if (success) { + if (metisLogger_IsLoggable(ether->logger, MetisLoggerFacility_IO, PARCLogLevel_Info)) { + char *str = parcBuffer_ToHexString(ether->macAddress); + metisLogger_Log(ether->logger, MetisLoggerFacility_IO, PARCLogLevel_Info, __func__, + "GenericEther %p created on device %s (%s) for ethertype 0x%04x fd %d bufferLength %u mtu %u", + (void *) ether, deviceName, str, etherType, ether->etherSocket, ether->etherBufferLength, ether->mtu); + parcMemory_Deallocate((void **) &str); + } + } else { + if (metisLogger_IsLoggable(ether->logger, MetisLoggerFacility_IO, PARCLogLevel_Error)) { + metisLogger_Log(ether->logger, MetisLoggerFacility_IO, PARCLogLevel_Error, __func__, + "GenericEther failed to created on device %s for ethertype 0x%04x", + deviceName, etherType); + } + + // this will also null ether + metisGenericEther_Release(ðer); + } + + assertTrue(ether->etherBufferLength < 65536, "Buffer length way too big, expected less than 65536 got %u", ether->etherBufferLength); + } else { + if (metisLogger_IsLoggable(metisForwarder_GetLogger(metis), MetisLoggerFacility_IO, PARCLogLevel_Error)) { + metisLogger_Log(metisForwarder_GetLogger(metis), MetisLoggerFacility_IO, PARCLogLevel_Error, __func__, + "GenericEther failed to created on device %s for ethertype 0x%04x, invalid ethertype", + deviceName, etherType); + } + } + + return ether; +} + +int +metisGenericEther_GetDescriptor(const MetisGenericEther *ether) +{ + assertNotNull(ether, "Parameter ether must be non-null"); + return ether->etherSocket; +} + + +/* + * Reading a BPF packet will include the BPF header. Frame may include the FCS. + */ +bool +metisGenericEther_ReadNextFrame(MetisGenericEther *ether, PARCEventBuffer *readbuffer) +{ + assertNotNull(ether, "Parameter ether must be non-null"); + assertNotNull(readbuffer, "Parameter readbuffer must be non-null"); + + bool success = false; + + if (metisLogger_IsLoggable(ether->logger, MetisLoggerFacility_IO, PARCLogLevel_Debug)) { + metisLogger_Log(ether->logger, MetisLoggerFacility_IO, PARCLogLevel_Debug, __func__, + "Workbuffer length %zu", __func__, parcEventBuffer_GetLength(ether->workBuffer)); + } + + // Do we already have something in our work buffer? If not, try to read something. + if (parcEventBuffer_GetLength(ether->workBuffer) == 0) { + _darwinEthernet_ReadSocket(ether); + } + + success = _darwinEthernet_WorkBufferToReadBuffer(ether, readbuffer); + return success; +} + +bool +metisGenericEther_SendFrame(MetisGenericEther *ether, PARCEventBuffer *buffer) +{ + assertNotNull(ether, "Parameter ether must be non-null"); + + size_t length = parcEventBuffer_GetLength(buffer); + int written = parcEventBuffer_WriteToFileDescriptor(buffer, ether->etherSocket, -1); + if (written == length) { + return true; + } + return false; +} + +PARCBuffer * +metisGenericEther_GetMacAddress(const MetisGenericEther *ether) +{ + assertNotNull(ether, "Parameter ether must be non-null"); + return ether->macAddress; +} + +uint16_t +metisGenericEther_GetEtherType(const MetisGenericEther *ether) +{ + assertNotNull(ether, "Parameter ether must be non-null"); + return ether->ethertype; +} + +unsigned +metisGenericEther_GetMTU(const MetisGenericEther *ether) +{ + return ether->mtu; +} + |