mac_protect.c revision e03914f9208eb53e6c8a6d5a436953ad983642b0
/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License (the "License").
* You may not use this file except in compliance with the License.
*
* You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at usr/src/OPENSOLARIS.LICENSE.
* If applicable, add the following below this CDDL HEADER, with the
* fields enclosed by brackets "[]" replaced with your own identifying
* information: Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*/
/*
* Copyright (c) 2014, Joyent, Inc. All rights reserved.
*/
/*
* Copyright 2014 Nexenta Systems, Inc. All rights reserved.
*/
#include <sys/mac_impl.h>
#include <sys/mac_client_impl.h>
#include <sys/mac_client_priv.h>
#include <sys/ethernet.h>
/*
* Implementation overview for DHCP address detection
*
* The purpose of DHCP address detection is to relieve the user of having to
* manually configure static IP addresses when ip-nospoof protection is turned
* on. To achieve this, the mac layer needs to intercept DHCP packets to
* determine the assigned IP addresses.
*
* A DHCP handshake between client and server typically requires at least
* 4 messages:
*
* 1. DISCOVER - client attempts to locate DHCP servers via a
* broadcast message to its subnet.
* 2. OFFER - server responds to client with an IP address and
* other parameters.
* 3. REQUEST - client requests the offered address.
* 4. ACK - server verifies that the requested address matches
* the one it offered.
*
* DHCPv6 behaves pretty much the same way aside from different message names.
*
* Address information is embedded in either the OFFER or REQUEST message.
* We chose to intercept REQUEST because this is at the last part of the
* handshake and it indicates that the client intends to keep the address.
* Intercepting OFFERs is unreliable because the client may receive multiple
* offers from different servers, and we can't tell which address the client
* will keep.
*
* Each DHCP message has a transaction ID. We use this transaction ID to match
* REQUESTs with ACKs received from servers.
*
* For IPv4, the process to acquire a DHCP-assigned address is as follows:
*
* 1. Client sends REQUEST. a new dhcpv4_txn_t object is created and inserted
* in the the mci_v4_pending_txn table (keyed by xid). This object represents
* a new transaction. It contains the xid, the client ID and requested IP
* address.
*
* 2. Server responds with an ACK. The xid from this ACK is used to lookup the
* pending transaction from the mci_v4_pending_txn table. Once the object is
* found, it is removed from the pending table and inserted into the
* completed table (mci_v4_completed_txn, keyed by client ID) and the dynamic
* IP table (mci_v4_dyn_ip, keyed by IP address).
*
* 3. An outgoing packet that goes through the ip-nospoof path will be checked
* against the dynamic IP table. Packets that have the assigned DHCP address
* as the source IP address will pass the check and be admitted onto the
* network.
*
* IPv4 notes:
*
* If the server never responds with an ACK, there is a timer that is set after
* the insertion of the transaction into the pending table. When the timer
* fires, it will check whether the transaction is old (by comparing current
* time and the txn's timestamp), if so the transaction will be freed. along
* ID of this stale transaction will also be freed. If the client fails to
* extend a lease, we want to stop the client from using any IP addresses that
* were granted previously.
*
* A RELEASE message from the client will not cause a transaction to be created.
* The client ID in the RELEASE message will be used for finding and removing
* transactions in the completed and dyn-ip tables.
*
*
* For IPv6, the process to acquire a DHCPv6-assigned address is as follows:
*
* 1. Client sends REQUEST. The DUID is extracted and stored into a dhcpv6_cid_t
* structure. A new transaction structure (dhcpv6_txn_t) is also created and
* it will point to the dhcpv6_cid_t. If an existing transaction with a
* matching xid is not found, this dhcpv6_txn_t will be inserted into the
* mci_v6_pending_txn table (keyed by xid).
*
* 2. Server responds with a REPLY. If a pending transaction is found, the
* addresses in the reply will be placed into the dhcpv6_cid_t pointed to by
* the transaction. The dhcpv6_cid_t will then be moved to the mci_v6_cid
* table (keyed by cid). The associated addresses will be added to the
* mci_v6_dyn_ip table (while still being pointed to by the dhcpv6_cid_t).
*
* 3. IPv6 ip-nospoof will now check mci_v6_dyn_ip for matching packets.
* Packets with a source address matching one of the DHCPv6-assigned
* addresses will be allowed through.
*
* IPv6 notes:
*
* The v6 code shares the same timer as v4 for scrubbing stale transactions.
* Just like v4, as part of removing an expired transaction, a RELEASE will be
* be triggered on the cid associated with the expired transaction.
*
* The data structures used for v6 are slightly different because a v6 client
* may have multiple addresses associated with it.
*/
/*
* These are just arbitrary limits meant for preventing abuse (e.g. a user
* flooding the network with bogus transactions). They are not meant to be
* user-modifiable so they are not exposed as linkprops.
*/
/*
* DHCPv4 transaction. It may be added to three different tables
* (keyed by different fields).
*/
typedef struct dhcpv4_txn {
struct dhcpv4_txn *dt_next;
} dhcpv4_txn_t;
/*
* DHCPv6 address. May be added to mci_v6_dyn_ip.
* It is always pointed to by its parent dhcpv6_cid_t structure.
*/
typedef struct dhcpv6_addr {
struct dhcpv6_addr *da_next;
/*
* DHCPv6 client ID. May be added to mci_v6_cid.
* No dhcpv6_txn_t should be pointing to it after it is added to mci_v6_cid.
*/
typedef struct dhcpv6_cid {
} dhcpv6_cid_t;
/*
* DHCPv6 transaction. Unlike its v4 counterpart, this object gets freed up
* as soon as the transaction completes or expires.
*/
typedef struct dhcpv6_txn {
struct dhcpv6_txn *dt_next;
} dhcpv6_txn_t;
static void start_txn_cleanup_timer(mac_client_impl_t *);
/*
* Comparison functions for the 3 AVL trees used:
* mci_v4_pending_txn, mci_v4_completed_txn, mci_v4_dyn_ip
*/
static int
{
return (-1);
return (1);
else
return (0);
}
static int
{
int ret;
return (-1);
return (1);
if (txn1->dt_cid_len == 0)
return (0);
if (ret < 0)
return (-1);
else if (ret > 0)
return (1);
else
return (0);
}
static int
{
return (-1);
return (1);
else
return (0);
}
/*
* Find the specified DHCPv4 option.
*/
static int
{
start++;
continue;
}
break;
return (0);
}
}
return (ENOENT);
}
/*
* Locate the start of a DHCPv4 header.
* The possible return values and associated meanings are:
* 0 - packet is DHCP and has a DHCP header.
* EINVAL - packet is not DHCP. the recommended action is to let it pass.
* ENOSPC - packet is a initial fragment that is DHCP or is unidentifiable.
* the recommended action is to drop it.
*/
static int
{
return (EINVAL);
/*
* All non-initial fragments may pass because we cannot
* identify their type. It's safe to let them through
* because reassembly will fail if we decide to drop the
* initial fragment.
*/
return (EINVAL);
first_frag = B_TRUE;
}
/* drop packets without a udp header */
return (ENOSPC);
return (EINVAL);
/* drop dhcp fragments */
if (first_frag)
return (ENOSPC);
return (EINVAL);
return (0);
}
/*
* Wrappers for accesses to avl trees to improve readability.
* Their purposes are fairly self-explanatory.
*/
static dhcpv4_txn_t *
{
}
static int
{
return (EEXIST);
return (EAGAIN);
}
return (0);
}
static void
{
}
static dhcpv4_txn_t *
{
if (cid_len > 0)
}
/*
* After a pending txn is removed from the pending table, it is inserted
* into both the completed and dyn-ip tables. These two insertions are
* done together because a client ID must have 1:1 correspondence with
* an IP address and IP addresses must be unique in the dyn-ip table.
*/
static int
{
return (EEXIST);
return (EAGAIN);
}
return (EEXIST);
}
return (0);
}
static void
{
}
/*
* Check whether an IP address is in the dyn-ip table.
*/
static boolean_t
{
}
/*
*/
static dhcpv4_txn_t *
{
return (NULL);
if (cid_len > 0)
return (txn);
}
static void
{
}
/*
* Clean up all v4 tables.
*/
static void
{
/*
* No freeing needed here because the same txn exists
* in the mci_v4_completed_txn table as well.
*/
}
}
}
}
/*
* Cleanup stale DHCPv4 transactions.
*/
static void
{
/*
* Find stale pending transactions and place them on a list
* to be removed.
*/
dhcpv4_txn_t *, txn);
}
}
/*
* Remove and free stale pending transactions and completed
* transactions with the same client IDs as the stale transactions.
*/
txn->dt_cid_len);
dhcpv4_txn_t *, ctxn);
}
dhcpv4_txn_t *, txn);
}
}
/*
* Core logic for intercepting outbound DHCPv4 packets.
*/
static boolean_t
{
return (B_TRUE);
/* ip_nospoof/allowed-ips and DHCP are mutually exclusive by default */
return (B_FALSE);
opt_len != 1) {
return (B_TRUE);
}
return (B_TRUE);
}
/* client ID is optional for IPv4 */
opt_len >= 2) {
} else {
cid_len = 0;
}
/* flush any completed txn with this cid */
}
goto done;
}
/*
* If a pending txn already exists, we'll update its timestamp so
* it won't get flushed by the timer. We don't need to create new
* txns for retransmissions.
*/
dhcpv4_txn_t *, txn);
goto done;
}
goto done;
}
goto done;
dhcpv4_txn_t *, txn);
goto done;
}
dhcpv4_txn_t *, txn);
done:
return (B_TRUE);
}
/*
* Core logic for intercepting inbound DHCPv4 packets.
*/
static void
{
return;
opt_len != 1) {
return;
}
return;
}
goto done;
}
/*
* We're about to move a txn from the pending table to the completed/
* dyn-ip tables. If there is an existing completed txn with the
* same cid as our txn, we need to remove and free it.
*/
dhcpv4_txn_t *, ctxn);
}
dhcpv4_txn_t *, txn);
goto done;
}
dhcpv4_txn_t *, txn);
goto done;
}
dhcpv4_txn_t *, txn);
done:
}
/*
* Comparison functions for the DHCPv6 AVL trees.
*/
static int
{
return (-1);
return (1);
else
return (0);
}
static int
{
int ret;
if (ret < 0)
return (-1);
else if (ret > 0)
return (1);
else
return (0);
}
static int
{
int ret;
return (-1);
return (1);
if (cid1->dc_cid_len == 0)
return (0);
if (ret < 0)
return (-1);
else if (ret > 0)
return (1);
else
return (0);
}
/*
* Locate the start of a DHCPv6 header.
* The possible return values and associated meanings are:
* 0 - packet is DHCP and has a DHCP header.
* EINVAL - packet is not DHCP. the recommended action is to let it pass.
* ENOSPC - packet is a initial fragment that is DHCP or is unidentifiable.
* the recommended action is to drop it.
*/
static int
{
return (ENOSPC);
if (proto != IPPROTO_UDP)
return (EINVAL);
/*
* All non-initial fragments may pass because we cannot
* identify their type. It's safe to let them through
* because reassembly will fail if we decide to drop the
* initial fragment.
*/
return (EINVAL);
first_frag = B_TRUE;
}
/* drop packets without a udp header */
return (ENOSPC);
return (EINVAL);
/* drop dhcp fragments */
if (first_frag)
return (ENOSPC);
return (EINVAL);
return (0);
}
/*
* Find the specified DHCPv6 option.
*/
static dhcpv6_option_t *
{
while (buflen >= sizeof (dhcpv6_option_t)) {
break;
continue;
}
/* LINTED : alignment */
return ((dhcpv6_option_t *)bp);
}
return (NULL);
}
/*
* Get the status code from a reply message.
*/
static int
{
uint16_t s;
/* Success is implied if status code is missing */
return (0);
}
return (EINVAL);
if (olen < sizeof (s))
return (EINVAL);
return (0);
}
/*
* Get the addresses from a reply message.
*/
static int
{
if (olen < sizeof (dhcpv6_ia_na_t) ||
goto fail;
olen -= sizeof (dhcpv6_ia_na_t);
if (solen < sizeof (dhcpv6_iaaddr_t) ||
goto fail;
sizeof (in6_addr_t)) == 0)
goto fail;
}
KM_NOSLEEP)) == NULL)
goto fail;
sizeof (in6_addr_t));
cid->dc_addrcnt++;
}
}
if (cid->dc_addrcnt == 0)
return (ENOENT);
return (0);
fail:
cid->dc_addrcnt--;
}
return (EINVAL);
}
/*
* Free a cid.
* Before this gets called the caller must ensure that all the
* addresses are removed from the mci_v6_dyn_ip table.
*/
static void
{
cnt++;
}
}
/*
* Extract the DUID from a message. The associated addresses will be
* extracted later from the reply message.
*/
static dhcpv6_cid_t *
{
return (NULL);
return (NULL);
return (NULL);
}
return (cid);
}
/*
* Remove a cid from mci_v6_cid. The addresses owned by the cid
* are also removed from mci_v6_dyn_ip.
*/
static void
{
}
}
/*
* Find and remove a matching cid and associated addresses from
* their respective tables.
*/
static void
{
return;
/*
* Since cid belongs to a pending txn, it can't possibly be in
* mci_v6_cid. Anything that's found must be an existing cid.
*/
}
/*
* Insert cid into mci_v6_cid.
*/
static int
{
return (EEXIST);
return (EAGAIN);
}
goto fail;
}
return (0);
fail:
return (EEXIST);
}
/*
* Check whether an IP address is in the dyn-ip table.
*/
static boolean_t
{
dhcpv6_addr_t tmp_addr, *a;
return (a != NULL);
}
static dhcpv6_txn_t *
{
}
static void
{
}
static dhcpv6_txn_t *
{
return (NULL);
return (txn);
}
static void
{
}
static int
{
return (EEXIST);
return (EAGAIN);
}
return (0);
}
/*
* Clean up all v6 tables.
*/
static void
{
}
}
}
}
/*
* Cleanup stale DHCPv6 transactions.
*/
static void
{
/*
* Find stale pending transactions and place them on a list
* to be removed.
*/
dhcpv6_txn_t *, txn);
}
}
/*
* Remove and free stale pending transactions.
* Release any existing cids matching the stale transactions.
*/
dhcpv6_txn_t *, txn);
}
}
/*
* Core logic for intercepting outbound DHCPv6 packets.
*/
static boolean_t
{
return (B_TRUE);
/* ip_nospoof/allowed-ips and DHCP are mutually exclusive by default */
return (B_FALSE);
return (B_TRUE);
return (B_TRUE);
if (mtype == DHCPV6_MSG_RELEASE) {
goto done;
}
dhcpv6_txn_t *, txn);
goto done;
}
goto done;
dhcpv6_txn_t *, txn);
goto done;
}
dhcpv6_txn_t *, txn);
done:
return (B_TRUE);
}
/*
* Core logic for intercepting inbound DHCPv6 packets.
*/
static void
{
return;
if (mtype != DHCPV6_MSG_REPLY)
return;
dhcpv6_message_t *, dh6);
goto done;
}
status != DHCPV6_STAT_SUCCESS) {
dhcpv6_txn_t *, txn);
goto done;
}
dhcpv6_txn_t *, txn);
goto done;
}
dhcpv6_txn_t *, txn);
goto done;
}
dhcpv6_txn_t *, txn);
done:
}
/*
* Timer for cleaning up stale transactions.
*/
static void
txn_cleanup_timer(void *arg)
{
if (mcip->mci_txn_cleanup_tid == 0) {
/* do nothing if timer got cancelled */
return;
}
mcip->mci_txn_cleanup_tid = 0;
/*
* Restart timer if pending transactions still exist.
*/
}
}
static void
{
if (mcip->mci_txn_cleanup_tid == 0) {
}
}
static void
{
/*
* This needs to be a while loop because the timer could get
* rearmed during untimeout().
*/
mcip->mci_txn_cleanup_tid = 0;
}
}
/*
* pulled-up packet needs to be freed by the caller.
*/
static int
{
uchar_t *s, *e;
/*
* Pullup if necessary but reject packets that do not have
* a proper mac header.
*/
return (EINVAL);
/*
* Temporarily adjust mp->b_rptr to ensure proper
* alignment of IP header in newmp.
*/
return (ENOMEM);
}
*start = s;
*end = e;
return (0);
}
void
{
int err;
if (err != 0) {
return;
}
if (err != 0) {
return;
}
switch (mhi.mhi_bindsap) {
case ETHERTYPE_IP: {
return;
break;
}
case ETHERTYPE_IPV6: {
return;
break;
}
}
}
void
{
/*
* Skip checks if we are part of an aggr.
*/
return;
}
void
{
}
void
{
}
/*
* Check if addr is in the 'allowed-ips' list.
*/
/* ARGSUSED */
static boolean_t
{
uint_t i;
/*
* The unspecified address is allowed.
*/
if (*addr == INADDR_ANY)
return (B_TRUE);
for (i = 0; i < protect->mp_ipaddrcnt; i++) {
/* LINTED E_SUSPICIOUS_COMPARISON */
/*
* Since we have a netmask we know this entry
* signifies the entire subnet. Check if the
* given address is on the subnet.
*/
return (B_TRUE);
}
}
return (protect->mp_ipaddrcnt == 0 ?
}
static boolean_t
{
uint_t i;
/*
* The unspecified address and the v6 link local address are allowed.
*/
if (IN6_IS_ADDR_UNSPECIFIED(addr) ||
return (B_TRUE);
for (i = 0; i < protect->mp_ipaddrcnt; i++) {
/* LINTED E_SUSPICIOUS_COMPARISON */
v6addr->ip_netmask))
return (B_TRUE);
}
return (protect->mp_ipaddrcnt == 0 ?
}
/*
* Checks various fields within an IPv6 NDP packet.
*/
static boolean_t
{
/*
* NDP packets do not have extension headers so the ICMPv6 header
* must immediately follow the IPv6 header.
*/
return (B_TRUE);
/* ICMPv6 header missing */
return (B_FALSE);
switch (type) {
case ND_ROUTER_SOLICIT:
hdrlen = sizeof (nd_router_solicit_t);
break;
case ND_ROUTER_ADVERT:
hdrlen = sizeof (nd_router_advert_t);
break;
case ND_NEIGHBOR_SOLICIT:
hdrlen = sizeof (nd_neighbor_solicit_t);
break;
case ND_NEIGHBOR_ADVERT:
hdrlen = sizeof (nd_neighbor_advert_t);
break;
case ND_REDIRECT:
hdrlen = sizeof (nd_redirect_t);
break;
default:
return (B_TRUE);
}
return (B_FALSE);
switch (type) {
case ND_NEIGHBOR_ADVERT: {
return (B_FALSE);
}
/* TLLA option for NA */
break;
}
case ND_REDIRECT: {
/* option checking not needed for RD */
return (B_TRUE);
}
default:
break;
}
/* no options, we're done */
return (B_TRUE);
}
/* find the option header we need */
while (optlen > sizeof (nd_opt_hdr_t)) {
break;
}
opt = (nd_opt_hdr_t *)
}
return (B_TRUE);
return (B_FALSE);
}
return (B_TRUE);
}
/*
* Enforce ip-nospoof protection.
*/
static int
{
int err;
if (err != 0) {
return (err);
}
switch (sap) {
case ETHERTYPE_IP: {
goto fail;
goto fail;
goto fail;
break;
}
case ETHERTYPE_ARP: {
goto fail;
goto fail;
goto fail;
if (hlen != 0 &&
goto fail;
goto fail;
break;
}
case ETHERTYPE_IPV6: {
goto fail;
goto fail;
goto fail;
goto fail;
break;
}
}
return (0);
fail:
return (err);
}
static boolean_t
{
int i;
for (i = 0; i < p->mp_cidcnt; i++) {
return (B_TRUE);
}
return (B_FALSE);
}
static boolean_t
{
int err;
return (B_FALSE);
}
if (cidlen == 0)
return (B_TRUE);
return (B_TRUE);
}
static boolean_t
{
int err;
/*
* We only check client-generated messages.
*/
return (B_TRUE);
return (B_TRUE);
return (B_TRUE);
}
}
return (B_TRUE);
}
}
/*
* Enforce dhcp-nospoof protection.
*/
static int
{
int err;
if (err != 0) {
return (err);
}
switch (sap) {
case ETHERTYPE_IP: {
goto fail;
goto fail;
break;
}
case ETHERTYPE_IPV6: {
goto fail;
goto fail;
break;
}
}
return (0);
fail:
/* increment dhcpnospoof stat here */
return (err);
}
/*
* This needs to be called whenever the mac client's mac address changes.
*/
void
{
switch (media) {
case DL_ETHER:
p[0] ^= 0x2;
p[3] = 0xff;
p[4] = 0xfe;
break;
case DL_IB:
p[0] |= 2;
break;
default:
/*
* We do not need to generate the local address for link types
* that do not support link protection. Wifi pretends to be
* ethernet so it is covered by the DL_ETHER case (note the
* use of mi_media instead of mi_nativemedia).
*/
return;
}
for (i = 0; i < 4; i++) {
ll_template.s6_addr32[i];
}
}
/*
* Enforce link protection on one packet.
*/
static int
{
int err;
if (err != 0) {
return (err);
}
if ((types & MPT_MACNOSPOOF) != 0) {
return (EINVAL);
}
}
if ((types & MPT_RESTRICTED) != 0) {
/*
* ETHERTYPE_VLAN packets are allowed through, provided that
* the vid is not spoofed.
*/
return (EINVAL);
}
sap != ETHERTYPE_ARP) {
return (EINVAL);
}
}
if ((types & MPT_IPNOSPOOF) != 0) {
return (err);
}
}
if ((types & MPT_DHCPNOSPOOF) != 0) {
return (err);
}
}
return (0);
}
/*
* Enforce link protection on a packet chain.
* Packets that pass the checks are returned back to the caller.
*/
mblk_t *
{
/*
* Skip checks if we are part of an aggr.
*/
return (mp);
} else {
}
}
return (ret_mp);
}
/*
* Check if a particular protection type is enabled.
*/
{
}
static int
{
uint_t i, j;
if (p->mp_ipaddrcnt == MPT_RESET)
return (0);
if (p->mp_ipaddrcnt > MPT_MAXIPADDR)
return (EINVAL);
for (i = 0; i < p->mp_ipaddrcnt; i++) {
/*
* The unspecified address is implicitly allowed so there's no
* need to add it to the list. Also, validate that the netmask,
* if any, is sane for the specific version of IP. A mask of
* some kind is always required.
*/
if (addr->ip_netmask == 0)
return (EINVAL);
return (EINVAL);
return (EINVAL);
return (EINVAL);
return (EINVAL);
return (EINVAL);
} else {
/* invalid ip version */
return (EINVAL);
}
for (j = 0; j < p->mp_ipaddrcnt; j++) {
continue;
/* found a duplicate */
return (EINVAL);
}
}
return (0);
}
/* ARGSUSED */
static int
{
uint_t i, j;
return (0);
if (p->mp_cidcnt > MPT_MAXCID)
return (EINVAL);
for (i = 0; i < p->mp_cidcnt; i++) {
return (EINVAL);
for (j = 0; j < p->mp_cidcnt; j++) {
continue;
/* found a duplicate */
return (EINVAL);
}
}
return (0);
}
/*
* Sanity-checks parameters given by userland.
*/
int
{
int err;
/* check for invalid types */
return (EINVAL);
if ((err = validate_ips(p)) != 0)
return (err);
if ((err = validate_cids(p)) != 0)
return (err);
return (0);
}
/*
*/
int
{
int err;
/* tunnels are not supported */
return (ENOTSUP);
return (err);
if (err != 0)
return (err);
return (0);
}
void
{
} else {
if (types != 0) {
}
}
if (np->mp_ipaddrcnt != 0) {
sizeof (cp->mp_ipaddrs));
cp->mp_ipaddrcnt = 0;
}
}
}
}
}
void
{
mcip->mci_protect_flags = 0;
mcip->mci_txn_cleanup_tid = 0;
}
void
{
mcip->mci_txn_cleanup_tid = 0;
mcip->mci_protect_flags = 0;
}
static boolean_t
{
int i;
return (B_TRUE);
}
return (B_FALSE);
}
{
}