/*
* 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 <strings.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <err.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/sockio.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>
#include <netinet/in_systm.h> /* legacy network types needed by ip_icmp.h */
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip6.h>
#include <netinet/ip_icmp.h>
#include <netinet/icmp6.h>
#include <net/if.h>
#include <arpa/inet.h>
#include <priv.h>
/*
* 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 %d)", 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);
}