root/usr/src/uts/common/io/mac/plugins/mac_wifi.c
/*
 * 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
 * or http://www.opensolaris.org/os/licensing.
 * 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 2009 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

/*
 * WiFi MAC Type plugin for the Nemo mac module
 *
 * This is a bit of mutant since we pretend to be mostly DL_ETHER.
 */

#include <sys/types.h>
#include <sys/modctl.h>
#include <sys/dlpi.h>
#include <sys/dld_impl.h>
#include <sys/mac_wifi.h>
#include <sys/ethernet.h>
#include <sys/byteorder.h>
#include <sys/strsun.h>
#include <inet/common.h>

uint8_t wifi_bcastaddr[]        = { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };
static uint8_t wifi_ietfmagic[] = { 0xaa, 0xaa, 0x03, 0x00, 0x00, 0x00 };
static uint8_t wifi_ieeemagic[] = { 0xaa, 0xaa, 0x03, 0x00, 0x00, 0xf8 };

static mac_stat_info_t wifi_stats[] = {
        /* statistics described in ieee802.11(7) */
{ WIFI_STAT_TX_FRAGS,           "tx_frags",             KSTAT_DATA_UINT32, 0 },
{ WIFI_STAT_MCAST_TX,           "mcast_tx",             KSTAT_DATA_UINT32, 0 },
{ WIFI_STAT_TX_FAILED,          "tx_failed",            KSTAT_DATA_UINT32, 0 },
{ WIFI_STAT_TX_RETRANS,         "tx_retrans",           KSTAT_DATA_UINT32, 0 },
{ WIFI_STAT_TX_RERETRANS,       "tx_reretrans",         KSTAT_DATA_UINT32, 0 },
{ WIFI_STAT_RTS_SUCCESS,        "rts_success",          KSTAT_DATA_UINT32, 0 },
{ WIFI_STAT_RTS_FAILURE,        "rts_failure",          KSTAT_DATA_UINT32, 0 },
{ WIFI_STAT_ACK_FAILURE,        "ack_failure",          KSTAT_DATA_UINT32, 0 },
{ WIFI_STAT_RX_FRAGS,           "rx_frags",             KSTAT_DATA_UINT32, 0 },
{ WIFI_STAT_MCAST_RX,           "mcast_rx",             KSTAT_DATA_UINT32, 0 },
{ WIFI_STAT_FCS_ERRORS,         "fcs_errors",           KSTAT_DATA_UINT32, 0 },
{ WIFI_STAT_WEP_ERRORS,         "wep_errors",           KSTAT_DATA_UINT32, 0 },
{ WIFI_STAT_RX_DUPS,            "rx_dups",              KSTAT_DATA_UINT32, 0 }
};

static struct modlmisc mac_wifi_modlmisc = {
        &mod_miscops,
        "WiFi MAC plugin 1.4"
};

static struct modlinkage mac_wifi_modlinkage = {
        MODREV_1,
        &mac_wifi_modlmisc,
        NULL
};

static mactype_ops_t mac_wifi_type_ops;

int
_init(void)
{
        mactype_register_t *mtrp = mactype_alloc(MACTYPE_VERSION);
        int err;

        /*
         * If `mtrp' is NULL, then this plugin is not compatible with
         * the system's MAC Type plugin framework.
         */
        if (mtrp == NULL)
                return (ENOTSUP);

        mtrp->mtr_ops           = &mac_wifi_type_ops;
        mtrp->mtr_ident         = MAC_PLUGIN_IDENT_WIFI;
        mtrp->mtr_mactype       = DL_ETHER;
        mtrp->mtr_nativetype    = DL_WIFI;
        mtrp->mtr_stats         = wifi_stats;
        mtrp->mtr_statcount     = A_CNT(wifi_stats);
        mtrp->mtr_addrlen       = IEEE80211_ADDR_LEN;
        mtrp->mtr_brdcst_addr   = wifi_bcastaddr;

        if ((err = mactype_register(mtrp)) == 0) {
                if ((err = mod_install(&mac_wifi_modlinkage)) != 0)
                        (void) mactype_unregister(MAC_PLUGIN_IDENT_WIFI);
        }
        mactype_free(mtrp);
        return (err);
}

int
_fini(void)
{
        int     err;

        if ((err = mactype_unregister(MAC_PLUGIN_IDENT_WIFI)) != 0)
                return (err);
        return (mod_remove(&mac_wifi_modlinkage));
}

int
_info(struct modinfo *modinfop)
{
        return (mod_info(&mac_wifi_modlinkage, modinfop));
}

/*
 * MAC Type plugin operations
 */

static boolean_t
mac_wifi_pdata_verify(void *pdata, size_t pdata_size)
{
        wifi_data_t *wdp = pdata;

        return (pdata_size == sizeof (wifi_data_t) && wdp->wd_opts == 0);
}

/* ARGSUSED */
static int
mac_wifi_unicst_verify(const void *addr, void *pdata)
{
        /* If it's not a group address, then it's a valid unicast address. */
        return (IEEE80211_IS_MULTICAST(addr) ? EINVAL : 0);
}

/* ARGSUSED */
static int
mac_wifi_multicst_verify(const void *addr, void *pdata)
{
        /* The address must be a group address. */
        if (!IEEE80211_IS_MULTICAST(addr))
                return (EINVAL);
        /* The address must not be the media broadcast address. */
        if (bcmp(addr, wifi_bcastaddr, sizeof (wifi_bcastaddr)) == 0)
                return (EINVAL);
        return (0);
}

/*
 * Verify that `sap' is valid, and return the actual SAP to bind to in
 * `*bind_sap'.  The WiFI SAP space is identical to Ethernet.
 */
/* ARGSUSED */
static boolean_t
mac_wifi_sap_verify(uint32_t sap, uint32_t *bind_sap, void *pdata)
{
        if (sap >= ETHERTYPE_802_MIN && sap <= ETHERTYPE_MAX) {
                if (bind_sap != NULL)
                        *bind_sap = sap;
                return (B_TRUE);
        }

        if (sap <= ETHERMTU) {
                if (bind_sap != NULL)
                        *bind_sap = DLS_SAP_LLC;
                return (B_TRUE);
        }
        return (B_FALSE);
}

/*
 * Create a template WiFi datalink header for `sap' packets between `saddr'
 * and `daddr'.  Any enabled modes and features relevant to building the
 * header are passed via `pdata'.  Return NULL on failure.
 */
/* ARGSUSED */
static mblk_t *
mac_wifi_header(const void *saddr, const void *daddr, uint32_t sap,
    void *pdata, mblk_t *payload, size_t extra_len)
{
        struct ieee80211_frame  *wh;
        struct ieee80211_llc    *llc;
        mblk_t                  *mp;
        wifi_data_t             *wdp = pdata;

        if (!mac_wifi_sap_verify(sap, NULL, NULL))
                return (NULL);

        if ((mp = allocb(WIFI_HDRSIZE + extra_len, BPRI_HI)) == NULL)
                return (NULL);
        bzero(mp->b_rptr, WIFI_HDRSIZE + extra_len);

        /*
         * Fill in the fixed parts of the ieee80211_frame.
         */
        wh = (struct ieee80211_frame *)mp->b_rptr;
        mp->b_wptr += sizeof (struct ieee80211_frame) + wdp->wd_qospad;
        wh->i_fc[0] = IEEE80211_FC0_VERSION_0 | IEEE80211_FC0_TYPE_DATA;

        switch (wdp->wd_opmode) {
        case IEEE80211_M_STA:
                wh->i_fc[1] = IEEE80211_FC1_DIR_TODS;
                IEEE80211_ADDR_COPY(wh->i_addr1, wdp->wd_bssid);
                IEEE80211_ADDR_COPY(wh->i_addr2, saddr);
                IEEE80211_ADDR_COPY(wh->i_addr3, daddr);
                break;

        case IEEE80211_M_IBSS:
        case IEEE80211_M_AHDEMO:
                wh->i_fc[1] = IEEE80211_FC1_DIR_NODS;
                IEEE80211_ADDR_COPY(wh->i_addr1, daddr);
                IEEE80211_ADDR_COPY(wh->i_addr2, saddr);
                IEEE80211_ADDR_COPY(wh->i_addr3, wdp->wd_bssid);
                break;

        case IEEE80211_M_HOSTAP:
                wh->i_fc[1] = IEEE80211_FC1_DIR_FROMDS;
                IEEE80211_ADDR_COPY(wh->i_addr1, daddr);
                IEEE80211_ADDR_COPY(wh->i_addr2, wdp->wd_bssid);
                IEEE80211_ADDR_COPY(wh->i_addr3, saddr);
                break;
        }

        if (wdp->wd_qospad) {
                struct ieee80211_qosframe *qwh =
                    (struct ieee80211_qosframe *)wh;
                qwh->i_qos[1] = 0;
                qwh->i_fc[0] |= IEEE80211_FC0_SUBTYPE_QOS;
        }

        switch (wdp->wd_secalloc) {
        case WIFI_SEC_WEP:
                /*
                 * Fill in the fixed parts of the WEP-portion of the frame.
                 */
                wh->i_fc[1] |= IEEE80211_FC1_WEP;
                /*
                 * The actual contents of the WEP-portion of the packet
                 * are computed when the packet is sent -- for now, we
                 * just need to account for the size.
                 */
                mp->b_wptr += IEEE80211_WEP_IVLEN + IEEE80211_WEP_KIDLEN;
                break;

        case WIFI_SEC_WPA:
                wh->i_fc[1] |= IEEE80211_FC1_WEP;
                mp->b_wptr += IEEE80211_WEP_IVLEN +
                    IEEE80211_WEP_KIDLEN + IEEE80211_WEP_EXTIVLEN;
                break;

        default:
                break;
        }

        /*
         * Fill in the fixed parts of the ieee80211_llc header.
         */
        llc = (struct ieee80211_llc *)mp->b_wptr;
        mp->b_wptr += sizeof (struct ieee80211_llc);
        bcopy(wifi_ietfmagic, llc, sizeof (wifi_ietfmagic));
        llc->illc_ether_type = htons(sap);

        return (mp);
}

/*
 * Use the provided `mp' (which is expected to point to a WiFi header), and
 * fill in the provided `mhp'.  Return an errno on failure.
 */
/* ARGSUSED */
static int
mac_wifi_header_info(mblk_t *mp, void *pdata, mac_header_info_t *mhp)
{
        struct ieee80211_frame  *wh;
        struct ieee80211_llc    *llc;
        uchar_t                 *llcp;
        wifi_data_t             *wdp = pdata;

        if (MBLKL(mp) < sizeof (struct ieee80211_frame))
                return (EINVAL);

        wh = (struct ieee80211_frame *)mp->b_rptr;
        llcp = mp->b_rptr + sizeof (struct ieee80211_frame);

        /*
         * Generally, QoS data field takes 2 bytes, but some special hardware,
         * such as Atheros, will need the 802.11 header padded to a 32-bit
         * boundary for 4-address and QoS frames, at this time, it's 4 bytes.
         */
        if (wh->i_fc[0] & IEEE80211_FC0_SUBTYPE_QOS)
                llcp += wdp->wd_qospad;

        /*
         * When we receive frames from other hosts, the hardware will have
         * already performed WEP decryption, and thus there will not be a WEP
         * portion.  However, when we receive a loopback copy of our own
         * packets, it will still have a WEP portion.  Skip past it to get to
         * the LLC header.
         */
        if (wh->i_fc[1] & IEEE80211_FC1_WEP) {
                llcp += IEEE80211_WEP_IVLEN + IEEE80211_WEP_KIDLEN;
                if (wdp->wd_secalloc == WIFI_SEC_WPA)
                        llcp += IEEE80211_WEP_EXTIVLEN;
        }

        if ((uintptr_t)mp->b_wptr - (uintptr_t)llcp <
            sizeof (struct ieee80211_llc))
                return (EINVAL);

        llc = (struct ieee80211_llc *)llcp;
        mhp->mhi_origsap = ntohs(llc->illc_ether_type);
        mhp->mhi_bindsap = mhp->mhi_origsap;
        mhp->mhi_pktsize = 0;
        mhp->mhi_hdrsize = (uintptr_t)llcp + sizeof (*llc) -
            (uintptr_t)mp->b_rptr;

        /*
         * Verify the LLC header is one of the known formats.  As per MSFT's
         * convention, if the header is using IEEE 802.1H encapsulation, then
         * treat the LLC header as data.  As per DL_ETHER custom when treating
         * the LLC header as data, set the mhi_bindsap to be DLS_SAP_LLC, and
         * assume mhi_origsap contains the data length.
         */
        if (bcmp(llc, wifi_ieeemagic, sizeof (wifi_ieeemagic)) == 0) {
                mhp->mhi_bindsap = DLS_SAP_LLC;
                mhp->mhi_hdrsize -= sizeof (*llc);
                mhp->mhi_pktsize = mhp->mhi_hdrsize + mhp->mhi_origsap;
        } else if (bcmp(llc, wifi_ietfmagic, sizeof (wifi_ietfmagic)) != 0) {
                return (EINVAL);
        }

        switch (wh->i_fc[1] & IEEE80211_FC1_DIR_MASK) {
        case IEEE80211_FC1_DIR_NODS:
                mhp->mhi_daddr = wh->i_addr1;
                mhp->mhi_saddr = wh->i_addr2;
                break;

        case IEEE80211_FC1_DIR_TODS:
                mhp->mhi_daddr = wh->i_addr3;
                mhp->mhi_saddr = wh->i_addr2;
                break;

        case IEEE80211_FC1_DIR_FROMDS:
                mhp->mhi_daddr = wh->i_addr1;
                mhp->mhi_saddr = wh->i_addr3;
                break;

        case IEEE80211_FC1_DIR_DSTODS:
                /* We don't support AP-to-AP mode yet */
                return (ENOTSUP);
        }

        if (mac_wifi_unicst_verify(mhp->mhi_daddr, NULL) == 0)
                mhp->mhi_dsttype = MAC_ADDRTYPE_UNICAST;
        else if (mac_wifi_multicst_verify(mhp->mhi_daddr, NULL) == 0)
                mhp->mhi_dsttype = MAC_ADDRTYPE_MULTICAST;
        else
                mhp->mhi_dsttype = MAC_ADDRTYPE_BROADCAST;

        return (0);
}

/*
 * Take the provided `mp' (which is expected to have an Ethernet header), and
 * return a pointer to an mblk_t with a WiFi header.  Note that the returned
 * header will not be complete until the driver finishes filling it in prior
 * to transmit.  If the conversion cannot be performed, return NULL.
 */
static mblk_t *
mac_wifi_header_cook(mblk_t *mp, void *pdata)
{
        struct ether_header     *ehp;
        mblk_t                  *llmp;

        if (MBLKL(mp) < sizeof (struct ether_header))
                return (NULL);

        ehp = (void *)mp->b_rptr;
        llmp = mac_wifi_header(&ehp->ether_shost, &ehp->ether_dhost,
            ntohs(ehp->ether_type), pdata, NULL, 0);
        if (llmp == NULL)
                return (NULL);

        /*
         * The plugin framework guarantees that we have the only reference
         * to the mblk_t, so we can safely modify it.
         */
        ASSERT(DB_REF(mp) == 1);
        mp->b_rptr += sizeof (struct ether_header);
        llmp->b_cont = mp;
        return (llmp);
}

/*
 * Take the provided `mp' (which is expected to have a WiFi header), and
 * return a pointer to an mblk_t with an Ethernet header.  If the conversion
 * cannot be performed, return NULL.
 */
static mblk_t *
mac_wifi_header_uncook(mblk_t *mp, void *pdata)
{
        mac_header_info_t       mhi;
        struct ether_header     eh;

        if (mac_wifi_header_info(mp, pdata, &mhi) != 0) {
                /*
                 * The plugin framework guarantees the header is properly
                 * formed, so this should never happen.
                 */
                return (NULL);
        }

        /*
         * The plugin framework guarantees that we have the only reference to
         * the mblk_t and the underlying dblk_t, so we can safely modify it.
         */
        ASSERT(DB_REF(mp) == 1);

        IEEE80211_ADDR_COPY(&eh.ether_dhost, mhi.mhi_daddr);
        IEEE80211_ADDR_COPY(&eh.ether_shost, mhi.mhi_saddr);
        eh.ether_type = htons(mhi.mhi_origsap);

        ASSERT(mhi.mhi_hdrsize >= sizeof (struct ether_header));
        mp->b_rptr += mhi.mhi_hdrsize - sizeof (struct ether_header);
        bcopy(&eh, mp->b_rptr, sizeof (struct ether_header));
        return (mp);
}

static mactype_ops_t mac_wifi_type_ops = {
        MTOPS_PDATA_VERIFY | MTOPS_HEADER_COOK | MTOPS_HEADER_UNCOOK,
        mac_wifi_unicst_verify,
        mac_wifi_multicst_verify,
        mac_wifi_sap_verify,
        mac_wifi_header,
        mac_wifi_header_info,
        mac_wifi_pdata_verify,
        mac_wifi_header_cook,
        mac_wifi_header_uncook
};