| // Copyright 2017 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <fuchsia/hardware/ethernet/c/fidl.h> |
| |
| extern "C" { |
| #include <inet6/inet6.h> |
| } |
| |
| #include <lib/fdio/directory.h> |
| #include <lib/fdio/fd.h> |
| #include <lib/fdio/fdio.h> |
| #include <lib/zx/channel.h> |
| #include <lib/zx/fifo.h> |
| #include <lib/zx/vmo.h> |
| #include <pretty/hexdump.h> |
| #include <zircon/assert.h> |
| #include <zircon/boot/netboot.h> |
| #include <zircon/device/ethernet.h> |
| #include <zircon/process.h> |
| #include <zircon/syscalls.h> |
| |
| #include <arpa/inet.h> |
| #include <fcntl.h> |
| #include <limits.h> |
| #include <netinet/if_ether.h> |
| #include <netinet/ip.h> |
| #include <netinet/tcp.h> |
| #include <netinet/udp.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <unistd.h> |
| |
| #define ROUNDUP(a, b) (((a) + ((b)-1)) & ~((b)-1)) |
| |
| constexpr size_t BUFSIZE = 2048; |
| |
| typedef struct { |
| const char* device; |
| bool raw; |
| bool link_level; |
| bool promisc; |
| size_t packet_count; |
| size_t verbose_level; |
| int dumpfile; |
| } netdump_options_t; |
| |
| typedef struct { |
| uint32_t type; |
| uint32_t blk_tot_len; |
| uint32_t magic; |
| uint16_t major; |
| uint16_t minor; |
| uint64_t section_len; |
| // TODO(smklein): Add options here |
| uint32_t blk_tot_len2; |
| } __attribute__((packed)) pcap_shb_t; |
| |
| typedef struct { |
| uint32_t type; |
| uint32_t blk_tot_len; |
| uint16_t linktype; |
| uint16_t reserved; |
| uint32_t snaplen; |
| uint32_t blk_tot_len2; |
| } __attribute__((packed)) pcap_idb_t; |
| |
| typedef struct { |
| uint32_t type; |
| uint32_t blk_tot_len; |
| uint32_t pkt_len; |
| } __attribute__((packed)) simple_pkt_t; |
| |
| #define SIMPLE_PKT_MIN_SIZE (sizeof(simple_pkt_t) + sizeof(uint32_t)) |
| |
| static void print_mac(const uint8_t mac[ETH_ALEN]) { |
| printf("%02x:%02x:%02x:%02x:%02x:%02x", |
| mac[0], mac[1], mac[2], |
| mac[3], mac[4], mac[5]); |
| } |
| |
| static const char* ethtype_to_string(uint16_t ethtype) { |
| switch (ethtype) { |
| case ETH_P_IP: |
| return "IPv4"; |
| case ETH_P_ARP: |
| return "ARP"; |
| case ETH_P_IPV6: |
| return "IPV6"; |
| case ETH_P_8021Q: |
| return "802.1Q"; |
| default: |
| return "Unknown"; |
| } |
| } |
| |
| static const char* protocol_to_string(uint8_t protocol) { |
| switch (protocol) { |
| case IPPROTO_HOPOPTS: |
| return "HOPOPTS"; |
| case IPPROTO_TCP: |
| return "TCP"; |
| case IPPROTO_UDP: |
| return "UDP"; |
| case IPPROTO_ICMP: |
| return "ICMP"; |
| case IPPROTO_ROUTING: |
| return "ROUTING"; |
| case IPPROTO_FRAGMENT: |
| return "FRAGMENT"; |
| case IPPROTO_ICMPV6: |
| return "ICMPV6"; |
| case IPPROTO_NONE: |
| return "NONE"; |
| default: |
| return "Transport Unknown"; |
| } |
| } |
| |
| static const char* port_to_string(uint16_t port) { |
| switch (port) { |
| case 7: |
| return "Echo"; |
| case 20: |
| return "FTP xfer"; |
| case 21: |
| return "FTP ctl"; |
| case 22: |
| return "SSH"; |
| case 23: |
| return "Telnet"; |
| case 53: |
| return "DNS"; |
| case 69: |
| return "TFTP"; |
| case 80: |
| return "HTTP"; |
| case 115: |
| return "SFTP"; |
| case 123: |
| return "NTP"; |
| case 194: |
| return "IRC"; |
| case 443: |
| return "HTTPS"; |
| case DEBUGLOG_PORT: |
| return "Netboot Debug"; |
| case DEBUGLOG_ACK_PORT: |
| return "Netboot Debug ack"; |
| default: |
| return ""; |
| } |
| } |
| |
| static void print_port(uint16_t port, size_t verbosity) { |
| const char* str = port_to_string(port); |
| if (verbosity && strcmp(str, "")) { |
| printf(":%u (%s) ", port, str); |
| } else { |
| printf(":%u ", port); |
| } |
| } |
| |
| void parse_packet(void* packet, size_t length, const netdump_options_t& options) { |
| struct ethhdr* frame = static_cast<struct ethhdr*>(packet); |
| if (length < ETH_ZLEN) { |
| printf("Packet size (%lu) too small for ethernet frame\n", length); |
| if (options.verbose_level == 2) { |
| hexdump8_ex(packet, length, 0); |
| } |
| return; |
| } |
| uint16_t ethtype = htons(frame->h_proto); |
| |
| if (options.link_level) { |
| print_mac(frame->h_source); |
| printf(" > "); |
| print_mac(frame->h_dest); |
| printf(", ethertype %s (0x%x), ", ethtype_to_string(ethtype), ethtype); |
| } |
| |
| struct iphdr* ipv4 = reinterpret_cast<struct iphdr*>(frame + 1); |
| char buf[256]; |
| |
| void* transport_packet = NULL; |
| uint8_t transport_protocol; |
| |
| if (ipv4->version == 4) { |
| printf("IP4 "); |
| printf("%s > ", inet_ntop(AF_INET, &ipv4->saddr, buf, sizeof(buf))); |
| printf("%s: ", inet_ntop(AF_INET, &ipv4->daddr, buf, sizeof(buf))); |
| printf("%s, ", protocol_to_string(ipv4->protocol)); |
| printf("length %u, ", ntohs(ipv4->tot_len)); |
| uintptr_t transport = reinterpret_cast<uintptr_t>(ipv4 + 1); |
| transport_packet = reinterpret_cast<void*>(transport + (ipv4->ihl > 5 ? ipv4->ihl * 4 : 0)); |
| transport_protocol = ipv4->protocol; |
| } else if (ipv4->version == 6) { |
| ip6_hdr_t* ipv6 = reinterpret_cast<ip6_hdr_t*>(ipv4); |
| printf("IP6 "); |
| printf("%s > ", inet_ntop(AF_INET6, &ipv6->src.u8, buf, sizeof(buf))); |
| printf("%s: ", inet_ntop(AF_INET6, &ipv6->dst.u8, buf, sizeof(buf))); |
| printf("%s, ", protocol_to_string(ipv6->next_header)); |
| printf("length %u, ", ntohs(ipv6->length)); |
| transport_packet = reinterpret_cast<void*>(ipv6 + 1); |
| transport_protocol = ipv6->next_header; |
| } else { |
| printf("IP Version Unknown (or unhandled)"); |
| } |
| |
| if (transport_packet != NULL) { |
| if (transport_protocol == IPPROTO_TCP) { |
| struct tcphdr* tcp = static_cast<struct tcphdr*>(transport_packet); |
| printf("Ports "); |
| print_port(ntohs(tcp->source), options.verbose_level); |
| printf("> "); |
| print_port(ntohs(tcp->dest), options.verbose_level); |
| } else if (transport_protocol == IPPROTO_UDP) { |
| struct udphdr* udp = static_cast<struct udphdr*>(transport_packet); |
| printf("Ports "); |
| print_port(ntohs(udp->uh_sport), options.verbose_level); |
| printf("> "); |
| print_port(ntohs(udp->uh_dport), options.verbose_level); |
| } else { |
| printf("Transport Version Unknown (or unhandled)"); |
| } |
| } |
| |
| printf("\n"); |
| } |
| |
| int write_shb(int fd) { |
| if (fd == -1) { |
| return 0; |
| } |
| pcap_shb_t shb = { |
| .type = 0x0A0D0D0A, |
| .blk_tot_len = sizeof(pcap_shb_t), |
| .magic = 0x1A2B3C4D, |
| .major = 1, |
| .minor = 0, |
| .section_len = 0xFFFFFFFFFFFFFFFF, |
| .blk_tot_len2 = sizeof(pcap_shb_t), |
| }; |
| |
| if (write(fd, &shb, sizeof(shb)) != sizeof(shb)) { |
| fprintf(stderr, "Couldn't write PCAP Section Header block\n"); |
| return -1; |
| } |
| return 0; |
| } |
| |
| int write_idb(int fd) { |
| if (fd == -1) { |
| return 0; |
| } |
| pcap_idb_t idb = { |
| .type = 0x00000001, |
| .blk_tot_len = sizeof(pcap_idb_t), |
| .linktype = 1, |
| .reserved = 0, |
| // We can't use a zero here, but tcpdump also rejects 2^32 - 1. Try 2^16 - 1. |
| // See http://seclists.org/tcpdump/2012/q2/8. |
| .snaplen = 0xFFFF, |
| .blk_tot_len2 = sizeof(pcap_idb_t), |
| }; |
| |
| if (write(fd, &idb, sizeof(idb)) != sizeof(idb)) { |
| fprintf(stderr, "Couldn't write PCAP Interface Description Block\n"); |
| return -1; |
| } |
| |
| return 0; |
| } |
| |
| int write_packet(int fd, void* data, size_t len) { |
| if (fd == -1) { |
| return 0; |
| } |
| |
| size_t padded_len = ROUNDUP(len, 4); |
| simple_pkt_t pkt = { |
| .type = 0x00000003, |
| .blk_tot_len = static_cast<uint32_t>(SIMPLE_PKT_MIN_SIZE + padded_len), |
| .pkt_len = static_cast<uint32_t>(len), |
| }; |
| |
| // TODO(tkilbourn): rewrite this to offload writing to another thread, and also deal with |
| // partial writes |
| if (write(fd, &pkt, sizeof(pkt)) != sizeof(pkt)) { |
| fprintf(stderr, "Couldn't write packet header\n"); |
| return -1; |
| } |
| if (write(fd, data, len) != static_cast<ssize_t>(len)) { |
| fprintf(stderr, "Couldn't write packet\n"); |
| return -1; |
| } |
| if (padded_len > len) { |
| size_t padding = padded_len - len; |
| ZX_DEBUG_ASSERT(padding <= 3); |
| static const uint32_t zero = 0; |
| if (write(fd, &zero, padding) != static_cast<ssize_t>(padding)) { |
| fprintf(stderr, "Couldn't write padding\n"); |
| return -1; |
| } |
| } |
| if (write(fd, &pkt.blk_tot_len, sizeof(pkt.blk_tot_len)) != sizeof(pkt.blk_tot_len)) { |
| fprintf(stderr, "Couldn't write packet footer\n"); |
| return -1; |
| } |
| |
| return 0; |
| } |
| |
| void handle_rx(const zx::fifo& rx_fifo, char* iobuf, unsigned count, const netdump_options_t& options) { |
| eth_fifo_entry_t entries[count]; |
| |
| if (write_shb(options.dumpfile)) { |
| return; |
| } |
| if (write_idb(options.dumpfile)) { |
| return; |
| } |
| |
| for (;;) { |
| size_t n; |
| zx_status_t status; |
| if ((status = rx_fifo.read(sizeof(entries[0]), entries, countof(entries), &n)) < 0) { |
| if (status == ZX_ERR_SHOULD_WAIT) { |
| rx_fifo.wait_one(ZX_FIFO_READABLE | ZX_FIFO_PEER_CLOSED, zx::time::infinite(), nullptr); |
| continue; |
| } |
| fprintf(stderr, "netdump: failed to read rx packets: %d\n", status); |
| return; |
| } |
| |
| eth_fifo_entry_t* e = entries; |
| size_t packet_count = options.packet_count; |
| for (size_t i = 0; i < n; i++, e++) { |
| if (e->flags & ETH_FIFO_RX_OK) { |
| if (options.raw) { |
| printf("---\n"); |
| hexdump8_ex(iobuf + e->offset, e->length, 0); |
| } else { |
| parse_packet(iobuf + e->offset, e->length, options); |
| } |
| |
| if (write_packet(options.dumpfile, iobuf + e->offset, e->length)) { |
| return; |
| } |
| |
| packet_count--; |
| if (packet_count == 0) { |
| return; |
| } |
| } |
| |
| e->length = BUFSIZE; |
| e->flags = 0; |
| if ((status = rx_fifo.write(sizeof(*e), e, 1, NULL)) < 0) { |
| fprintf(stderr, "netdump: failed to queue rx packet: %d\n", status); |
| break; |
| } |
| } |
| } |
| } |
| |
| int usage() { |
| fprintf(stderr, "usage: netdump [ <option>* ] <network-device>\n"); |
| fprintf(stderr, " -w file : Write packet output to file in pcapng format\n"); |
| fprintf(stderr, " -c count: Exit after receiving count packets\n"); |
| fprintf(stderr, " -e : Print link-level header information\n"); |
| fprintf(stderr, " -p : Use promiscuous mode\n"); |
| fprintf(stderr, " -v : Print verbose output\n"); |
| fprintf(stderr, " -vv : Print extra verbose output\n"); |
| fprintf(stderr, " --raw : Print raw bytes of all incoming packets\n"); |
| fprintf(stderr, " --help : Show this help message\n"); |
| return -1; |
| } |
| |
| int parse_args(int argc, const char** argv, netdump_options_t& options) { |
| while (argc > 1) { |
| if (!strncmp(argv[0], "-c", strlen("-c"))) { |
| argv++; |
| argc--; |
| if (argc < 1) { |
| return usage(); |
| } |
| char* endptr; |
| options.packet_count = strtol(argv[0], &endptr, 10); |
| if (*endptr != '\0') { |
| return usage(); |
| } |
| argv++; |
| argc--; |
| } else if (!strcmp(argv[0], "-e")) { |
| argv++; |
| argc--; |
| options.link_level = true; |
| } else if (!strcmp(argv[0], "-p")) { |
| argv++; |
| argc--; |
| options.promisc = true; |
| } else if (!strcmp(argv[0], "-w")) { |
| argv++; |
| argc--; |
| if (argc < 1 || options.dumpfile != -1) { |
| return usage(); |
| } |
| options.dumpfile = open(argv[0], O_WRONLY | O_CREAT); |
| if (options.dumpfile < 0) { |
| fprintf(stderr, "Error: Could not output to file: %s\n", argv[0]); |
| return usage(); |
| } |
| argv++; |
| argc--; |
| } else if (!strcmp(argv[0], "-v")) { |
| argv++; |
| argc--; |
| options.verbose_level = 1; |
| } else if (!strncmp(argv[0], "-vv", sizeof("-vv"))) { |
| // Since this is the max verbosity, adding extra 'v's does nothing. |
| argv++; |
| argc--; |
| options.verbose_level = 2; |
| } else if (!strcmp(argv[0], "--raw")) { |
| argv++; |
| argc--; |
| options.raw = true; |
| } else { |
| return usage(); |
| } |
| } |
| |
| if (argc == 0) { |
| return usage(); |
| } else if (!strcmp(argv[0], "--help")) { |
| return usage(); |
| } |
| |
| options.device = argv[0]; |
| return 0; |
| } |
| |
| int main(int argc, const char** argv) { |
| netdump_options_t options; |
| memset(&options, 0, sizeof(options)); |
| options.dumpfile = -1; |
| if (parse_args(argc - 1, argv + 1, options)) { |
| return -1; |
| } |
| |
| int fd; |
| if ((fd = open(options.device, O_RDWR)) < 0) { |
| fprintf(stderr, "netdump: cannot open '%s'\n", options.device); |
| return -1; |
| } |
| |
| zx::channel svc; |
| zx_status_t status = fdio_get_service_handle(fd, svc.reset_and_get_address()); |
| if (status != ZX_OK) { |
| fprintf(stderr, "netdump: failed to get service handle\n"); |
| return -1; |
| } |
| |
| fuchsia_hardware_ethernet_Fifos fifos; |
| zx_status_t call_status = ZX_OK; |
| status = fuchsia_hardware_ethernet_DeviceGetFifos(svc.get(), &call_status, &fifos); |
| if (status != ZX_OK || call_status != ZX_OK) { |
| fprintf(stderr, "netdump: failed to get fifos: %d, %d\n", status, call_status); |
| return -1; |
| } |
| zx::fifo rx_fifo = zx::fifo(fifos.rx); |
| |
| unsigned count = fifos.rx_depth / 2; |
| zx::vmo iovmo; |
| // allocate shareable ethernet buffer data heap |
| if ((status = zx::vmo::create(count * BUFSIZE, ZX_VMO_NON_RESIZABLE, &iovmo)) < 0) { |
| return -1; |
| } |
| |
| char* iobuf; |
| if ((status = zx_vmar_map(zx_vmar_root_self(), |
| ZX_VM_PERM_READ | ZX_VM_PERM_WRITE, |
| 0, iovmo.get(), 0, count * BUFSIZE, reinterpret_cast<uintptr_t*>(&iobuf))) < 0) { |
| return -1; |
| } |
| |
| status = fuchsia_hardware_ethernet_DeviceSetIOBuffer(svc.get(), iovmo.get(), &call_status); |
| if (status != ZX_OK || call_status != ZX_OK) { |
| fprintf(stderr, "netdump: failed to set iobuf: %d, %d\n", status, call_status); |
| return -1; |
| } |
| |
| status = fuchsia_hardware_ethernet_DeviceSetClientName(svc.get(), "netdump", 7, &call_status); |
| if (status != ZX_OK || call_status != ZX_OK) { |
| fprintf(stderr, "netdump: failed to set client name %d, %d\n", status, call_status); |
| } |
| |
| if (options.promisc) { |
| status = fuchsia_hardware_ethernet_DeviceSetPromiscuousMode(svc.get(), true, &call_status); |
| if (status != ZX_OK || call_status != ZX_OK) { |
| fprintf(stderr, "netdump: failed to set promisc mode: %d, %d\n", status, call_status); |
| } |
| } |
| |
| // assign data chunks to ethbufs |
| for (unsigned n = 0; n < count; n++) { |
| eth_fifo_entry_t entry = { |
| .offset = static_cast<uint32_t>(n * BUFSIZE), |
| .length = BUFSIZE, |
| .flags = 0, |
| .cookie = 0, |
| }; |
| if ((status = zx_fifo_write(fifos.rx, sizeof(entry), &entry, 1, NULL)) < 0) { |
| fprintf(stderr, "netdump: failed to queue rx packet: %d\n", status); |
| return -1; |
| } |
| } |
| |
| status = fuchsia_hardware_ethernet_DeviceStart(svc.get(), &call_status); |
| if (status != ZX_OK || call_status != ZX_OK) { |
| fprintf(stderr, "netdump: failed to start network interface\n"); |
| return -1; |
| } |
| |
| status = fuchsia_hardware_ethernet_DeviceListenStart(svc.get(), &call_status); |
| if (status != ZX_OK || call_status != ZX_OK) { |
| fprintf(stderr, "netdump: failed to start listening\n"); |
| return -1; |
| } |
| |
| handle_rx(rx_fifo, iobuf, count, options); |
| |
| zx_handle_close(rx_fifo.get()); |
| if (options.dumpfile != -1) { |
| close(options.dumpfile); |
| } |
| return 0; |
| } |