Add Airplane Mode + Host Network Interface Details (#204)

Adds Airplane Mode function to settings, host states, etc.
Windows implemented only for now.

Closes #203

Co-authored-by: crueter <swurl@swurl.xyz>
Co-authored-by: Aleksandr Popovich <alekpopo@pm.me>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/204
Co-authored-by: Maufeat <sahyno1996@gmail.com>
Co-committed-by: Maufeat <sahyno1996@gmail.com>
This commit is contained in:
Maufeat 2025-06-26 18:55:34 +00:00 committed by crueter
parent b2e602325c
commit 2e6a289a0b
34 changed files with 1193 additions and 203 deletions

View file

@ -0,0 +1,101 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include "common/logging/log.h"
#include "core/internal_network/emu_net_state.h"
#include "core/internal_network/network.h"
#include "core/internal_network/network_interface.h"
#ifdef _WIN32
#define NOMINMAX
#include <windows.h>
#include <wlanapi.h>
#pragma comment(lib, "wlanapi.lib")
#endif
#include <common/settings.h>
namespace Network {
EmuNetState& EmuNetState::Get() {
static EmuNetState instance;
return instance;
}
u8 QualityToBars(u8 q) {
if (q == 0)
return 0;
else if (q < 34)
return 1;
else if (q < 67)
return 2;
else
return 3;
}
void RefreshFromHost() {
auto& st = Network::EmuNetState::Get();
std::scoped_lock lk{st.mtx};
const auto sel = GetSelectedNetworkInterface();
if (!sel.has_value()) {
st.connected = false;
st.via_wifi = false;
st.wifi_enabled = false;
st.ethernet_enabled = false;
std::memset(st.ssid, 0, sizeof(st.ssid));
st.secure = false;
st.bars = 0;
return;
}
st.wifi_enabled = !Settings::values.airplane_mode.GetValue();
st.ethernet_enabled = sel->kind == HostAdapterKind::Ethernet;
st.connected = true;
st.via_wifi = sel->kind == HostAdapterKind::Wifi;
std::strncpy(st.ssid, sel->name.c_str(), sizeof(st.ssid) - 1);
st.secure = true;
st.ip = TranslateIPv4(sel->ip_address);
st.mask = TranslateIPv4(sel->subnet_mask);
st.gw = TranslateIPv4(sel->gateway);
#ifdef _WIN32
if (st.via_wifi) {
HANDLE hClient{};
DWORD ver{};
if (WlanOpenHandle(2, nullptr, &ver, &hClient) == ERROR_SUCCESS) {
PWLAN_INTERFACE_INFO_LIST ifs{};
if (WlanEnumInterfaces(hClient, nullptr, &ifs) == ERROR_SUCCESS) {
const auto& g = ifs->InterfaceInfo[0].InterfaceGuid;
PWLAN_CONNECTION_ATTRIBUTES attr{};
DWORD attrSize = 0;
WLAN_OPCODE_VALUE_TYPE opType{};
if (WlanQueryInterface(hClient, &g, wlan_intf_opcode_current_connection, nullptr,
&attrSize, reinterpret_cast<PVOID*>(&attr),
&opType) == ERROR_SUCCESS) {
const DOT11_SSID& ssid = attr->wlanAssociationAttributes.dot11Ssid;
const size_t len = std::min<size_t>(ssid.uSSIDLength, sizeof(st.ssid) - 1);
std::memset(st.ssid, 0, sizeof(st.ssid));
std::memcpy(st.ssid, ssid.ucSSID, len);
st.bars = QualityToBars(
static_cast<u8>(attr->wlanAssociationAttributes.wlanSignalQuality));
WlanFreeMemory(attr);
}
WlanFreeMemory(ifs);
}
WlanCloseHandle(hClient, nullptr);
}
} else {
st.bars = 3;
}
#else
/* non-Windows stub */
st.bars = st.via_wifi ? 2 : 3;
#endif
}
} // namespace Network

View file

@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <atomic>
#include <common/socket_types.h>
namespace Network {
struct EmuNetState {
static EmuNetState& Get();
EmuNetState(const EmuNetState&) = delete;
EmuNetState& operator=(const EmuNetState&) = delete;
EmuNetState(EmuNetState&&) = delete;
EmuNetState& operator=(EmuNetState&&) = delete;
std::atomic<bool> wifi_enabled{true};
std::atomic<bool> ethernet_enabled{true};
std::mutex mtx;
bool connected = false;
bool via_wifi = false;
char ssid[20] = {};
u8 bars = 0;
bool secure = false;
IPv4Address ip{}, mask{}, gw{};
private:
EmuNetState() = default;
};
void RefreshFromHost();
u8 QualityToBars(u8 quality);
} // namespace Network

View file

@ -12,6 +12,7 @@
#include "common/polyfill_ranges.h"
#include "common/settings.h"
#include "common/string_util.h"
#include "core/internal_network/emu_net_state.h"
#include "core/internal_network/network_interface.h"
#ifdef _WIN32
@ -27,65 +28,65 @@ namespace Network {
#ifdef _WIN32
std::vector<NetworkInterface> GetAvailableNetworkInterfaces() {
std::vector<IP_ADAPTER_ADDRESSES> adapter_addresses;
DWORD ret = ERROR_BUFFER_OVERFLOW;
DWORD buf_size = 0;
// retry up to 5 times
for (int i = 0; i < 5 && ret == ERROR_BUFFER_OVERFLOW; i++) {
ret = GetAdaptersAddresses(
ULONG buf_size = 0;
if (GetAdaptersAddresses(
AF_INET, GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER | GAA_FLAG_INCLUDE_GATEWAYS,
nullptr, adapter_addresses.data(), &buf_size);
if (ret != ERROR_BUFFER_OVERFLOW) {
break;
}
adapter_addresses.resize((buf_size / sizeof(IP_ADAPTER_ADDRESSES)) + 1);
nullptr, nullptr, &buf_size) != ERROR_BUFFER_OVERFLOW) {
LOG_ERROR(Network, "GetAdaptersAddresses(overrun probe) failed");
return {};
}
if (ret != NO_ERROR) {
LOG_ERROR(Network, "Failed to get network interfaces with GetAdaptersAddresses");
std::vector<u8> buffer(buf_size, 0);
auto* addrs = reinterpret_cast<PIP_ADAPTER_ADDRESSES>(buffer.data());
if (GetAdaptersAddresses(
AF_INET, GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER | GAA_FLAG_INCLUDE_GATEWAYS,
nullptr, addrs, &buf_size) != NO_ERROR) {
LOG_ERROR(Network, "GetAdaptersAddresses(data) failed");
return {};
}
std::vector<NetworkInterface> result;
for (auto current_address = adapter_addresses.data(); current_address != nullptr;
current_address = current_address->Next) {
if (current_address->FirstUnicastAddress == nullptr ||
current_address->FirstUnicastAddress->Address.lpSockaddr == nullptr) {
for (auto* a = addrs; a; a = a->Next) {
if (a->OperStatus != IfOperStatusUp || !a->FirstUnicastAddress ||
!a->FirstUnicastAddress->Address.lpSockaddr)
continue;
}
if (current_address->OperStatus != IfOperStatusUp) {
const in_addr ip =
reinterpret_cast<sockaddr_in*>(a->FirstUnicastAddress->Address.lpSockaddr)->sin_addr;
ULONG mask_raw = 0;
if (ConvertLengthToIpv4Mask(a->FirstUnicastAddress->OnLinkPrefixLength, &mask_raw) !=
NO_ERROR)
continue;
}
const auto ip_addr = Common::BitCast<struct sockaddr_in>(
*current_address->FirstUnicastAddress->Address.lpSockaddr)
.sin_addr;
in_addr mask{.S_un{.S_addr{mask_raw}}};
ULONG mask = 0;
if (ConvertLengthToIpv4Mask(current_address->FirstUnicastAddress->OnLinkPrefixLength,
&mask) != NO_ERROR) {
LOG_ERROR(Network, "Failed to convert IPv4 prefix length to subnet mask");
continue;
}
in_addr gw{.S_un{.S_addr{0}}};
if (a->FirstGatewayAddress && a->FirstGatewayAddress->Address.lpSockaddr)
gw = reinterpret_cast<sockaddr_in*>(a->FirstGatewayAddress->Address.lpSockaddr)
->sin_addr;
struct in_addr gateway = {.S_un{.S_addr{0}}};
if (current_address->FirstGatewayAddress != nullptr &&
current_address->FirstGatewayAddress->Address.lpSockaddr != nullptr) {
gateway = Common::BitCast<struct sockaddr_in>(
*current_address->FirstGatewayAddress->Address.lpSockaddr)
.sin_addr;
HostAdapterKind kind = HostAdapterKind::Ethernet;
switch (a->IfType) {
case IF_TYPE_IEEE80211: // 802.11 Wi-Fi
kind = HostAdapterKind::Wifi;
break;
default:
kind = HostAdapterKind::Ethernet;
break;
}
result.emplace_back(NetworkInterface{
.name{Common::UTF16ToUTF8(std::wstring{current_address->FriendlyName})},
.ip_address{ip_addr},
.subnet_mask = in_addr{.S_un{.S_addr{mask}}},
.gateway = gateway});
.name = Common::UTF16ToUTF8(std::wstring{a->FriendlyName}),
.ip_address = ip,
.subnet_mask = mask,
.gateway = gw,
.kind = kind
});
}
return result;
@ -197,6 +198,7 @@ std::vector<NetworkInterface> GetAvailableNetworkInterfaces() {
#endif // _WIN32
std::optional<NetworkInterface> GetSelectedNetworkInterface() {
const auto& selected_network_interface = Settings::values.network_interface.GetValue();
const auto network_interfaces = Network::GetAvailableNetworkInterfaces();
if (network_interfaces.empty()) {

View file

@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@ -15,15 +18,26 @@
namespace Network {
enum class HostAdapterKind { Wifi, Ethernet };
struct NetworkInterface {
std::string name;
struct in_addr ip_address;
struct in_addr subnet_mask;
struct in_addr gateway;
HostAdapterKind kind{HostAdapterKind::Ethernet};
};
std::vector<NetworkInterface> GetAvailableNetworkInterfaces();
std::optional<NetworkInterface> GetSelectedNetworkInterface();
void SelectFirstNetworkInterface();
struct HostAdapter {
in_addr ip_address;
in_addr subnet_mask;
in_addr gateway;
std::string name;
HostAdapterKind kind;
};
} // namespace Network

View file

@ -0,0 +1,186 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <chrono>
#include <cstring>
#include <thread>
#include <vector>
#include "common/logging/log.h"
#include "core/internal_network/wifi_scanner.h"
using namespace std::chrono_literals;
namespace Network {
#ifdef _WIN32
#define NOMINMAX
#include <windows.h>
#include <wlanapi.h>
#pragma comment(lib, "wlanapi.lib")
static u8 QualityToPercent(DWORD q) {
return static_cast<u8>(q);
}
static std::vector<ScanData> ScanWifiWin(std::chrono::milliseconds deadline) {
std::vector<ScanData> out;
HANDLE hClient{};
DWORD ver{};
if (WlanOpenHandle(2, nullptr, &ver, &hClient) != ERROR_SUCCESS)
return out;
PWLAN_INTERFACE_INFO_LIST ifs{};
if (WlanEnumInterfaces(hClient, nullptr, &ifs) != ERROR_SUCCESS || ifs->dwNumberOfItems == 0) {
WlanCloseHandle(hClient, nullptr);
return out;
}
// fire a scan on every adapter
for (DWORD i = 0; i < ifs->dwNumberOfItems; ++i)
WlanScan(hClient, &ifs->InterfaceInfo[i].InterfaceGuid, nullptr, nullptr, nullptr);
const auto start = std::chrono::steady_clock::now();
bool have = false;
while (!have && std::chrono::steady_clock::now() - start < deadline) {
std::this_thread::sleep_for(100ms);
out.clear();
for (DWORD i = 0; i < ifs->dwNumberOfItems; ++i) {
PWLAN_AVAILABLE_NETWORK_LIST list{};
if (WlanGetAvailableNetworkList(hClient, &ifs->InterfaceInfo[i].InterfaceGuid,
WLAN_AVAILABLE_NETWORK_INCLUDE_ALL_ADHOC_PROFILES,
nullptr, &list) != ERROR_SUCCESS)
continue;
for (DWORD n = 0; n < list->dwNumberOfItems; ++n) {
const auto& nw = list->Network[n];
ScanData sd{};
sd.ssid_len = static_cast<u8>(nw.dot11Ssid.uSSIDLength);
std::memcpy(sd.ssid, nw.dot11Ssid.ucSSID, sd.ssid_len);
sd.quality = QualityToPercent(nw.wlanSignalQuality);
if (nw.bNetworkConnectable)
sd.flags |= 1;
if (nw.bSecurityEnabled)
sd.flags |= 2;
out.emplace_back(sd);
char tmp[0x22]{};
std::memcpy(tmp, sd.ssid, sd.ssid_len);
LOG_INFO(Network, "[WifiScan] +%u \"%s\" quality=%u flags=0x%X", sd.ssid_len, tmp,
sd.quality, sd.flags);
}
WlanFreeMemory(list);
}
have = !out.empty();
}
WlanFreeMemory(ifs);
WlanCloseHandle(hClient, nullptr);
return out;
}
#endif /* _WIN32 */
#if defined(__linux__) && !defined(_WIN32) && !defined(ANDROID)
#include <iwlib.h>
static u8 QualityToPercent(const iwrange& r, const wireless_scan* ws) {
const iw_quality qual = ws->stats.qual;
const int lvl = qual.level;
const int max = r.max_qual.level ? r.max_qual.level : 100;
return static_cast<u8>(std::clamp(100 * lvl / max, 0, 100));
}
static int wifi_callback(int skfd, char* ifname, char* args[], int count)
{
iwrange range;
int res = iw_get_range_info(skfd, ifname, &range);
LOG_INFO(Network, "ifname {} returned {} on iw_get_range_info", ifname, res);
if (res >= 0) {
strncpy(args[0], ifname, IFNAMSIZ - 1);
args[0][IFNAMSIZ - 1] = 0;
return 1;
}
return 0;
}
// TODO(crueter, Maufeat): Check if driver supports wireless extensions, fallback to nl80211 if not
static std::vector<ScanData> ScanWifiLinux(std::chrono::milliseconds deadline) {
std::vector<ScanData> out;
int sock = iw_sockets_open();
if (sock < 0) {
LOG_ERROR(Network, "iw_sockets_open() failed");
return out;
}
char ifname[IFNAMSIZ] = {0};
char *args[1] = {ifname};
iw_enum_devices(sock, &wifi_callback, args, 0);
if (strlen(ifname) == 0) {
LOG_WARNING(Network, "No wireless interface found");
iw_sockets_close(sock);
return out;
}
iwrange range{};
if (iw_get_range_info(sock, ifname, &range) < 0) {
LOG_WARNING(Network, "iw_get_range_info failed on {}", ifname);
iw_sockets_close(sock);
return out;
}
wireless_scan_head head{};
const auto start = std::chrono::steady_clock::now();
bool have = false;
while (!have && std::chrono::steady_clock::now() - start < deadline) {
std::this_thread::sleep_for(100ms);
if (iw_scan(sock, ifname, range.we_version_compiled, &head) != 0)
continue;
out.clear();
for (auto* ws = head.result; ws; ws = ws->next) {
if (!ws->b.has_essid)
continue;
ScanData sd{};
sd.ssid_len = static_cast<u8>(std::min<int>(ws->b.essid_len, 0x20));
std::memcpy(sd.ssid, ws->b.essid, sd.ssid_len);
sd.quality = QualityToPercent(range, ws);
sd.flags |= 1;
if (ws->b.has_key)
sd.flags |= 2;
out.emplace_back(sd);
char tmp[0x22]{};
std::memcpy(tmp, sd.ssid, sd.ssid_len);
}
have = !out.empty();
}
iw_sockets_close(sock);
return out;
}
#endif /* linux */
std::vector<ScanData> ScanWifiNetworks(std::chrono::milliseconds deadline) {
#ifdef _WIN32
return ScanWifiWin(deadline);
#elif defined(__linux__) && !defined(ANDROID)
return ScanWifiLinux(deadline);
#else
std::this_thread::sleep_for(deadline);
return {}; // unsupported host, pretend no results
#endif
}
} // namespace Network

View file

@ -0,0 +1,22 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <chrono>
#include <vector>
#include "core/core.h"
namespace Network {
struct ScanData {
u8 ssid_len{};
char ssid[0x21]{};
u8 quality{};
u32 flags{};
};
static_assert(sizeof(ScanData) <= 0x2C, "ScanData layout changed update conversions!");
std::vector<ScanData> ScanWifiNetworks(std::chrono::milliseconds deadline);
}