root/usr/src/test/os-tests/tests/spoof-ras/spoof-ras.c
/*
 * 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 %" _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);
}