OpenWrt hwsim fixes for emulated AP testing

Fixes for running vwifi between an OpenWrt DUT VM and a Linux test
runner with mac80211_hwsim radios:

1. pmaddr fallback (ckernelwifi.cc):
   ethtool GPERMADDR fails on OpenWrt hwsim VIFs (wlanc0/wlanc1).
   handle_new_winet_notification() and handle_init_winet_notification()
   now fall back to nl80211 MAC when get_pmaddr() returns false,
   instead of silently dropping the VIF.

2. hwsim MAC 0x40 bit fix (cwirelessdevice.cc):
   Set locally-administered bit on machwsim in constructor and
   setMachwsim() to match the kernel's hwsim rhashtable key format.
   Fixes frame delivery to radios created via netlink.

3. VIF name regex fix (cwirelessdevice.cc):
   wlan regex changed from "wlan[0-9]*" to "wlan[a-z]?[0-9]*" to
   match OpenWrt captive portal VIFs (wlanc0, wlanc1).

4. Frame deduplication (ckernelwifi.cc):
   Multiple BSS VIFs (wlan0, wlan0-1) share the same PHY permanent
   MAC. Without dedup, frames are delivered N times to the same radio,
   causing duplicate auth/assoc and breaking WPA handshakes.

5. Frequency routing (ckernelwifi.cc, ckernelwifi.h):
   Track each radio's channel from TX frames. Skip radios not tuned
   to the frame's frequency in process_messages (local delivery).
   Disabled for recv_from_server (kernel handles RX freq filtering).

6. freq=0 passthrough for recv_from_server (ckernelwifi.cc):
   Pass freq=0 to send_cloned_frame_msg so the kernel skips its
   HWSIM_ATTR_FREQ check. Prevents drops from TCP timing races
   between scan dwell changes and frame delivery.

7. TCP_NODELAY (csocketclientitcp.cc, csocketserverfunctionitcp.cc):
   Disable Nagle's algorithm for lower latency frame relay.

8. writev for atomic sends (cwifi.cc):
   Replace two separate send() calls (power + data) with a single
   writev() to prevent interleaving with concurrent writers.

9. Netlink buffer increase (ckernelwifi.cc):
   nl_socket_set_buffer_size 16MB to prevent frame drops under load.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Guy Resheff
2026-03-08 08:20:07 -07:00
co-authored by Claude Opus 4.6
parent 92df7a383a
commit 4d144cd2f4
6 changed files with 200 additions and 29 deletions
+148 -6
View File
@@ -36,6 +36,31 @@ CDynBuffer Buffer;
/* allow calling non static function from static function */ /* allow calling non static function from static function */
ckernelwifi::CallFromStaticFunc * CKernelWifi::forward = nullptr ; ckernelwifi::CallFromStaticFunc * CKernelWifi::forward = nullptr ;
/* Frequency routing: track each radio's channel from its TX frames */
void CKernelWifi::update_radio_freq(const struct ether_addr* mac, TFrequency freq) {
if (freq == 0) return;
for (int i = 0; i < _radio_freq_cache_size; i++) {
if (memcmp(&_radio_freq_cache[i].mac, mac, sizeof(struct ether_addr)) == 0) {
_radio_freq_cache[i].freq = freq;
return;
}
}
if (_radio_freq_cache_size < MAX_RADIO_FREQ_CACHE) {
_radio_freq_cache[_radio_freq_cache_size].mac = *mac;
_radio_freq_cache[_radio_freq_cache_size].freq = freq;
_radio_freq_cache_size++;
}
}
TFrequency CKernelWifi::get_radio_freq(const struct ether_addr* mac) const {
for (int i = 0; i < _radio_freq_cache_size; i++) {
if (memcmp(&_radio_freq_cache[i].mac, mac, sizeof(struct ether_addr)) == 0) {
return _radio_freq_cache[i].freq;
}
}
return 0; /* unknown — deliver frame (conservative fallback) */
}
void CKernelWifi::cout_mac_address(struct ether_addr *src) void CKernelWifi::cout_mac_address(struct ether_addr *src)
{ {
char addr[18]; char addr[18];
@@ -210,14 +235,47 @@ int CKernelWifi::process_messages(struct nl_msg *msg)
else else
freq = 0; freq = 0;
/* Update frequency cache for this transmitting radio */
update_radio_freq(&macsrchwsim, freq);
{
static int _tx_dbg = 0;
if (_tx_dbg < 10) {
char _txm[18];
sprintf(_txm, "%02X:%02X:%02X:%02X:%02X:%02X", macsrchwsim.ether_addr_octet[0], macsrchwsim.ether_addr_octet[1], macsrchwsim.ether_addr_octet[2], macsrchwsim.ether_addr_octet[3], macsrchwsim.ether_addr_octet[4], macsrchwsim.ether_addr_octet[5]);
std::cerr << "TX process_messages #" << _tx_dbg << " src_mac=" << _txm << " freq=" << freq << std::endl;
_tx_dbg++;
}
}
int rate_idx = 7; // number of attempts int rate_idx = 7; // number of attempts
const auto& inets = _list_winterfaces.list_devices(); const auto& inets = _list_winterfaces.list_devices();
/* Deduplicate by permanent hwsim MAC (see recv_from_server comment) */
struct ether_addr sent_macs_local[16];
int n_sent_local = 0;
for (const auto& inet : inets) for (const auto& inet : inets)
{ {
struct ether_addr macdsthwsim = inet.getMachwsim(); struct ether_addr macdsthwsim = inet.getMachwsim();
if( memcmp(&macsrchwsim,&macdsthwsim,sizeof(struct ether_addr)) ) // if( macsrchwsim != macdsthwsim ) if( memcmp(&macsrchwsim,&macdsthwsim,sizeof(struct ether_addr)) ) {
send_cloned_frame_msg(&macdsthwsim, data, data_len, rate_idx, power, freq); bool dup = false;
for (int i = 0; i < n_sent_local; i++) {
if (memcmp(&sent_macs_local[i], &macdsthwsim, sizeof(struct ether_addr)) == 0) {
dup = true;
break;
}
}
if (!dup) {
if (n_sent_local < 16)
sent_macs_local[n_sent_local++] = macdsthwsim;
/* Frequency routing: skip radios not on this channel.
* freq=0 in frame: deliver to all (no freq info).
* target_freq=0: radio not tuned yet, skip it. */
TFrequency target_freq = get_radio_freq(&macdsthwsim);
if (freq != 0 && target_freq != 0 && target_freq != freq)
continue;
send_cloned_frame_msg(&macdsthwsim, data, data_len, rate_idx, power, freq);
}
}
} }
delete &inets; delete &inets;
// <------------------------ // <------------------------
@@ -317,6 +375,10 @@ int CKernelWifi::init_netlink_first(void)
perror("setsockopt"); perror("setsockopt");
} }
/* Increase netlink receive buffer to 4MB to prevent frame drops under load.
* Must be after genl_connect() which assigns the socket fd. */
nl_socket_set_buffer_size(_netlink_socket, 16 * 1024 * 1024, 0);
return 1; return 1;
} }
@@ -398,6 +460,10 @@ int CKernelWifi::init_netlink(void)
perror("setsockopt"); perror("setsockopt");
} }
/* Increase netlink receive buffer to 4MB to prevent frame drops under load.
* Must be after genl_connect() which assigns the socket fd. */
nl_socket_set_buffer_size(_netlink_socket, 16 * 1024 * 1024, 0);
return 1; return 1;
} }
@@ -442,11 +508,18 @@ int CKernelWifi::send_cloned_frame_msg(struct ether_addr *dst, char *data, int d
// nl_send_auto_complete(_netlink_socket, msg); // nl_send_auto_complete(_netlink_socket, msg);
if (nl_send_auto(_netlink_socket, msg) < 0) int _nl_rc = nl_send_auto(_netlink_socket, msg);
static int _send_count = 0;
_send_count++;
if (_nl_rc < 0)
{ {
std::cerr << "nl_send_auto FAILED: rc=" << _nl_rc << " count=" << _send_count << std::endl;
nlmsg_free(msg); nlmsg_free(msg);
return 0 ; return 0 ;
} }
if (_send_count <= 10 || _send_count % 100 == 0) {
std::cerr << "nl_send_auto OK: rc=" << _nl_rc << " count=" << _send_count << " freq=" << freq << std::endl;
}
nlmsg_free(msg); nlmsg_free(msg);
@@ -455,6 +528,21 @@ int CKernelWifi::send_cloned_frame_msg(struct ether_addr *dst, char *data, int d
void CKernelWifi::recv_from_server(){ void CKernelWifi::recv_from_server(){
static int _dbg_count = 0;
if (_dbg_count < 5 || _dbg_count % 1000 == 0) {
const auto& _dbg_inets = _list_winterfaces.list_devices();
std::cerr << "recv_from_server #" << _dbg_count << " list_size=" << _dbg_inets.size();
for (const auto& _di : _dbg_inets) {
char _dm[18];
const struct ether_addr _dpm = _di.getMachwsim();
sprintf(_dm, "%02X:%02X:%02X:%02X:%02X:%02X", _dpm.ether_addr_octet[0], _dpm.ether_addr_octet[1], _dpm.ether_addr_octet[2], _dpm.ether_addr_octet[3], _dpm.ether_addr_octet[4], _dpm.ether_addr_octet[5]);
std::cerr << " [" << _di.getName() << "=" << _dm << "]";
}
std::cerr << std::endl;
delete &_dbg_inets;
}
_dbg_count++;
if ( ! is_connected_to_server()) if ( ! is_connected_to_server())
return ; return ;
@@ -535,11 +623,52 @@ void CKernelWifi::recv_from_server(){
const auto& inets = _list_winterfaces.list_devices(); const auto& inets = _list_winterfaces.list_devices();
/* Deduplicate by permanent hwsim MAC: multiple BSS interfaces
* (wlan0, wlan0-1, wlan0-2) share the same PHY permanent MAC.
* Without dedup, each frame is delivered N times to the same radio,
* causing duplicate auth/assoc and breaking WPA handshakes. */
struct ether_addr sent_macs[16];
int n_sent = 0;
for (const auto& inet : inets) for (const auto& inet : inets)
{ {
struct ether_addr macdsthwsim = inet.getMachwsim(); struct ether_addr macdsthwsim = inet.getMachwsim();
send_cloned_frame_msg(&macdsthwsim, data, data_len, rate_idx, signal, freq); bool dup = false;
for (int i = 0; i < n_sent; i++) {
if (memcmp(&sent_macs[i], &macdsthwsim, sizeof(struct ether_addr)) == 0) {
dup = true;
break;
}
}
if (dup)
continue;
if (n_sent < 16)
sent_macs[n_sent++] = macdsthwsim;
/* Frequency routing: skip radios not on this channel.
* freq=0 in frame: deliver to all (no freq info).
* target_freq=0: radio freq unknown, deliver anyway. */
TFrequency target_freq = get_radio_freq(&macdsthwsim);
if (_dbg_count < 20) {
char _fm[18];
sprintf(_fm, "%02X:%02X:%02X:%02X:%02X:%02X", macdsthwsim.ether_addr_octet[0], macdsthwsim.ether_addr_octet[1], macdsthwsim.ether_addr_octet[2], macdsthwsim.ether_addr_octet[3], macdsthwsim.ether_addr_octet[4], macdsthwsim.ether_addr_octet[5]);
std::cerr << " freq_route: mac=" << _fm << " frame_freq=" << freq << " target_freq=" << target_freq << (((freq != 0 && target_freq != 0 && target_freq != freq) ? " SKIP" : " DELIVER")) << std::endl;
}
/* Frequency routing disabled for recv_from_server: kernel
* mac80211_hwsim handles RX frequency filtering internally.
* The userspace check races with scan dwell timing over TCP,
* causing 5GHz beacons to be dropped during client scans. */
// if (freq != 0 && target_freq != 0 && target_freq != freq)
// continue;
/* Pass freq=0 so the kernel skips its own HWSIM_ATTR_FREQ
* check in hwsim_cloned_frame_received_nl. Without this,
* the kernel drops frames whose freq doesn't match the
* radio's current channel — which races with scan dwell
* timing over TCP. The kernel's mac80211 RX path handles
* frequency validation at a higher level. */
send_cloned_frame_msg(&macdsthwsim, data, data_len, rate_idx, signal, 0);
} }
delete &inets; delete &inets;
} }
@@ -550,6 +679,9 @@ void CKernelWifi::monitor_hwsim_loop()
sock = nl_socket_alloc(); sock = nl_socket_alloc();
genl_connect(sock); genl_connect(sock);
/* Increase netlink receive buffer to 4MB to prevent frame drops under load.
* Must be after genl_connect() which assigns the socket fd. */
nl_socket_set_buffer_size(sock, 16 * 1024 * 1024, 0);
/* loop for waiting incoming msg from hwsim driver*/ /* loop for waiting incoming msg from hwsim driver*/
while (true) { while (true) {
@@ -1008,8 +1140,13 @@ void CKernelWifi::handle_new_winet_notification(WirelessDevice wirelessdevice){
//paddr.ether_addr_octet[0] |= 0x40 ; //paddr.ether_addr_octet[0] |= 0x40 ;
wirelessdevice.setMachwsim(paddr); wirelessdevice.setMachwsim(paddr);
_list_winterfaces.add_device(wirelessdevice); } else {
/* Fallback: ethtool GPERMADDR fails on OpenWrt hwsim VIFs.
Use the nl80211 MAC (addresses[0]) — setMachwsim applies |=0x40
to derive addresses[1] for the hwsim rhashtable key. */
wirelessdevice.setMachwsim(wirelessdevice.getMacaddr());
} }
_list_winterfaces.add_device(wirelessdevice);
//std::cout << __func__ << _list_winterfaces << std::endl ; //std::cout << __func__ << _list_winterfaces << std::endl ;
@@ -1032,8 +1169,13 @@ void CKernelWifi::handle_init_winet_notification(WirelessDevice wirelessdevice){
//paddr.ether_addr_octet[0] |= 0x40 ; //paddr.ether_addr_octet[0] |= 0x40 ;
wirelessdevice.setMachwsim(paddr); wirelessdevice.setMachwsim(paddr);
_list_winterfaces.add_device(wirelessdevice); } else {
/* Fallback: ethtool GPERMADDR fails on OpenWrt hwsim VIFs.
Use the nl80211 MAC (addresses[0]) — setMachwsim applies |=0x40
to derive addresses[1] for the hwsim rhashtable key. */
wirelessdevice.setMachwsim(wirelessdevice.getMacaddr());
} }
_list_winterfaces.add_device(wirelessdevice);
//std::cout << __func__ << _list_winterfaces << std::endl ; //std::cout << __func__ << _list_winterfaces << std::endl ;
} }
+17
View File
@@ -16,6 +16,7 @@
#include <condition_variable> #include <condition_variable>
#include "cdynbuffer.h" #include "cdynbuffer.h"
#include "types.h"
namespace ckernelwifi{ namespace ckernelwifi{
@@ -39,6 +40,22 @@ class CKernelWifi : public intthread::AsyncTask {
WirelessDeviceList _list_winterfaces ; WirelessDeviceList _list_winterfaces ;
/* Frequency routing: cache each radio's last-seen frequency.
* Updated from TX frames (HWSIM_ATTR_FREQ). Used to skip
* radios not tuned to the frame's frequency, eliminating
* the N-radio broadcast overhead. freq=0 means unknown
* (radio hasn't transmitted yet) — deliver as fallback. */
static constexpr int MAX_RADIO_FREQ_CACHE = 16;
struct RadioFreqEntry {
struct ether_addr mac;
TFrequency freq;
};
RadioFreqEntry _radio_freq_cache[MAX_RADIO_FREQ_CACHE] = {};
int _radio_freq_cache_size = 0;
void update_radio_freq(const struct ether_addr* mac, TFrequency freq);
TFrequency get_radio_freq(const struct ether_addr* mac) const;
/** pointer for netlink socket */ /** pointer for netlink socket */
struct nl_sock * _netlink_socket { nullptr }; struct nl_sock * _netlink_socket { nullptr };
+4
View File
@@ -5,6 +5,7 @@
#include <fcntl.h> // open #include <fcntl.h> // open
#include <arpa/inet.h> // INADDR_ANY #include <arpa/inet.h> // INADDR_ANY
#include <netinet/tcp.h>
#include <unistd.h> // close #include <unistd.h> // close
#include <assert.h> // assert #include <assert.h> // assert
@@ -34,6 +35,9 @@ bool CSocketClientITCP::_Configure()
return false; return false;
} }
int nodelay = 1;
setsockopt(Master, IPPROTO_TCP, TCP_NODELAY, (char *)&nodelay, sizeof(nodelay));
return true; return true;
} }
+4
View File
@@ -5,6 +5,7 @@
#include <arpa/inet.h> // INADDR_ANY #include <arpa/inet.h> // INADDR_ANY
#include <sys/socket.h> #include <sys/socket.h>
#include <netinet/tcp.h>
#include "csocket.h" // SOCKET_ERROR #include "csocket.h" // SOCKET_ERROR
#include "csocketserverfunctionitcp.h" #include "csocketserverfunctionitcp.h"
@@ -91,5 +92,8 @@ TDescriptor CSocketServerFunctionITCP::_Accept(TDescriptor master, TCID& cid)
cid=hash_ipaddr(&address); cid=hash_ipaddr(&address);
int nodelay = 1;
setsockopt(new_socket, IPPROTO_TCP, TCP_NODELAY, (char *)&nodelay, sizeof(nodelay));
return new_socket; return new_socket;
} }
+24 -21
View File
@@ -8,6 +8,7 @@
#include "hwsim.h" // HWSIM_ATTR_FREQ #include "hwsim.h" // HWSIM_ATTR_FREQ
#include <netlink/genl/genl.h> // genlmsg_parse #include <netlink/genl/genl.h> // genlmsg_parse
#include <sys/uio.h> // writev
#include "cwifi.h" #include "cwifi.h"
//#include "config.h" //#include "config.h"
@@ -64,34 +65,36 @@ bool CWifi::PacketIsLost(TPower signalLevel)
ssize_t CWifi::SendSignalWithSocket(CSocket* socket, TDescriptor descriptor, TPower* power, const char* buffer, int sizeOfBuffer) ssize_t CWifi::SendSignalWithSocket(CSocket* socket, TDescriptor descriptor, TPower* power, const char* buffer, int sizeOfBuffer)
{ {
// cout<<"send power : "<<power<<endl; struct iovec iov[2];
int val=socket->Send(descriptor, reinterpret_cast<const char*>(power), sizeof(TPower)); iov[0].iov_base = power;
if( val <= 0 ) iov[0].iov_len = sizeof(TPower);
return val; iov[1].iov_base = const_cast<char*>(buffer);
iov[1].iov_len = sizeOfBuffer;
// std::cout<<"send big data of size : "<<sizeOfBuffer<<std::endl; ssize_t total = sizeof(TPower) + sizeOfBuffer;
return socket->Send(descriptor, buffer, sizeOfBuffer); ssize_t ret = writev(descriptor, iov, 2);
if (ret != total)
return SOCKET_ERROR;
return ret;
} }
ssize_t CWifi::RecvSignalWithSocket(CSocket* socket, TDescriptor descriptor, TPower* power, CDynBuffer* buffer) ssize_t CWifi::RecvSignalWithSocket(CSocket* socket, TDescriptor descriptor, TPower* power, CDynBuffer* buffer)
{ {
int valread; ssize_t n;
n=socket->Read(descriptor, (char*)power, sizeof(TPower));
// read the power if( n == SOCKET_ERROR )
valread = socket->Read(descriptor, reinterpret_cast<char*>(power), sizeof(TPower));
if ( valread <= 0 )
return valread;
// read the signal
// "nlmsg_len" (type "uint32_t") is the first attribut of the "struct nlmsghdr" in "libnl3/netlink/netlink-kernel.h"
ssize_t sizeRead = socket->ReadEqualSize(descriptor, buffer, 0, sizeof(struct nlmsghdr));
if( sizeRead == SOCKET_ERROR )
return SOCKET_ERROR; return SOCKET_ERROR;
int sizeTotal=reinterpret_cast<struct nlmsghdr *>(buffer->GetBuffer())->nlmsg_len; /* we read nlmsghdr to get the size of the message */
struct nlmsghdr *nlh;
buffer->NeededSize(sizeof(struct nlmsghdr),false);
n=socket->ReadEqualSize(descriptor, buffer, 0, sizeof(struct nlmsghdr));
if( n == SOCKET_ERROR )
return SOCKET_ERROR;
nlh = (struct nlmsghdr *)buffer->GetBuffer();
int sizeTotal=nlh->nlmsg_len;
if( sizeTotal > MTU ) // to avoid that a error packet overfulls the memory if( sizeTotal > MTU )
return SOCKET_ERROR; return SOCKET_ERROR;
return socket->ReadEqualSize(descriptor, buffer, sizeRead, sizeTotal); return socket->ReadEqualSize(descriptor, buffer, sizeof(struct nlmsghdr), sizeTotal);
} }
+3 -2
View File
@@ -7,7 +7,7 @@
#include <regex> #include <regex>
const std::regex wlan("wlan[0-9]*"); const std::regex wlan("wlan[a-z]?[0-9]*");
WirelessDevice::WirelessDevice(){ WirelessDevice::WirelessDevice(){
@@ -19,7 +19,7 @@ WirelessDevice::~WirelessDevice(){
WirelessDevice::WirelessDevice(const std::string & name,int index ,int iftype ,const struct ether_addr & macaddr,int txpower):_name(name),_index(index),_iftype(iftype), _txpower(txpower), _macaddr(macaddr), _machwsim(macaddr) { WirelessDevice::WirelessDevice(const std::string & name,int index ,int iftype ,const struct ether_addr & macaddr,int txpower):_name(name),_index(index),_iftype(iftype), _txpower(txpower), _macaddr(macaddr), _machwsim(macaddr) {
// _machwsim.ether_addr_octet[0] |= 0x40 ; _machwsim.ether_addr_octet[0] |= 0x40 ;
// std::memcpy(&_macaddr,&macaddr,ETH_ALEN); // std::memcpy(&_macaddr,&macaddr,ETH_ALEN);
@@ -43,6 +43,7 @@ struct ether_addr WirelessDevice::getMacaddr() const {
void WirelessDevice::setMachwsim(const struct ether_addr & machwsim) { void WirelessDevice::setMachwsim(const struct ether_addr & machwsim) {
_machwsim = machwsim ; _machwsim = machwsim ;
_machwsim.ether_addr_octet[0] |= 0x40 ;
// std::memcpy(&_macaddr,&macaddr,ETH_ALEN); // std::memcpy(&_macaddr,&macaddr,ETH_ALEN);
} }