/* * igmp_snoopd: an IGMP Snooping Daemon. * Copyright (C) 2011 SPiDCOM Technologies * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "socket.h" #include #include #include #include #include #include #include #include "internal.h" #include "utils.h" /** * Attach a Linux Socket Filter to a socket. * \param sockfd the file descriptor of the socket. * \param filter the filter. * \param filter_size The size of the filter. * \return 0, on succes. * -1, on error. */ static int attach_socket_filter (int sockfd, struct sock_filter *filter, size_t filter_size) { struct sock_fprog fprog = { .filter = filter, .len = filter_size }; if (setsockopt (sockfd, SOL_SOCKET, SO_ATTACH_FILTER, (const void *) &fprog, (socklen_t) sizeof (struct sock_fprog)) == -1) { log_error ("setsockopt: %s", strerror (errno)); return -1; } return 0; } /** * Attach the IGMP Linux Socket Filter to the socket. * \param sockfd the file descriptor of the socket. * \param dominant_traffic_type the type of the dominant traffic. * \return 0, on success. * -1, on error. */ static int attach_igmp_socket_filter (int sockfd, traffic_type_t dominant_traffic_type) { /* A Linux Socket Filter is used to let the Kernel do the filtering on IGMP. * So our code will only receive IGMP packets. * For more info about the Filter, see the BPF man page. (The Linux Socket * Filter is derived from the Berkeley Packet Filter). */ #define OFFSET_ETHERTYPE (offsetof (struct ethhdr, h_proto)) #define OFFSET_IP_PROTOCOL \ (sizeof (struct ethhdr) + offsetof (struct iphdr, protocol)) /* This filter handles both VLAN tagged and untagged packets. * This filter performs slightly better on untagged traffic, because the * first check is done on ETH_P_IP. */ struct sock_filter filter__favor_untagged[] = { /* A is the accumulator of the state machine. * P is the Packet. */ /* A = P[OFFSET_ETHERTYPE] */ BPF_STMT (BPF_LD | BPF_H | BPF_ABS, OFFSET_ETHERTYPE), /* (A == ETH_P_IP) ? continue : goto vlan_check */ BPF_JUMP (BPF_JMP | BPF_JEQ | BPF_K, ETH_P_IP, 0, 2), /* A = P[OFFSET_IP_PROTOCOL] */ BPF_STMT (BPF_LD | BPF_B | BPF_ABS, OFFSET_IP_PROTOCOL), /* (A == IPPROTO_IGMP) ? goto accept : goto ignore */ BPF_JUMP (BPF_JMP | BPF_JEQ | BPF_K, IPPROTO_IGMP, 5, 6), /* vlan_check: (A == ETH_P_8021Q) ? continue : goto ignore */ BPF_JUMP (BPF_JMP | BPF_JEQ | BPF_K, ETH_P_8021Q, 0, 5), /* A = P[OFFSET_ETHERTYPE + VLAN_HEADER_LEN] */ BPF_STMT (BPF_LD | BPF_H | BPF_ABS, OFFSET_ETHERTYPE + VLAN_HEADER_LEN), /* (A == ETH_P_IP) ? continue : goto ignore */ BPF_JUMP (BPF_JMP | BPF_JEQ | BPF_K, ETH_P_IP, 0, 3), /* A = P[OFFSET_IP_PROTOCOL + VLAN_HEADER_LEN] */ BPF_STMT (BPF_LD | BPF_B | BPF_ABS, OFFSET_IP_PROTOCOL + VLAN_HEADER_LEN), /* (A == IPPROTO_IGMP) ? goto accept : goto ignore */ BPF_JUMP (BPF_JMP | BPF_JEQ | BPF_K, IPPROTO_IGMP, 0, 1), /* accept: */ /* Pass this packet to our code */ BPF_STMT (BPF_RET | BPF_K, MSG_LEN), /* ignore: */ /* Don't pass this packet to our code */ BPF_STMT (BPF_RET | BPF_K, 0) }; /* This filter handles both VLAN tagged and untagged packets. * This filter performs slightly better on VLAN tagged traffic, because the * first check is done on ETH_P_8021Q. */ struct sock_filter filter__favor_vlan_tagged[] = { /* A is the accumulator of the state machine. * P is the Packet. */ /* A = P[OFFSET_ETHERTYPE] */ BPF_STMT (BPF_LD | BPF_H | BPF_ABS, OFFSET_ETHERTYPE), /* (A == ETH_P_8021Q) ? continue : goto untagged_check */ BPF_JUMP (BPF_JMP | BPF_JEQ | BPF_K, ETH_P_8021Q, 0, 4), /* A = P[OFFSET_ETHERTYPE + VLAN_HEADER_LEN] */ BPF_STMT (BPF_LD | BPF_H | BPF_ABS, OFFSET_ETHERTYPE + VLAN_HEADER_LEN), /* (A == ETH_P_IP) ? continue : goto ignore */ BPF_JUMP (BPF_JMP | BPF_JEQ | BPF_K, ETH_P_IP, 0, 6), /* A = P[OFFSET_IP_PROTOCOL + VLAN_HEADER_LEN] */ BPF_STMT (BPF_LD | BPF_B | BPF_ABS, OFFSET_IP_PROTOCOL + VLAN_HEADER_LEN), /* (A == IPPROTO_IGMP) ? goto accept : goto ignore */ BPF_JUMP (BPF_JMP | BPF_JEQ | BPF_K, IPPROTO_IGMP, 3, 4), /* untagged_check: (A == ETH_P_IP) ? continue : goto ignore */ BPF_JUMP (BPF_JMP | BPF_JEQ | BPF_K, ETH_P_IP, 0, 3), /* A = P[OFFSET_IP_PROTOCOL] */ BPF_STMT (BPF_LD | BPF_B | BPF_ABS, OFFSET_IP_PROTOCOL), /* (A == IPPROTO_IGMP) ? goto accept : goto ignore */ BPF_JUMP (BPF_JMP | BPF_JEQ | BPF_K, IPPROTO_IGMP, 0, 1), /* accept: */ /* Pass this packet to our code */ BPF_STMT (BPF_RET | BPF_K, MSG_LEN), /* ignore: */ /* Don't pass this packet to our code */ BPF_STMT (BPF_RET | BPF_K, 0) }; struct sock_filter *filter; size_t filter_size; switch (dominant_traffic_type) { case TRAFFIC_TYPE_UNTAGGED: filter = filter__favor_untagged; filter_size = sizeof (filter__favor_untagged)/sizeof (struct sock_filter); break; case TRAFFIC_TYPE_VLAN_TAGGED: filter = filter__favor_vlan_tagged; filter_size = sizeof (filter__favor_vlan_tagged)/sizeof (struct sock_filter); break; default: log_error ("unexpected value for dominant_traffic_type"); return -1; } return attach_socket_filter (sockfd, filter, filter_size); } /** * Empty the reception queue of the socket. * \param sockfd the socket file descriptor. * \return 0, on success. * -1, on error. */ static int empty_socket_rx_queue (int sockfd) { /* First block reception on the socket by attaching a filter that rejects * all packets. Then, consume the packets to empty the queue. * * This technique was found in libpcap (pcap-linux.c). */ /* This filter rejects all packets */ struct sock_filter filter__reject_all [] = { BPF_STMT (BPF_RET | BPF_K, 0) }; size_t filter_size = sizeof (filter__reject_all)/sizeof (struct sock_filter); if (attach_socket_filter (sockfd, filter__reject_all, filter_size) == -1) { log_error ("Error while installing (reject all) socket filter"); return -1; } /* Consume all packets that arrived between creating the socket and attaching * the (reject all) filter. */ unsigned char buffer[ETH_ALEN]; ssize_t ret; /* This recv is non blocking thanks to MSG_DONTWAIT */ while ((ret = recv (sockfd, buffer, sizeof (buffer), MSG_DONTWAIT)) > 0) { /* Nothing */ } /* Check that we exited the loop because there are no more packets left */ if ((ret != -1) || ((errno != EAGAIN) && (errno != EWOULDBLOCK))) { log_error ("empty_socket_rx_queue - recv: %s", strerror (errno)); return -1; } return 0; } int open_rx_socket_and_attach_filter (const char *ifname, traffic_type_t dominant_traffic_type) { /* We want the MAC addresses, so AF_PACKET and SOCK_RAW * Although we only want the IP Packets, we set ETH_P_ALL here * and let the Linux Socket Filter do the filtering on IP and IGMP */ int domain = AF_PACKET; int protocol = htons (ETH_P_ALL); int sockfd = socket (domain, SOCK_RAW, protocol); if (sockfd == -1) { log_error ("socket: %s", strerror (errno)); return -1; } /* Before attaching the filter for IGMP packets, we need to first remove any * non-IGMP packets that may have been received since creating the socket. * Attaching the IGMP filter will not prevent the kernel from passing us * these non IGMP packets. */ if (empty_socket_rx_queue (sockfd) == -1) { return -1; } if (attach_igmp_socket_filter (sockfd, dominant_traffic_type) == -1) { log_error ("Error while installing (IGMP) socket filter"); return -1; } struct sockaddr_ll sock_addr; sock_addr.sll_family = domain; sock_addr.sll_protocol = protocol; sock_addr.sll_ifindex = get_ifindex (sockfd, ifname); if (bind (sockfd, (struct sockaddr *) &sock_addr, sizeof (sock_addr)) == -1) { log_error ("bind: %s", strerror (errno)); return -1; } return sockfd; } int open_tx_socket (const char *ifname) { int sockfd = socket (AF_INET, SOCK_RAW, IPPROTO_IGMP); if (sockfd == -1) { log_error ("socket: %s", strerror (errno)); return -1; } /* bind */ if (setsockopt (sockfd, SOL_SOCKET, SO_BINDTODEVICE, ifname, strlen (ifname) + 1) == -1) { log_error ("setsockopt: %s", strerror (errno)); return -1; } return sockfd; }