/* * Copyright (c) 2009-2012 Apple Inc. All rights reserved. * * @APPLE_LICENSE_HEADER_START@ * * This file contains Original Code and/or Modifications of Original Code * as defined in and that are subject to the Apple Public Source License * Version 2.0 (the 'License'). You may not use this file except in * compliance with the License. Please obtain a copy of the License at * http://www.opensource.apple.com/apsl/ and read it before using this * file. * * The Original Code and all software distributed under the License are * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. * Please see the License for the specific language governing rights and * limitations under the License. * * @APPLE_LICENSE_HEADER_END@ */ /* * DHCPv6Socket.c * - maintain list of DHCPv6 client "sockets" * - distribute packet reception to enabled "sockets" */ /* * Modification History * * September 30, 2009 Dieter Siegmund (dieter@apple.com) * - created (based on bootp_session.c) */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "DHCPv6.h" #include "DHCPv6Socket.h" #include "DHCPv6Options.h" #include "util.h" #include #include #include "interfaces.h" #include "FDSet.h" #include "dynarray.h" #include "ipconfigd_globals.h" #include "symbol_scope.h" #include "timer.h" #include "IPv6Sock_Compat.h" typedef struct DHCPv6SocketGlobals { dynarray_t sockets; FDCalloutRef read_fd; int read_fd_refcount; timer_callout_t * timer_callout; } DHCPv6SocketGlobals, * DHCPv6SocketGlobalsRef; struct DHCPv6Socket { interface_t * if_p; boolean_t fd_open; DHCPv6SocketReceiveFuncPtr receive_func; void * receive_arg1; void * receive_arg2; }; STATIC FILE * S_log_file; STATIC uint16_t S_client_port = DHCPV6_CLIENT_PORT; STATIC uint16_t S_server_port = DHCPV6_SERVER_PORT; STATIC DHCPv6SocketGlobalsRef S_globals; STATIC const struct sockaddr_in6 dhcpv6_all_servers_and_relay_agents = { sizeof(dhcpv6_all_servers_and_relay_agents), AF_INET6, 0, 0, All_DHCP_Relay_Agents_and_Servers_INIT, 0 }; STATIC int open_dhcpv6_socket(uint16_t client_port) { struct sockaddr_in6 me; int opt = 1; int sockfd; sockfd = socket(AF_INET6, SOCK_DGRAM, 0); if (sockfd < 0) { my_log(LOG_ERR, "DHCPv6Socket: socket failed, %m"); return (sockfd); } bzero(&me, sizeof(me)); me.sin6_family = AF_INET6; me.sin6_port = htons(client_port); if (bind(sockfd, (struct sockaddr *)&me, sizeof(me)) != 0) { my_log(LOG_ERR, "DHCPv6Socket: bind failed, %m"); goto failed; } /* set non-blocking I/O */ if (ioctl(sockfd, FIONBIO, &opt) < 0) { my_log(LOG_ERR, "DHCPv6Socket: ioctl FIONBIO failed, %m"); goto failed; } /* ask for packet info */ if (setsockopt(sockfd, IPPROTO_IPV6, IPCONFIG_SOCKOPT_PKTINFO, &opt, sizeof(opt)) < 0) { my_log(LOG_ERR, "DHCPv6Socket: setsockopt(IPV6_PKTINFO) failed, %m"); goto failed; } #ifdef SO_TC_CTL /* set the traffic class */ opt = SO_TC_CTL; /* set traffic class, we don't care if it failed. */ (void)setsockopt(sockfd, SOL_SOCKET, SO_TRAFFIC_CLASS, &opt, sizeof(opt)); #endif /* SO_TC_CTL */ return (sockfd); failed: close(sockfd); return (-1); } STATIC void DHCPv6SocketFreeElement(void * arg); STATIC DHCPv6SocketGlobalsRef DHCPv6SocketCreateGlobals(void) { DHCPv6SocketGlobalsRef globals = malloc(sizeof(*globals)); if (globals == NULL) { return (NULL); } bzero(globals, sizeof(*globals)); dynarray_init(&globals->sockets, DHCPv6SocketFreeElement, NULL); globals->timer_callout = timer_callout_init(); return (globals); } STATIC void DHCPv6SocketReleaseGlobals(DHCPv6SocketGlobalsRef * globals_p) { DHCPv6SocketGlobalsRef globals; if (globals_p == NULL) { return; } globals = *globals_p; if (globals == NULL) { return; } *globals_p = NULL; dynarray_free(&globals->sockets); FDCalloutRelease(&globals->read_fd); timer_callout_free(&globals->timer_callout); bzero(globals, sizeof(*globals)); free(globals); return; } STATIC DHCPv6SocketGlobalsRef DHCPv6SocketGetGlobals(void) { if (S_globals != NULL) { return (S_globals); } S_globals = DHCPv6SocketCreateGlobals(); return (S_globals); } static void DHCPv6SocketDelayedClose(void * arg1, void * arg2, void * arg3) { if (S_globals->read_fd == NULL) { my_log(LOG_ERR, "DHCPv6SocketDelayedClose(): socket is already closed"); return; } if (S_globals->read_fd_refcount > 0) { my_log(LOG_ERR, "DHCPv6SocketDelayedClose(): called when socket in use"); return; } my_log(LOG_DEBUG, "DHCPv6SocketDelayedClose(): closing DHCPv6 socket %d", FDCalloutGetFD(S_globals->read_fd)); /* this closes the file descriptor */ FDCalloutRelease(&S_globals->read_fd); return; } static void DHCPv6SocketDemux(int if_index, const DHCPv6PacketRef pkt, int pkt_len) { DHCPv6SocketReceiveData data; DHCPv6OptionErrorString err; int i; if (pkt_len < DHCPV6_PACKET_HEADER_LENGTH) { return; } data.pkt = pkt; data.pkt_len = pkt_len; data.options = DHCPv6OptionListCreateWithPacket(pkt, pkt_len, &err); if (data.options == NULL) { my_log(LOG_NOTICE, "DHCPv6Socket: options parse failed, %s", err.str); return; } for (i = 0; i < dynarray_count(&S_globals->sockets); i++) { DHCPv6SocketRef client; client = dynarray_element(&S_globals->sockets, i); if (if_index != if_link_index(DHCPv6SocketGetInterface(client))) { continue; } if (S_log_file != NULL) { fprintf(S_log_file, "----------------------------\n"); timestamp_fprintf(S_log_file, "[%s] Receive\n", if_name(DHCPv6SocketGetInterface(client))); DHCPv6PacketFPrint(S_log_file, pkt, pkt_len); if (data.options != NULL) { DHCPv6OptionListFPrint(S_log_file, data.options); } fflush(S_log_file); } if (client->receive_func != NULL) { (*client->receive_func)(client->receive_arg1, client->receive_arg2, &data); } } DHCPv6OptionListRelease(&data.options); return; } void DHCPv6SocketSetLogFile(FILE * log_file) { S_log_file = log_file; return; } void DHCPv6SocketSetPorts(uint16_t client_port, uint16_t server_port) { S_client_port = client_port; S_server_port = server_port; return; } interface_t * DHCPv6SocketGetInterface(DHCPv6SocketRef sock) { return (sock->if_p); } DHCPv6SocketRef DHCPv6SocketCreate(interface_t * if_p) { DHCPv6SocketRef sock; DHCPv6SocketGlobalsRef globals; globals = DHCPv6SocketGetGlobals(); if (globals == NULL) { my_log(LOG_ERR, "DHCPv6SocketCreate: could not allocate globals"); return (NULL); } sock = malloc(sizeof(*sock)); if (sock == NULL) { return (NULL); } bzero(sock, sizeof(*sock)); if (dynarray_add(&globals->sockets, sock) == FALSE) { free(sock); return (NULL); } sock->if_p = if_p; return (sock); } STATIC void DHCPv6SocketFreeElement(void * arg) { DHCPv6SocketRef sock = (DHCPv6SocketRef)arg; DHCPv6SocketDisableReceive(sock); free(sock); return; } void DHCPv6SocketRelease(DHCPv6SocketRef * sock_p) { DHCPv6SocketRef sock = *sock_p; int i; if (sock == NULL) { return; } i = dynarray_index(&S_globals->sockets, sock); if (i != -1) { dynarray_remove(&S_globals->sockets, i, NULL); } else { my_log(LOG_ERR, "DHCPv6SocketRelease: %s not in list?", if_name(sock->if_p)); } DHCPv6SocketFreeElement(sock); *sock_p = NULL; if (dynarray_count(&S_globals->sockets) == 0) { DHCPv6SocketReleaseGlobals(&S_globals); } return; } STATIC void DHCPv6SocketCloseSocket(DHCPv6SocketRef sock) { if (sock->fd_open == FALSE) { return; } if (S_globals->read_fd_refcount <= 0) { my_log(LOG_ERR, "DHCPv6SocketCloseSocket(%s): refcount %d", if_name(sock->if_p), S_globals->read_fd_refcount); return; } S_globals->read_fd_refcount--; my_log(LOG_DEBUG, "DHCPv6SocketCloseSocket(%s): refcount %d", if_name(sock->if_p), S_globals->read_fd_refcount); sock->fd_open = FALSE; if (S_globals->read_fd_refcount == 0) { struct timeval tv; my_log(LOG_DEBUG, "DHCPv6SocketCloseSocket(): scheduling delayed close"); tv.tv_sec = 1; /* close it after 1 second of non-use */ tv.tv_usec = 0; timer_set_relative(S_globals->timer_callout, tv, DHCPv6SocketDelayedClose, NULL, NULL, NULL); } return; } static void DHCPv6SocketRead(void * arg1, void * arg2) { struct cmsghdr * cm; char cmsgbuf[CMSG_SPACE(sizeof(struct in6_pktinfo))]; struct iovec iov; struct sockaddr_in6 from; struct msghdr mhdr; int n; struct in6_pktinfo *pktinfo = NULL; char receive_buf[1500]; /* initialize msghdr for receiving packets */ iov.iov_base = (caddr_t)receive_buf; iov.iov_len = sizeof(receive_buf); mhdr.msg_name = (caddr_t)&from; mhdr.msg_namelen = sizeof(from); mhdr.msg_iov = &iov; mhdr.msg_iovlen = 1; mhdr.msg_control = (caddr_t)cmsgbuf; mhdr.msg_controllen = sizeof(cmsgbuf); /* get message */ n = recvmsg(FDCalloutGetFD(S_globals->read_fd), &mhdr, 0); if (n < 0) { if (errno != EAGAIN) { my_log(LOG_ERR, "DHCPv6SocketRead: recvfrom failed %s (%d)", strerror(errno), errno); } return; } if (n == 0) { /* nothing to read */ return; } /* get the control message that has the interface index */ pktinfo = NULL; for (cm = (struct cmsghdr *)CMSG_FIRSTHDR(&mhdr); cm != NULL; cm = (struct cmsghdr *)CMSG_NXTHDR(&mhdr, cm)) { if (cm->cmsg_level != IPPROTO_IPV6) { continue; } switch (cm->cmsg_type) { case IPV6_PKTINFO: if (cm->cmsg_len < CMSG_LEN(sizeof(struct in6_pktinfo))) { continue; } /* ALIGN: CMSG_DATA is should return aligned data */ pktinfo = (struct in6_pktinfo *)(void *)(CMSG_DATA(cm)); break; default: /* this should never occur */ my_log(LOG_ERR, "Why did we get control message type %d?", cm->cmsg_type); break; } } if (pktinfo == NULL) { my_log(LOG_ERR, "DHCPv6SocketRead: missing IPV6_PKTINFO"); return; } DHCPv6SocketDemux(pktinfo->ipi6_ifindex, (const DHCPv6PacketRef)receive_buf, n); return; } static boolean_t DHCPv6SocketOpenSocket(DHCPv6SocketRef sock) { if (sock->fd_open) { return (TRUE); } timer_cancel(S_globals->timer_callout); S_globals->read_fd_refcount++; my_log(LOG_DEBUG, "DHCPv6SocketOpenSocket (%s): refcount %d", if_name(sock->if_p), S_globals->read_fd_refcount); sock->fd_open = TRUE; if (S_globals->read_fd_refcount > 1) { /* already open */ return (TRUE); } if (S_globals->read_fd != NULL) { my_log(LOG_DEBUG, "DHCPv6SocketOpenSocket(): socket is still open"); } else { int sockfd; sockfd = open_dhcpv6_socket(S_client_port); if (sockfd < 0) { my_log(LOG_ERR, "DHCPv6SocketOpenSocket: socket() failed, %m"); goto failed; } my_log(LOG_DEBUG, "DHCPv6SocketOpenSocket(): opened DHCPv6 socket %d\n", sockfd); /* register as a reader */ S_globals->read_fd = FDCalloutCreate(sockfd, DHCPv6SocketRead, NULL, NULL); } return (TRUE); failed: DHCPv6SocketCloseSocket(sock); return (FALSE); } void DHCPv6SocketEnableReceive(DHCPv6SocketRef sock, DHCPv6SocketReceiveFuncPtr func, void * arg1, void * arg2) { sock->receive_func = func; sock->receive_arg1 = arg1; sock->receive_arg2 = arg2; if (DHCPv6SocketOpenSocket(sock) == FALSE) { my_log(LOG_ERR, "DHCPv6SocketEnableReceive(%s): failed", if_name(sock->if_p)); } return; } void DHCPv6SocketDisableReceive(DHCPv6SocketRef sock) { sock->receive_func = NULL; sock->receive_arg1 = NULL; sock->receive_arg2 = NULL; DHCPv6SocketCloseSocket(sock); return; } STATIC int S_send_packet(int sockfd, int ifindex, DHCPv6PacketRef pkt, int pkt_size) { struct cmsghdr * cm; char cmsgbuf[CMSG_SPACE(sizeof(struct in6_pktinfo))]; struct sockaddr_in6 dst; struct iovec iov; struct msghdr mhdr; int n; struct in6_pktinfo *pi; int ret; /* initialize msghdr for sending packets */ iov.iov_base = (caddr_t)pkt; iov.iov_len = pkt_size; dst = dhcpv6_all_servers_and_relay_agents; dst.sin6_port = htons(S_server_port); mhdr.msg_name = (caddr_t)&dst; mhdr.msg_namelen = sizeof(struct sockaddr_in6); mhdr.msg_flags = 0; mhdr.msg_iov = &iov; mhdr.msg_iovlen = 1; mhdr.msg_control = (caddr_t)cmsgbuf; mhdr.msg_controllen = sizeof(cmsgbuf); /* specify the outgoing interface */ bzero(cmsgbuf, sizeof(cmsgbuf)); cm = CMSG_FIRSTHDR(&mhdr); if (cm == NULL) { /* this can't happen, keep static analyzer happy */ return (EINVAL); } cm->cmsg_level = IPPROTO_IPV6; cm->cmsg_type = IPV6_PKTINFO; cm->cmsg_len = CMSG_LEN(sizeof(struct in6_pktinfo)); /* ALIGN: CMSG_DATA should return aligned data */ pi = (struct in6_pktinfo *)(void *)CMSG_DATA(cm); pi->ipi6_ifindex = ifindex; n = sendmsg(sockfd, &mhdr, 0); if (n != pkt_size) { ret = errno; } else { ret = 0; } return (ret); } int DHCPv6SocketTransmit(DHCPv6SocketRef sock, DHCPv6PacketRef pkt, int pkt_len) { DHCPv6OptionErrorString err; int ret; boolean_t needs_close = FALSE; if (sock->fd_open == FALSE) { /* open the DHCPv6 socket in case it's needed */ if (DHCPv6SocketOpenSocket(sock) == FALSE) { my_log(LOG_ERR, "DHCPv6Socket: failed to open socket\n"); return (FALSE); } needs_close = TRUE; } if (S_log_file != NULL) { DHCPv6OptionListRef options; fprintf(S_log_file, "============================\n"); timestamp_fprintf(S_log_file, "[%s] Transmit\n", if_name(sock->if_p)); DHCPv6PacketFPrint(S_log_file, pkt, pkt_len); options = DHCPv6OptionListCreateWithPacket(pkt, pkt_len, &err); if (options == NULL) { my_log(LOG_NOTICE, "DHCPv6SocketTransmit: parse options failed, %s", err.str); return (FALSE); } DHCPv6OptionListFPrint(S_log_file, options); DHCPv6OptionListRelease(&options); fflush(S_log_file); } ret = S_send_packet(FDCalloutGetFD(S_globals->read_fd), if_link_index(sock->if_p), pkt, pkt_len); if (needs_close) { DHCPv6SocketCloseSocket(sock); } return (ret); }