# 2022q1 vwifi
contributed by < `rickywu0421` >
> [sysprog21/vwifi](https://github.com/sysprog21/vwifi)
[toc]
## PR #27 - Implement real AP and STA mode
- [ ] commit: [97f03a](https://github.com/sysprog21/vwifi/commit/97f03a530dd39afd5bcef7ee3c94750c03a7e291)
vwifi 於本次 commit 實現 HostAP 以及 STA modem, 在此之前 STA 的通訊方式類似 adhoc。
新的實驗環境如下圖:
![](https://i.imgur.com/gxWCRs7.png)
實作上主要完成了下列部分:
1. [STA 得以 scan 真實的 Host AP 網路介面](#one)
2. [STA 得以 connect 真實的 Host AP 網路介面](#two)
3. [實作 AP 轉送封包機制, 禁止 BSS 內的 STA 進行 Ad-hoc like 通訊](#three)
### <a id="one"></a>1. STA 得以 scan 真實的 Host AP 網路介面
以下展示部分實作:
```c
static void inform_bss(struct owl_vif *vif)
{
struct owl_vif *ap;
list_for_each_entry (ap, &owl->ap_list, ap_list) {
...
bss = cfg80211_inform_bss_data(
vif->wdev.wiphy, &data, CFG80211_BSS_FTYPE_UNKNOWN, ap->bssid, tsf,
WLAN_CAPABILITY_ESS, 100, ie, ap->ssid_len + 2, GFP_KERNEL);
...
}
}
```
其作法很單純, 即是走訪整個 AP list `owl->ap_list`, 並將 AP 的部分資訊透過呼叫 `cfg80211_inform_bss_data()` 通知核心。
### <a id="two"></a>2. STA 得以 connect 真實的 Host AP 網路介面
以下展示部分實作:
```c
static void owl_connect_routine(struct work_struct *w)
{
struct owl_vif *vif = container_of(w, struct owl_vif, ws_connect);
struct owl_vif *ap = NULL;
/* Finding the AP by request SSID */
list_for_each_entry (ap, &owl->ap_list, ap_list) {
if (!memcmp(ap->ssid, vif->req_ssid, ap->ssid_len)) {
cfg80211_connect_result(vif->ndev, NULL, NULL, 0, NULL, 0,
WLAN_STATUS_SUCCESS, GFP_KERNEL);
...
vif->sme_state = SME_CONNECTED;
vif->ap = ap;
...
/* Add STA to bss_list, and the head is AP */
list_add_tail(&vif->bss_list, &ap->bss_list);
...
}
}
...
}
```
這個部分也很單純, 走訪整個 AP list `owl->ap_list`, 比對使用者傳入的 `vif->req_ssid` 以及 `ap->ssid` 是否一致, 若是, 則呼叫 `cfg80211_connect_result()` 通知核心此 STA 連接到了該 BSS。
最後更新 STA 的狀態, 並把 STA 加入到該 AP 的 BSS list。
### <a id="three"></a>3. 實作 AP 轉送封包機制
過往 vwifi 在傳送封包上是以 Ad-hoc 的形式傳送, 即封包傳送沒有 AP 介入。現在實作了真實世界的 STA 以及 Host AP mode, 故不再允許此通訊方式。取而代之的是 AP 負責 relay STA 間的封包, 具體處理方式依據網路介面為 STA 或 AP, 以及 TX/RX 有所不同:
- STA TX: 不論是 unicast/multicast/broadcast 封包, 一律傳到已經關聯 (associated) 的 AP
- STA RX: 不論是 unicast/multicast/broadcast 封包, 均把封包交給核心網路子系統。
- AP TX:
- unicast: 根據封包的目的 MAC 地址, 將封包發送到對應的網路介面
- broadcast/multicast: 將封包送往 BSS list 中的所有 STA, 除了發送者之網路介面 (don't send back)
- AP RX:
- unicast: 查看封包的目的 MAC 地址確認是否是給自己的, 若是, 則把封包交給核心網路子系統, 若否, 則傳送封包到對應 STA 的網路介面, 並且不要將封包交給核心網路子系統
- broadcast/multicast: 將封包送往 BSS list 中的所有 STA, 除了發送者之網路介面 (don't send back), 並且將封包交給核心網路子系統
這邊展示 AP RX 的部份程式碼:
```c
static void owl_rx(struct net_device *dev)
{
struct sk_buff *skb, *skb1 = NULL;
...
struct ethhdr *eth_hdr = (struct ethhdr *) skb->data;
/* Receiving a multicast/broadcast packet, send it to every
* STA except the source STA, and pass it to protocol stack.
*/
if (is_multicast_ether_addr(eth_hdr->h_dest)) {
pr_info("owl: is_multicast_ether_addr\n");
skb1 = skb_copy(skb, GFP_KERNEL);
}
/* Receiving a unicast packet */
else {
/* The packet is not for AP itself, send it to destination
* STA, and do not pass it to procotol stack.
*/
if (!ether_addr_equal(eth_hdr->h_dest, vif->ndev->dev_addr)) {
skb1 = skb;
skb = NULL;
}
}
if (skb1) {
pr_info("owl: AP %s relay:\n", vif->ndev->name);
owl_ndo_start_xmit(skb1, vif->ndev);
}
/* Nothing to pass to protocol stack */
if (!skb)
return;
/* Pass the skb to protocol stack */
skb->dev = dev;
skb->protocol = eth_type_trans(skb, dev);
skb->ip_summed = CHECKSUM_UNNECESSARY; /* don't check it */
netif_rx_ni(skb);
}
```
這邊要先介紹兩個區域變數: `skb` 以及 `skb1`:
- `skb`: 指向要被送至核心網路子系統的 socket buffer
- `skb1`: 指向要被傳送至其他 STA 之 socket buffer
這邊列個 AP RX 的表格幫助大家記憶:
| packet type | skb | skb1 |
| -------- | -------- | -------- |
| unicast (給自己) | exist | NULL |
| unicast (給其他人) | NULL | exist |
| multicast/broadcast | exist | exist |
繼續看下去, `is_multicast_ether_addr()` 在封包是 multicast/broadcast 時回傳 `1` (該函式判斷 MAC 地址中的第一個 byte 的 LSB 是否為 `1`, 由於 broadcast 之所有 bit 皆為 `1`, 故會使函式回傳 1)。若回傳 `1`, 則表示封包要被傳送至其他 STA, 並且要被送至核心網路子系統, 故 `skb` 與 `skb1` 都指向 socket buffer。**這邊是關鍵**, 由於 vwifi TX 完成後會把 socket buffer `free` 掉 (這是驅動程式 TX 的工作), 若 `skb` 與 `skb1` 指向同個 socket buffer, 此時可能導致核心網路子系統讀到該被 `free` 掉的區段, 而使核心崩潰。故我用 `skb_copy()` 將 `skb` 完整複製一份給 `skb1` (有興趣的讀者可以去看看 `skb_clone`, `pskb_copy()` 以及 `skb_copy()` 的差別)。
若封包是 unicast, 則判斷封包是否是給自己的, 若是, 則將封包送至核心網路子系統, 若否, 則傳送封包到指定的網路介面, 並且不要將其送至核心網路子系統。
## PR #24 - Refactor and assign one wiphy to each interface
- [ ] commit: [bfc9330](https://github.com/sysprog21/vwifi/commit/bfc933015127a055a5531e5b45718be7e1958de0)
此 commit 的目的是為了讓程式能彈性的支援之後將被實作的各種模式 (hostap/station/IBSS mode)。
### 引進虛擬網路介面
原本的程式將 "網路介面相關" 與 "獨立於網路介面" 的成員分散在兩個不同的結構體, 在參考 [Atheros ath6kl](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/drivers/net/wireless/ath/ath6kl/core.h?h=v5.18.2) 以及 [Broadcom FullMAC](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.h?h=v5.18.2) 後, 引進虛擬網路介面, 其作為 `struct net_device` 以及 cfg80211 的擴充, 表示一個虛擬網路介面:
```c
struct owl_vif {
struct wireless_dev wdev;
struct net_device *ndev;
struct net_device_stats stats;
/* Currently connected BSS id */
u8 bssid[ETH_ALEN];
u8 ssid[IEEE80211_MAX_SSID_LEN];
/* For the case the STA is going to roam to another BSS */
u8 req_bssid[ETH_ALEN];
u8 req_ssid[IEEE80211_MAX_SSID_LEN];
struct cfg80211_scan_request *scan_request;
enum sme_state sme_state; /* connection information */
unsigned long conn_time; /* last connection time to a AP (in jiffies) */
unsigned long active_time; /* last tx/rx time (in jiffies) */
u16 disconnect_reason_code;
struct mutex lock;
struct timer_list scan_timeout;
struct work_struct ws_connect, ws_disconnect;
struct work_struct ws_scan, ws_scan_timeout;
struct list_head rx_queue; /* Head of received packet queue */
/* List entry for maintaining multiple private data of net_device in
* owl_context.vif_list.
*/
struct list_head list;
};
```
### 使實驗環境可以支援超過兩個以上的網路介面
原本的程式將一個 `wiphy` 共享給所有的網路介面, 在原本的測試情境下這樣做沒有問題:
![](https://i.imgur.com/MhVbjiy.png)
首先我先解釋這個實驗環境:
1. 為何需要 network namespace:假設我們將 `owl0` 的 IP address 設為 `10.0.0.1`, `owl0sink` 的 IP address 設為 `10.0.0.2`, 接著透過下 `ping 10.0.0.2`, 預期結果是封包會透過 `owl0` 傳送而 `owl0sink` 接收, 然而真正的行為是封包會通過 `lo0` (loopback) 傳送及接收, 因為核心通過路由表發現 `10.0.0.1` 以及 `10.0.0.2` 在同一個主機上。Network namespace 實現了網路虛擬化, 其可以從主機中隔離出網路介面以及路由表, 而這使我們的實驗得以進行。
2. 為何需要 macvlan:這是上述的延伸, 無線網路環境下加入 network namespace 是以 `wiphy` 為單位的, 也就是說, 兩個虛擬網路介面 (`owl0` 與 `owl0sink`) 不能被加入不同的 network namespace, 因為他們共用同一個 `wiphy` (然而 Ethernet 沒有這項限制)。因此我們在兩個虛擬網路介面之上建立 macvlan, 其能將封包傳至下層裝置並能被加入 network namespace。
這樣的實驗環境存在一個問題:只容許兩個虛擬網路裝置互相溝通, 因為傳送封包的程式實作如下:
```c
/* @dev: interface structure for current device.
* @dest_np: interface structure for destination device.
*/
dest_np =
list_last_entry(&owl->netintf_list, struct owl_ndev_priv_context, list);
if (dest_np->ndev == dev)
dest_np = list_first_entry(&owl->netintf_list,
struct owl_ndev_priv_context, list);
```
其作法很簡單, 確認是否 `dest_np == dev`, 若是則賦值 `dev` 為 list 中的另一個節點。
為了 vwifi 的擴充性, 這段程式改寫為:
```c
/* @dest_vif is similar to original @dest_np */
if (is_broadcast_ether_addr(eth_hdr->h_dest)) {
list_for_each_entry (dest_vif, &owl->vif_list, list) {
if (dest_vif == vif)
continue;
...
}
}
/* The packet is unicasting */
else {
list_for_each_entry (dest_vif, &owl->vif_list, list) {
if (ether_addr_equal(eth_hdr->h_dest, dest_vif->ndev->dev_addr)) {
...
}
}
}
```
首先先判斷封包是否為廣播封包 (如 ARP request, dhcp request 等等), 若是則傳送給所有網路介面, 若否則迭代網路介面, 確認封包的目的 MAC 位址是否與網路介面的地址相同, 僅相同時傳送封包。
**這迎來了一個問題**:實驗環境真正傳送封包的是 macvlan 裝置, 也就是封包的目的 MAC 位址是其中一個 macvlan 裝置的 MAC 位址而不是 `owl0` 或 `owl0sink` 的 MAC 位址, 於是檢查封包目的 MAC 位址的方式會失敗。
**於是為了解決問題, 我們又生出了一個問題**:我們必須移除 macvlan 裝置, 將 `owl0` 及 `owl0sink` 加入到各自的 network namespace。但上述有解釋過為何需要 macvlan, 就是因為 `owl` 以及 `owl0sink` 共用同一個 `wiphy`, 因此這兩個介面勢必會在同一個 network namespace 中。
**解決問題**: 賦予每個虛擬網路介面不同的 `wiphy`, 使每個虛擬網路介面可以被加入到不同的 network namespace。
新的實驗環境:
![](https://i.imgur.com/Ecminbf.png)
## PR #23: Implement `cfg80211_ops->get_station`
- [ ] commit: [2af271d](https://github.com/sysprog21/vwifi/commit/2af271d8316ca80d2c22d886620b9a133c4227be)
實作 `cfg80211_ops->get_station` 使得某個 station 可以透過 `nl80211` 獲得同個 BSS 中的其他 station 的資訊, 包括:
- TX 資訊
- RX 資訊
- 時間相關資訊
- 無線接收訊號強度 (RSSI)
其中原本模擬 RSSI 的作法是在範圍內生成隨機的值, 但這使得訊號強度不規則跳動, Jserv 老師希望我可以使訊號強度平滑化且不隨機跳動。
### 透過 3rd order sine approximation 模擬 RSSI
$sin(x)$ 的值位於 $-1$ 到 $1$ 之間, 且函數不存在不可微分的點, 因此很適合用來做為平滑化的函數。
#### 泰勒級數
我們將 $sin(x)$ 透過泰勒級數在 $x = 0$ 的位置展開到第三項:
$$
\begin{equation}
\begin{split}
f(x) &= sin(x) \\
&= sin(0) + \frac{sin^{(1)}(0)}{1!}x + \frac{sin^{(2)}(0)}{2!}x^2 + \frac{sin^{(3)}(0)}{3!}x^3 \\
&= x - \frac{1}{6}x^3
\end{split}
\end{equation}
$$
由於 $sin(x)$ 是奇函數, 故最後只會留下奇次項。我們將 $f(x)$ 對 $x$ 作圖, $x$ 範圍為 $[0, 1/2\pi]$:
![](https://i.imgur.com/kscQWvK.png)
我們可以發現, $f(x)$ 在 $x$ 尚未接近 $\frac{1}{2}\pi$ 前都很正常, 但在 $x \rightarrow\frac{1}{2}\pi$ 時開始轉向, 這使得 $f(\frac12\pi)\neq 1$。這樣的結果是因為我們使用的是泰勒級數的近似, 但其實結果還可以接受, 只是需要做一些調整使函式在 crucial point ($x = 0$ or $x = \frac12\pi$) 能夠經過正確的點。
#### 曲線擬合
我們透過三次多項式來重新近似 $sin(x)$, 由於 $sin(x)$ 是奇函數, 我們可以省略偶次項:
$$
\begin{equation}
\left\{
\begin{array}{l}
S_3(x) = ax - bx^3 = x(a - bx^2) \\
S^{'}_{3}{(x)} = a - 3bx^2
\end{array}
\right.
\end{equation}
$$
我們的期望是 $S_3(x)$ 之值介於 $0$ ~ $1$ 之間, 故只需代入 cricial point 使得$S_3(0) = 0$ (不需代入, 因為 $S_3(0)$ 勢必為 $0$) 且 $S_3(\frac12\pi) = 1$:
$$
\begin{equation}
\left\{
\begin{array}{l}
S_3(\frac12\pi) = 1 = \frac{\pi}{2}a - (\frac{\pi}{2})^3b \\
S^{'}_3(\frac12\pi) = 0 = a - 3(\frac{\pi}{2})^2b
\end{array}
\right.
\end{equation}
$$
$$
\begin{equation}
\Rightarrow
\left\{
\begin{array}{l}
a = \frac3\pi \approx 0.955 \\
b = \frac{4}{\pi^3} \approx 0.129
\end{array}
\right.
\end{equation}
$$
於是最終的方程式如下:
$S_3(x) = \frac3\pi x - \frac{4}{\pi^3}x^3$
我們將其作圖, $x$ 範圍為 $[0, \frac{1}{2}\pi]$:
![](https://i.imgur.com/mhedySh.png)
可以發現 crucial point 的值正常了。要注意的是, 參數 $a$, $b$ 的值並非唯一, 若帶入不同的點則會有不同的結果, 也許這組參數的正確率不是最高的, 但他至少確保 crucial point 的值是對的。
#### 座標轉換
若使用原本的單位 (radius), $x$ 的 crucial point 將會是 $0, \frac12\pi, \pi, \frac32\pi, 2\pi, ...$, 這使得範圍不好界定。將 $x$ 經過座標轉換 ($z = x/ (\frac12\pi)$) 後使得 crucial point 變成 $0, 1, 2, 3, 4, ...$:
$$
\begin{equation}
\begin{split}
S_3(x) &= \frac3\pi x - \frac{4}{\pi^3}x^3 \\
&= \frac32 \frac{2x}\pi - \frac12 (\frac{2x}{\pi})^3 \\
&= \frac32 z - \frac12 z^3 \\
S_3(z) &= \frac12 z(3 - z^2)
\end{split}
\end{equation}
$$
#### 實作第一步:floating-point to fixed-point
上述的方程式是基於 floating-point 的, $z$ 以及 $S_3(z)$ 都要以浮點數表式, 這顯然是不符合預期的, 我們可以將輸入以及輸出拉伸到更大的值 (震盪頻率降低), 並用 fixed-point 表示。
為了將 floating-point 的函數轉換成 fixed-point 函數, 我們需要加上幾個 factor:
- scale of outcome: $2^A$, $[-1, 1] \Rightarrow [-2^A, 2^A]$
- scale of angle: $2^n$, 這相當於將 $\frac12\pi$ 擴展到 $2^n$, 也就是 $z = x/2^n$。$x$ 從 $0$ 走到 $2^n$ 相當於走完一個 sine quardrant ($\frac14$ 個週期)。
- scale inside the parentheses: $2^p$, 這是為了防止運算時發生 overflow。
採用到原函數:
$$
\begin{equation}
\begin{split}
S_3(z) &= \frac12 z(3 - z^2)2^A \\
&= z(3 - z^2)2^{A-1} \\
&= x \cdot 2^{-n}(3 - x^2 \cdot 2^{-2n})2^{A-1} \\
&= x (3 - x^2 \cdot 2^{-2n})2^{A-1-n} \\
&= x (3 \cdot 2^p - x^2 \cdot 2^{p-2n})2^{A-1-n-p} \\
S_3(x) &= x(3 \cdot 2^p - x^2 / 2^r)/2^s, \\
&where\ r = 2n-p, s = n+p+1+A
\end{split}
\end{equation}
$$
經過測試, 以下是 $x$ 每若干個 ms 帶入 [jiffies](https://www.linkedin.com/pulse/linux-kernel-system-timer-jiffies-mohamed-yasser) 時震盪頻率最理想的參數集合:
| A | n | p | r | s |
| ---| --| ---| --- |--- |
| 12 | 6 | 10| 2 | 5 |
#### 實作第二步:對稱性
我們的 $S_3(z)$ 近似的範圍僅限 $z = [-1, 1]$ (雖然我們只有近似 $z = [0, 1]$, 但由於 sine 函數是奇函數, 故 $z = [-1, 0]$ 的值也會是對的), 但 sine 函數的 domain 是無限大的。於是我們要用對稱性讓同個週期內的剩下三個 sine quardrant 映射到左右第一個 sine quardrant。
見下圖:
![](https://i.imgur.com/QIqisDc.png)
以下 Q0 代表 $z = [0, 1)$, Q1 代表 $z = [1, 2)$, 以此類推。
我們可以發現 Q0 的值是正確的, 而 Q3 的值恰巧等於 Q(-1) 的值, 故有問題的是 Q1 及 Q2。不過我們發現, Q1 及 Q2 對 $z = 1$ 鏡像後值就正常了, 我們可以根據以下方程式鏡像:
$$
z = 1 - (z - 1) = 2 - z
$$
#### 實作 3rd order sine approximation 程式碼
而程式要如何判斷現在是處於哪個 quardrant 呢?
我們回到 fixed-point 的參數 $n$, 若 $n = 6$ 為, 則 $z = x/2^6$, 若我們要走完一個 sine 函數的週期, 即是 $z = [0, 4)$, 則 $x = [0, 2^8)$。因此, 我們可以透過從 LSB 往 MSB 方向的 bit 6 與 bit 7 組合判斷 (以 32 bit bitstream 為例):
$$
b = \overbrace{............}^{bit \ 8\sim31}\ \overbrace{\underbrace{01}_Q\ 010001}^{bit\ 0\sim7}
$$
只有 Q2 與 Q3 需要做鏡像, 而我們發現 `(b >> 6) ^ (b >> 7) = 1` 即代表 Q2 或 Q3, 反之則為 Q1 或 Q4。但這樣做還不夠, 我們要讓 bit 8 ~ bit 31 成為 bit 7 的 sign extension, 這樣才能確保整個 bit 8 ~ bit 31 不會影響結果。
具體作法是:把 `b` 左移 `((32 - 2) - n) = 24` 個 bit:
```c
x = x << (30 - n);
```
則原本的 bit 7 會跑到 MSB 得位置, 此時進行以下判斷:
```c
if ((x ^ (x << 1)) < 0)
x = (1 << 31) - x;
```
條件成立則表示 MSB (原本的 bit 7) 以及 MSB 的下一個 bit (原本的 bit 6) 不同時為 0 或 1, 即 Q2 或 Q3, 此時進行鏡像 $x = 2 - x$ (`1 << 31` 在此時表示 `2` (記得 `x` 已經被左移過了), 也就是 `0b10....`)。
最後記得右移回去:
```c
x = x >> (30 - n);
```
因為 `x` 是 signed integer, 故右移會自動進行 signed extension。
最後代入 $S_3(x)$:
```c
return (x * ((3 << p) - ((x * x) >> r))) >> s;
```
完整程式碼如下:
```c
static inline s32 __sin_s3(s32 x)
{
/* S(x) = (x * (3 * 2^p - (x * x)/2^r)) / 2^s
* @n: the angle scale
* @A: the amplitude
* @p: keep the multiplication from overflowing
*/
const int32_t n = 6, A = 12, p = 10, r = 2 * n - p, s = n + p + 1 - A;
x = x << (30 - n);
if ((x ^ (x << 1)) < 0)
x = (1 << 31) - x;
x = x >> (30 - n);
return (x * ((3 << p) - ((x * x) >> r))) >> s;
}
```
此函式的參數 `x` 可為任一 32 bit ineger, 而回傳值範圍為 `-2^A ~ 2^A`。
#### 正規化回傳值
對回傳值進行操作, 使值介於 `-100 ~ -30` (RSSI 合理的範圍為 `-100` dBm ~ `-30` dBm)。這邊要注意不能用 floating point 變數 (Linux 核心不使用 FPU)。
```c
#define SIN_S3_MIN (-(1 << 12)) /* -2^A */
#define SIN_S3_MAX (1 << 12) /* 2^A */
static inline s32 rand_int_smooth(s32 low, s32 up, s32 seed):
{
s32 result = __sin_s3(seed) - SIN_S3_MIN;
result = (result * (up - low)) / (SIN_S3_MAX - SIN_S3_MIN);
result += low;
return result;
}
```
呼叫此函式:
```c
s32 rssi = rand_int_smooth(-100, -30, jiffies);
```
#### `cfg80211_ops->get_station()` 之 RSSI 作圖
每若干 ms 透過 `iw` (netlink) 得到 `cfg80211_ops->get_station()` 之 RSSI, 記錄 1000 次, 並對其作圖:
![](https://i.imgur.com/pYQQ1k5.png)
可見 RSSI 具有穩定且平滑化的特性。
## 參考資料
- [Braodcom FullMAC source code](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/drivers/net/wireless/broadcom/brcm80211/brcmfmac?h=v5.18.2)
- [Atheros ath6kl source code](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/drivers/net/wireless/ath/ath6kl?h=v5.18.2)
- [Another fast fixed-point sine approximation](https://www.coranac.com/2009/07/sines/)
## 開發者文件
- [Linux 802.11 Driver Developer’s Guide ](https://www.kernel.org/doc/html/v4.12/driver-api/80211/cfg80211.html)
- [Linux Wi-Fi Driver Tutorial: How to Write a Simple Linux Wireless Driver Prototype](https://www.apriorit.com/dev-blog/645-lin-linux-wi-fi-driver-tutorial-how-to-write-simple-linux-wireless-driver-prototype)
- [mac80211_hwsim](https://www.kernel.org/doc/html/latest/networking/mac80211_hwsim/mac80211_hwsim.html)
- [Emulating WLAN in Linux - part I: the 802.11 stack](https://linuxembedded.fr/2020/05/emulating-wlan-in-linux-part-i-the-80211-stack)
- [Emulating WLAN in Linux - part II: mac80211_hwsim](https://linuxembedded.fr/2021/01/emulating-wlan-in-linux-part-ii-mac80211hwsim)
- [virt_wifi](https://github.com/torvalds/linux/blob/master/drivers/net/wireless/virt_wifi.c)