/* * This file and its contents are supplied under the terms of the * Common Development and Distribution License ("CDDL"), version 1.0. * You may only use this file in accordance with the terms of version * 1.0 of the CDDL. * * A full copy of the text of the CDDL should have accompanied this * source. A copy of the CDDL is also available via the Internet at * http://www.illumos.org/license/CDDL. */ /* * Copyright 2015 Joyent, Inc. All rights reserved. */ #include #include #include #include #include #include #include #include #include #include #include #include /* legacy network types needed by ip_icmp.h */ #include #include #include #include #include #include #include #include /* * This program is meant to test the behaviour of processing incoming Router * Advertisements when IP spoofing protection (ip-nospoof) is enabled. When * run, it creates an etherstub on which it places two VNICs: a source VNIC, * and a destination VNIC with protection enabled. It then sends out spoofed * Router Advertisements with varying incorrect values. * * IMPORTANT: These tests expect that there is no other IPv6 traffic on the * machine that would be delivered to a VNIC with spoofing protection enabled, * since this would trip the DTrace probes installed by this suite of tests. * Care should therefore be taken to not run it as a part of any series of * tests which may be executed in such an environment, as it could lead to * spurious failures. */ #define DLADM(args...) spoof_run_proc("/usr/sbin/dladm", \ (char *[]) { "dladm", args, NULL }) #define IFCONFIG(args...) spoof_run_proc("/usr/sbin/ifconfig", \ (char *[]) { "ifconfig", args, NULL }) typedef struct sockaddr_in6 sin6_t; typedef int (spoof_test_f)(int, struct lif_nd_req *, sin6_t *); /* * Get the link-layer address of the given interface by querying * the neighbour cache. */ static int spoof_get_lla(int s, const char *iface, struct lifreq *addrp, struct lifreq *llap) { if (strstr(iface, ":")) { warnx("Specified interface should be the zeroth " "logical interface on the physical device."); } bzero(addrp, sizeof (*addrp)); bzero(llap, sizeof (*llap)); (void) strlcpy(addrp->lifr_name, iface, LIFNAMSIZ); if (ioctl(s, SIOCGLIFADDR, addrp) < 0) { warn("Unable to get link-local address"); return (-1); } (void) strlcpy(llap->lifr_name, iface, LIFNAMSIZ); bcopy(&addrp->lifr_addr, &llap->lifr_nd.lnr_addr, sizeof (struct sockaddr_storage)); if (ioctl(s, SIOCLIFGETND, llap) < 0) { warn("Failed to get link-layer address"); return (-1); } return (0); } static void spoof_prepare_lla(struct nd_opt_lla *llap, struct lif_nd_req *nce, struct iovec *iov) { uint_t optlen; bzero(llap, sizeof (*llap)); llap->nd_opt_lla_type = ND_OPT_SOURCE_LINKADDR; optlen = ((sizeof (struct nd_opt_hdr) + nce->lnr_hdw_len + 7) / 8) * 8; llap->nd_opt_lla_len = optlen / 8; bcopy(&nce->lnr_hdw_addr, &llap->nd_opt_lla_hdw_addr, nce->lnr_hdw_len); iov->iov_base = (caddr_t)llap; iov->iov_len = optlen; } static void spoof_prepare_pi(const char *prefix, int prefix_len, struct nd_opt_prefix_info *pip, struct iovec *iov) { bzero(pip, sizeof (*pip)); pip->nd_opt_pi_type = ND_OPT_PREFIX_INFORMATION; pip->nd_opt_pi_len = 4; pip->nd_opt_pi_prefix_len = prefix_len; pip->nd_opt_pi_flags_reserved = ND_OPT_PI_FLAG_AUTO | ND_OPT_PI_FLAG_ONLINK; pip->nd_opt_pi_valid_time = 86400; pip->nd_opt_pi_preferred_time = 86400; if (inet_pton(AF_INET6, prefix, &pip->nd_opt_pi_prefix) == 0) { errx(EXIT_FAILURE, "The prefix \"%s\" is " "not a valid input prefix", prefix); } iov->iov_base = (caddr_t)pip; iov->iov_len = sizeof (*pip); } static void spoof_prepare_header(struct nd_router_advert *ichdrp, struct iovec *iov) { bzero(ichdrp, sizeof (*ichdrp)); ichdrp->nd_ra_type = ND_ROUTER_ADVERT; ichdrp->nd_ra_curhoplimit = 0; iov->iov_base = (caddr_t)ichdrp; iov->iov_len = sizeof (*ichdrp); } static int spoof_set_max_hops(int s) { int ttl = 255; if (setsockopt(s, IPPROTO_IPV6, IPV6_UNICAST_HOPS, (char *)&ttl, sizeof (ttl)) < 0) { warn("Failed to set IPV6_UNICAST_HOPS socket option"); return (-1); } if (setsockopt(s, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, (char *)&ttl, sizeof (ttl)) < 0) { warn("Failed to set IPV6_UNICAST_HOPS socket option"); return (-1); } return (0); } /* * Send bad option lengths in the Link-Layer Source Address option */ static int spoof_bad_lla_optlen_test(int s, struct lif_nd_req *nce, sin6_t *multicast) { struct msghdr msg6; struct iovec iovs[3]; struct nd_router_advert ichdr; struct nd_opt_lla lla; struct nd_opt_prefix_info pi; uint8_t old_lla_len; spoof_prepare_header(&ichdr, &iovs[0]); spoof_prepare_lla(&lla, nce, &iovs[1]); spoof_prepare_pi("fd00::", 64, &pi, &iovs[2]); /* Prepare message */ bzero(&msg6, sizeof (struct msghdr)); msg6.msg_name = multicast; msg6.msg_namelen = sizeof (sin6_t); msg6.msg_iov = iovs; msg6.msg_iovlen = 3; old_lla_len = lla.nd_opt_lla_len; /* * Length is now smaller than the option is, so this should * be rejected. */ lla.nd_opt_lla_len = 0; if (sendmsg(s, &msg6, 0) < 0) { warn("Failed to send ICMPv6 message"); return (-1); } /* * Length is bigger than the option, so the following prefix * will be offset. */ lla.nd_opt_lla_len = 2; if (sendmsg(s, &msg6, 0) < 0) { warn("Failed to send ICMPv6 message"); return (-1); } /* * Restore the length, but shorten the amount of data to send, so we're * sending truncated packets. (Stop before 0, so that we still send part * of the option.) */ lla.nd_opt_lla_len = old_lla_len; for (iovs[1].iov_len--; iovs[1].iov_len > 0; iovs[1].iov_len--) { if (sendmsg(s, &msg6, 0) < 0) { warn("Failed to send ICMPv6 message"); return (-1); } } return (0); } /* * Send bad option lengths in the Prefix Information option */ static int spoof_bad_pi_optlen_test(int s, struct lif_nd_req *nce, sin6_t *multicast) { struct msghdr msg6; struct iovec iovs[3]; struct nd_router_advert ichdr; struct nd_opt_lla lla; struct nd_opt_prefix_info pi; uint8_t old_pi_len; spoof_prepare_header(&ichdr, &iovs[0]); spoof_prepare_lla(&lla, nce, &iovs[1]); spoof_prepare_pi("fd00::", 64, &pi, &iovs[2]); /* Prepare message */ bzero(&msg6, sizeof (struct msghdr)); msg6.msg_name = multicast; msg6.msg_namelen = sizeof (sin6_t); msg6.msg_iov = iovs; msg6.msg_iovlen = 3; old_pi_len = pi.nd_opt_pi_len; /* * Length is now smaller than the option is, so this should * be rejected. */ pi.nd_opt_pi_len = 0; if (sendmsg(s, &msg6, 0) < 0) { warn("Failed to send ICMPv6 message"); return (-1); } /* * Length is smaller than a PI option should be. */ pi.nd_opt_pi_len = 3; if (sendmsg(s, &msg6, 0) < 0) { warn("Failed to send ICMPv6 message"); return (-1); } /* * Length is bigger than the option, so the following prefix * will be offset. */ pi.nd_opt_pi_len = 5; if (sendmsg(s, &msg6, 0) < 0) { warn("Failed to send ICMPv6 message"); return (-1); } /* * Restore the length, but shorten the amount of data to send, so we're * sending truncated packets. (Stop before 0, so that we still send part * of the option.) */ pi.nd_opt_pi_len = old_pi_len; for (iovs[2].iov_len--; iovs[2].iov_len > 0; iovs[2].iov_len--) { if (sendmsg(s, &msg6, 0) < 0) { warn("Failed to send ICMPv6 message"); return (-1); } } return (0); } /* * Advertise a prefix with a prefix length greater than 128. */ static int spoof_bad_plen_test(int s, struct lif_nd_req *nce, sin6_t *multicast) { struct msghdr msg6; struct iovec iovs[3]; struct nd_router_advert ichdr; struct nd_opt_lla lla; struct nd_opt_prefix_info pi; spoof_prepare_header(&ichdr, &iovs[0]); spoof_prepare_lla(&lla, nce, &iovs[1]); spoof_prepare_pi("fd00::", 130, &pi, &iovs[2]); /* Prepare message */ bzero(&msg6, sizeof (struct msghdr)); msg6.msg_name = multicast; msg6.msg_namelen = sizeof (sin6_t); msg6.msg_iov = iovs; msg6.msg_iovlen = 3; if (sendmsg(s, &msg6, 0) < 0) { warn("Failed to send ICMPv6 message"); return (-1); } return (0); } /* * Advertise a link-local prefix, which should be disallowed and ignored. */ static int spoof_link_local_test(int s, struct lif_nd_req *nce, sin6_t *multicast) { struct msghdr msg6; struct iovec iovs[3]; struct nd_router_advert ichdr; struct nd_opt_lla lla; struct nd_opt_prefix_info pi; spoof_prepare_header(&ichdr, &iovs[0]); spoof_prepare_lla(&lla, nce, &iovs[1]); spoof_prepare_pi("fe80::", 64, &pi, &iovs[2]); /* Prepare message */ bzero(&msg6, sizeof (struct msghdr)); msg6.msg_name = multicast; msg6.msg_namelen = sizeof (sin6_t); msg6.msg_iov = iovs; msg6.msg_iovlen = 3; if (sendmsg(s, &msg6, 0) < 0) { warn("Failed to send ICMPv6 message"); return (-1); } return (0); } static int spoof_good_test(int s, struct lif_nd_req *nce, sin6_t *multicast) { struct msghdr msg6; struct iovec iovs[3]; struct nd_router_advert ichdr; struct nd_opt_lla lla; struct nd_opt_prefix_info pi; spoof_prepare_header(&ichdr, &iovs[0]); spoof_prepare_lla(&lla, nce, &iovs[1]); spoof_prepare_pi("fd00::", 64, &pi, &iovs[2]); /* Prepare message */ bzero(&msg6, sizeof (struct msghdr)); msg6.msg_name = multicast; msg6.msg_namelen = sizeof (sin6_t); msg6.msg_iov = iovs; msg6.msg_iovlen = 3; if (sendmsg(s, &msg6, 0) < 0) { warn("Failed to send ICMPv6 message"); return (-1); } return (0); } static spoof_test_f *test_cases[] = { spoof_bad_lla_optlen_test, spoof_bad_pi_optlen_test, spoof_bad_plen_test, spoof_link_local_test }; static int test_cases_count = sizeof (test_cases) / sizeof (spoof_test_f *); static pid_t spoof_dtrace_launch(void) { pid_t child_pid = fork(); if (child_pid == (pid_t)-1) { err(EXIT_FAILURE, "Failed to fork to execute dtrace"); } else if (child_pid == (pid_t)0) { (void) execl("/usr/sbin/dtrace", "dtrace", "-q", "-n", "sdt:mac:insert_slaac_ip:generated-addr { exit(10) }", NULL); err(EXIT_FAILURE, "Failed to execute dtrace"); } return (child_pid); } static pid_t spoof_dtrace_wait(pid_t dtrace, int *stat) { int retpid; /* Give time for probe to fire before checking status */ (void) sleep(5); while ((retpid = waitpid(dtrace, stat, WNOHANG)) == -1) { if (errno == EINTR) continue; err(EXIT_FAILURE, "Failed to wait on child"); } return (retpid); } /* * Run a function that's going to exec in a child process, and don't return * until it exits. */ static int spoof_run_proc(char *path, char *args[]) { pid_t child_pid; int childstat = 0, status = 0; child_pid = fork(); if (child_pid == (pid_t)-1) { err(EXIT_FAILURE, "Unable to fork to execute %s", path); } else if (child_pid == (pid_t)0) { (void) execv(path, args); err(EXIT_FAILURE, "Failed to execute %s", path); } while (waitpid(child_pid, &childstat, 0) == -1) { if (errno == EINTR) continue; warn("Failed to wait on child"); return (-1); } status = WEXITSTATUS(childstat); if (status != 0) { warnx("Child process %s exited with %d", path, status); return (-1); } return (0); } static void spoof_network_teardown(char *testether, char *testvnic0, char *testvnic1) { // Delete dest vnic (void) IFCONFIG(testvnic1, "inet6", "unplumb"); (void) DLADM("delete-vnic", testvnic1); // Delete source vnic (void) IFCONFIG(testvnic0, "inet6", "unplumb"); (void) DLADM("delete-vnic", testvnic0); // Delete etherstub (void) DLADM("delete-etherstub", testether); } static int spoof_network_setup(char *testether, char *testvnic0, char *testvnic1) { // Create etherstub if (DLADM("create-etherstub", "-t", testether) != 0) { warnx("Failed to create etherstub for test network"); return (-1); } // Create source vnic if (DLADM("create-vnic", "-t", "-l", testether, testvnic0) != 0) { warnx("Failed to create source VNIC for test network"); return (-1); } if (IFCONFIG(testvnic0, "inet6", "plumb", "up") != 0) { warnx("Failed to plumb source VNIC for test network"); return (-1); } // Create dest vnic if (DLADM("create-vnic", "-t", "-l", testether, "-p", "protection=mac-nospoof,restricted,ip-nospoof,dhcp-nospoof", testvnic1) != 0) { warnx("Failed to create destination VNIC for test network"); return (-1); } if (IFCONFIG(testvnic1, "inet6", "plumb", "up") != 0) { warnx("Failed to plumb destination VNIC for test network"); return (-1); } return (0); } static void spoof_run_test(spoof_test_f *func, int s, struct lif_nd_req *nce, sin6_t *multicast) { static int cas = 1; (void) printf("Executing test case #%d...", cas++); if (func(s, nce, multicast) == 0) { (void) printf(" Done.\n"); } else { (void) printf(" Error while running!\n"); } } static int spoof_run_tests(int s, struct lif_nd_req *nce) { int cas, stat; pid_t dtrace; sin6_t multicast; /* Prepare all-nodes multicast address */ bzero(&multicast, sizeof (multicast)); multicast.sin6_family = AF_INET6; (void) inet_pton(AF_INET6, "ff02::1", &multicast.sin6_addr); dtrace = spoof_dtrace_launch(); /* Wait an adequate amount of time for the probes to be installed */ (void) sleep(5); /* * We send a packet where everything is good, except for the hop limit. * This packet should be rejected. */ spoof_run_test(spoof_good_test, s, nce, &multicast); if (spoof_set_max_hops(s) != 0) { warnx("Failed to set hop limit on socket"); return (EXIT_FAILURE); } for (cas = 0; cas < test_cases_count; cas++) { spoof_run_test(test_cases[cas], s, nce, &multicast); } if (spoof_dtrace_wait(dtrace, &stat) != 0) { (void) printf("One or more tests of bad behaviour failed!\n"); return (EXIT_FAILURE); } /* * Now that we've executed all of the test cases that should fail, we * can execute the test that should succeed, to make sure the normal * case works properly. This should trip the dtrace probe. */ spoof_run_test(spoof_good_test, s, nce, &multicast); if (spoof_dtrace_wait(dtrace, &stat) != 0 && WIFEXITED(stat) && WEXITSTATUS(stat) == 10) { (void) printf("Tests completed successfully!\n"); } else { if (kill(dtrace, SIGKILL) != 0) { warn("Failed to kill dtrace child (pid %" _PRIdID ")", dtrace); } (void) printf("Test of normal behaviour didn't succeed!\n"); return (EXIT_FAILURE); } return (0); } /* * Make sure that we have all of the privileges we need to execute these tests, * so that we can error out before we would fail. */ void spoof_check_privs(void) { priv_set_t *privset = priv_allocset(); if (privset == NULL) { err(EXIT_FAILURE, "Failed to allocate memory for " "checking privileges"); } if (getppriv(PRIV_EFFECTIVE, privset) != 0) { err(EXIT_FAILURE, "Failed to get current privileges"); } if (!priv_ismember(privset, PRIV_DTRACE_KERNEL)) { errx(EXIT_FAILURE, "These tests need to be run as a user " "capable of tracing the kernel."); } if (!priv_ismember(privset, PRIV_SYS_NET_CONFIG)) { errx(EXIT_FAILURE, "These tests need to be run as a user " "capable of creating and configuring network interfaces."); } if (!priv_ismember(privset, PRIV_NET_ICMPACCESS)) { errx(EXIT_FAILURE, "These tests need to be run as a user " "capable of sending ICMP packets."); } priv_freeset(privset); } int main(void) { struct lifreq addr, llar; int error, s; char testether[LIFNAMSIZ]; char testvnic0[LIFNAMSIZ]; char testvnic1[LIFNAMSIZ]; pid_t curpid = getpid(); spoof_check_privs(); /* * Set up the socket and test network for sending */ s = socket(PF_INET6, SOCK_RAW, IPPROTO_ICMPV6); if (s < 0) { err(EXIT_FAILURE, "Failed to open ICMPv6 socket"); } (void) snprintf(testether, sizeof (testether), "testether%d", curpid); (void) snprintf(testvnic0, sizeof (testvnic0), "testvnic%d_0", curpid); (void) snprintf(testvnic1, sizeof (testvnic1), "testvnic%d_1", curpid); if (spoof_network_setup(testether, testvnic0, testvnic1) != 0) { warnx("Failed to set up test network"); error = EXIT_FAILURE; goto cleanup; } if (spoof_get_lla(s, testvnic0, &addr, &llar) != 0) { warnx("Failed to get link-layer address"); error = EXIT_FAILURE; goto cleanup; } if (setsockopt(s, IPPROTO_IPV6, IPV6_BOUND_IF, (char *)&((sin6_t *)&addr.lifr_addr)->sin6_scope_id, sizeof (int)) < 0) { warn("Failed to set IPV6_UNICAST_HOPS socket option"); return (-1); } if (bind(s, (struct sockaddr *)&addr.lifr_addr, sizeof (sin6_t)) != 0) { warnx("Failed to bind to link-local address"); error = EXIT_FAILURE; goto cleanup; } error = spoof_run_tests(s, &llar.lifr_nd); cleanup: if (close(s) != 0) { warnx("Failed to close ICMPv6 socket"); } spoof_network_teardown(testether, testvnic0, testvnic1); return (error); }