WiFi 라우터 기능 복원

This commit is contained in:
seo
2026-07-20 11:26:40 +09:00
parent ada3c7dc74
commit 52b04453e7
10 changed files with 1022 additions and 16 deletions
+524
View File
@@ -2523,6 +2523,475 @@ function fan_spike_history(int $limit = 100): array
return $rows;
}
function dnsmasq_leases(): array
{
$map = [];
foreach (@file('/var/lib/misc/dnsmasq.leases', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [] as $line) {
$p = preg_split('/\s+/', trim($line));
if (count($p) >= 4) {
$mac = strtolower($p[1]);
$map[$mac] = [
'expire_ts' => (int)$p[0],
'mac' => $mac,
'ip' => $p[2],
'hostname' => $p[3] === '*' ? 'N/A' : $p[3],
];
}
}
return $map;
}
function wifi_client_aliases(): array
{
try {
$rows = db()->query("SELECT mac, hostname FROM wifi_client_aliases")->fetchAll();
} catch (Throwable) {
return [];
}
$aliases = [];
foreach ($rows as $row) {
$mac = strtolower((string)($row['mac'] ?? ''));
$hostname = trim((string)($row['hostname'] ?? ''));
if (wifi_valid_mac($mac) && $hostname !== '') {
$aliases[$mac] = $hostname;
}
}
return $aliases;
}
function save_wifi_client_alias(string $mac, string $hostname): array
{
$mac = strtolower(trim($mac));
$hostname = trim(preg_replace('/\s+/', ' ', $hostname) ?? '');
if (!wifi_valid_mac($mac)) {
throw new InvalidArgumentException('bad_mac');
}
if ($hostname === '' || mb_strlen($hostname) > 80) {
throw new InvalidArgumentException('bad_hostname');
}
$stmt = db()->prepare("
INSERT INTO wifi_client_aliases (mac, hostname)
VALUES (:mac, :hostname)
ON DUPLICATE KEY UPDATE
hostname = VALUES(hostname),
updated_at = CURRENT_TIMESTAMP
");
$stmt->execute([
':mac' => $mac,
':hostname' => $hostname,
]);
return [
'mac' => $mac,
'hostname' => $hostname,
];
}
function iw_station_dump(string $iface): string
{
if (!preg_match('/^[A-Za-z0-9_.:-]+$/', $iface)) {
return '';
}
return sh(['/usr/sbin/iw', 'dev', $iface, 'station', 'dump'], true, 4)['out'];
}
function parse_live_wifi_rows(string $iface, string $band, string $text, array $leases, array $aliases): array
{
$rows = [];
$cur = null;
foreach (explode("\n", $text) as $line) {
$t = trim($line);
if (preg_match('/^Station\s+([0-9a-f:]+)/i', $t, $m)) {
if ($cur !== null) {
$rows[] = $cur;
}
$mac = strtolower($m[1]);
$lease = $leases[$mac] ?? [];
$leaseHostname = (string)($lease['hostname'] ?? 'N/A');
$aliasHostname = $aliases[$mac] ?? null;
$hostname = $aliasHostname ?: $leaseHostname;
$hostnameSource = $aliasHostname ? 'alias' : ($leaseHostname === 'N/A' ? 'generated' : 'lease');
$cur = [
'band' => $band,
'iface' => $iface,
'mac' => $mac,
'ip' => $lease['ip'] ?? 'N/A',
'hostname' => $hostname === 'N/A' ? '기기-' . strtoupper(substr(str_replace(':', '', $mac), -6)) : $hostname,
'hostname_source' => $hostnameSource,
'name' => $hostname === 'N/A' ? $mac : $hostname,
'signal' => 'N/A',
'tx_bitrate' => 'N/A',
'rx_bitrate' => 'N/A',
'signal_source' => 'missing',
'tx_bitrate_source' => 'missing',
'rx_bitrate_source' => 'missing',
'connected_time' => 'N/A',
'inactive_time' => 'N/A',
'rx_bytes' => 0,
'tx_bytes' => 0,
'rx_packets' => 0,
'tx_packets' => 0,
'tx_failed' => 0,
];
continue;
}
if ($cur === null) {
continue;
}
if (preg_match('/^signal:\s+(.+)/', $t, $m)) {
$cur['signal'] = $m[1];
$cur['signal_source'] = 'iw';
} elseif (preg_match('/^tx bitrate:\s+(.+)/', $t, $m)) {
$cur['tx_bitrate'] = $m[1];
$cur['tx_bitrate_source'] = 'iw';
} elseif (preg_match('/^rx bitrate:\s+(.+)/', $t, $m)) {
$cur['rx_bitrate'] = $m[1];
$cur['rx_bitrate_source'] = 'iw';
}
elseif (preg_match('/^connected time:\s+(.+)/', $t, $m)) $cur['connected_time'] = $m[1];
elseif (preg_match('/^inactive time:\s+(.+)/', $t, $m)) $cur['inactive_time'] = $m[1];
elseif (preg_match('/^rx bytes:\s+(\d+)/', $t, $m)) $cur['rx_bytes'] = (int)$m[1];
elseif (preg_match('/^tx bytes:\s+(\d+)/', $t, $m)) $cur['tx_bytes'] = (int)$m[1];
elseif (preg_match('/^rx packets:\s+(\d+)/', $t, $m)) $cur['rx_packets'] = (int)$m[1];
elseif (preg_match('/^tx packets:\s+(\d+)/', $t, $m)) $cur['tx_packets'] = (int)$m[1];
elseif (preg_match('/^tx failed:\s+(\d+)/', $t, $m)) $cur['tx_failed'] = (int)$m[1];
}
if ($cur !== null) {
$rows[] = $cur;
}
return $rows;
}
function wifi_time_ms(string $value): int
{
if (preg_match('/(\d+)/', $value, $m)) {
return (int)$m[1];
}
return PHP_INT_MAX;
}
function wifi_unknown(mixed $value): bool
{
$text = strtoupper(trim((string)$value));
return $text === '' || $text === 'N/A' || $text === '-';
}
function wifi_bitrate_mbps(mixed $value): ?float
{
if (preg_match('/([0-9]+(?:\.[0-9]+)?)/', (string)$value, $m)) {
return (float)$m[1];
}
return null;
}
function wifi_signal_dbm(string $value): int
{
if (preg_match('/-?\d+/', $value, $m)) {
return (int)$m[0];
}
return -999;
}
function estimate_wifi_rate_from_signal(string $band, int $dbm): float
{
if ($band === '5G') {
if ($dbm >= -50) return 1300.0;
if ($dbm >= -58) return 866.7;
if ($dbm >= -66) return 585.0;
if ($dbm >= -73) return 390.0;
if ($dbm >= -80) return 173.3;
return 54.0;
}
if ($dbm >= -50) return 300.0;
if ($dbm >= -58) return 144.4;
if ($dbm >= -66) return 72.2;
if ($dbm >= -73) return 39.0;
if ($dbm >= -80) return 19.5;
return 6.5;
}
function estimate_wifi_signal_from_rate(string $band, float $mbps): int
{
if ($band === '5G') {
if ($mbps >= 900) return -48;
if ($mbps >= 600) return -55;
if ($mbps >= 350) return -63;
if ($mbps >= 150) return -71;
if ($mbps >= 54) return -79;
return -84;
}
if ($mbps >= 250) return -48;
if ($mbps >= 120) return -56;
if ($mbps >= 65) return -64;
if ($mbps >= 30) return -72;
if ($mbps >= 10) return -80;
return -86;
}
function format_estimated_mbps(float $mbps): string
{
$value = abs($mbps - round($mbps)) < 0.05
? (string)(int)round($mbps)
: number_format($mbps, 1);
return $value . ' MBit/s';
}
function apply_wifi_estimates(array $clients): array
{
foreach ($clients as &$client) {
$band = (string)($client['band'] ?? '2.4G');
$signalDbm = wifi_signal_dbm((string)($client['signal'] ?? ''));
$txMbps = wifi_bitrate_mbps($client['tx_bitrate'] ?? null);
$rxMbps = wifi_bitrate_mbps($client['rx_bitrate'] ?? null);
if (wifi_unknown($client['signal'] ?? null)) {
$sourceRate = max($txMbps ?? 0.0, $rxMbps ?? 0.0);
$signalDbm = $sourceRate > 0
? estimate_wifi_signal_from_rate($band, $sourceRate)
: ($band === '5G' ? -67 : -70);
$client['signal'] = $signalDbm . ' dBm';
$client['signal_source'] = $sourceRate > 0 ? 'estimated_from_rate' : 'estimated_default';
}
if (wifi_unknown($client['tx_bitrate'] ?? null)) {
$base = $rxMbps ?? estimate_wifi_rate_from_signal($band, $signalDbm);
$client['tx_bitrate'] = format_estimated_mbps($base);
$client['tx_bitrate_source'] = $rxMbps === null ? 'estimated_from_signal' : 'estimated_from_rx';
}
if (wifi_unknown($client['rx_bitrate'] ?? null)) {
$base = $txMbps ?? estimate_wifi_rate_from_signal($band, $signalDbm);
$client['rx_bitrate'] = format_estimated_mbps($base);
$client['rx_bitrate_source'] = $txMbps === null ? 'estimated_from_signal' : 'estimated_from_tx';
}
}
unset($client);
return $clients;
}
function dedupe_wifi_clients(array $clients): array
{
$deduped = [];
foreach ($clients as $client) {
$mac = strtolower((string)($client['mac'] ?? ''));
$key = $mac !== '' && $mac !== 'n/a'
? 'mac:' . $mac
: 'row:' . ($client['band'] ?? '') . ':' . ($client['ip'] ?? '') . ':' . ($client['hostname'] ?? '');
if (!isset($deduped[$key])) {
$deduped[$key] = $client;
continue;
}
$currentInactive = wifi_time_ms((string)($deduped[$key]['inactive_time'] ?? ''));
$nextInactive = wifi_time_ms((string)($client['inactive_time'] ?? ''));
$currentSignal = wifi_signal_dbm((string)($deduped[$key]['signal'] ?? ''));
$nextSignal = wifi_signal_dbm((string)($client['signal'] ?? ''));
if ($nextInactive < $currentInactive || ($nextInactive === $currentInactive && $nextSignal > $currentSignal)) {
$deduped[$key] = $client;
}
}
return array_values($deduped);
}
function wifi_connected_time_missing(mixed $value): bool
{
$text = strtoupper(trim((string)$value));
return $text === '' || $text === 'N/A' || $text === '-';
}
function wifi_valid_mac(string $mac): bool
{
return preg_match('/^[0-9a-f]{2}(?::[0-9a-f]{2}){5}$/', strtolower($mac)) === 1;
}
function prune_observed_wifi_sessions(PDO $pdo, array $visibleMacs): void
{
$visibleMacs = array_values(array_unique(array_filter(
array_map(static fn($mac) => strtolower((string)$mac), $visibleMacs),
static fn($mac) => wifi_valid_mac($mac)
)));
if ($visibleMacs === []) {
$pdo->exec("DELETE FROM wifi_observed_sessions WHERE band = '5G'");
return;
}
$placeholders = implode(',', array_fill(0, count($visibleMacs), '?'));
$stmt = $pdo->prepare("
DELETE FROM wifi_observed_sessions
WHERE band = '5G'
AND mac NOT IN ($placeholders)
");
$stmt->execute($visibleMacs);
}
function observed_wifi_duration_seconds(array $clients): array
{
$targets = [];
$visible5gMacs = [];
foreach ($clients as $client) {
$band = (string)($client['band'] ?? '');
$mac = strtolower((string)($client['mac'] ?? ''));
if ($band !== '5G' || !wifi_valid_mac($mac)) {
continue;
}
$visible5gMacs[] = $mac;
if (!wifi_connected_time_missing($client['connected_time'] ?? null)) {
continue;
}
$targets[$mac] = [
'mac' => $mac,
'band' => $band,
'iface' => (string)($client['iface'] ?? ''),
'ip' => (string)($client['ip'] ?? ''),
'hostname' => (string)($client['hostname'] ?? ''),
];
}
try {
$pdo = db();
$pdo->beginTransaction();
prune_observed_wifi_sessions($pdo, $visible5gMacs);
if ($targets === []) {
$pdo->commit();
return [];
}
$upsert = $pdo->prepare("
INSERT INTO wifi_observed_sessions
(mac, band, iface, first_seen_at, last_seen_at, last_ip, hostname)
VALUES
(:mac, :band, :iface, NOW(3), NOW(3), :last_ip, :hostname)
ON DUPLICATE KEY UPDATE
band = VALUES(band),
iface = VALUES(iface),
last_seen_at = NOW(3),
last_ip = VALUES(last_ip),
hostname = VALUES(hostname)
");
foreach ($targets as $target) {
$upsert->execute([
':mac' => $target['mac'],
':band' => $target['band'],
':iface' => $target['iface'],
':last_ip' => $target['ip'] === 'N/A' ? null : $target['ip'],
':hostname' => $target['hostname'] === 'N/A' ? null : $target['hostname'],
]);
}
$placeholders = implode(',', array_fill(0, count($targets), '?'));
$stmt = $pdo->prepare("
SELECT
mac,
GREATEST(0, TIMESTAMPDIFF(SECOND, first_seen_at, NOW(3))) AS observed_seconds
FROM wifi_observed_sessions
WHERE mac IN ($placeholders)
");
$stmt->execute(array_keys($targets));
$rows = $stmt->fetchAll();
$pdo->commit();
$seconds = [];
foreach ($rows as $row) {
$seconds[strtolower((string)$row['mac'])] = (int)$row['observed_seconds'];
}
return $seconds;
} catch (Throwable) {
if (isset($pdo) && $pdo instanceof PDO && $pdo->inTransaction()) {
$pdo->rollBack();
}
return [];
}
}
function apply_observed_wifi_connected_time(array $clients): array
{
$observedSeconds = observed_wifi_duration_seconds($clients);
if ($observedSeconds === []) {
return $clients;
}
foreach ($clients as &$client) {
$mac = strtolower((string)($client['mac'] ?? ''));
if (($client['band'] ?? '') !== '5G'
|| !wifi_connected_time_missing($client['connected_time'] ?? null)
|| !array_key_exists($mac, $observedSeconds)
) {
continue;
}
$client['connected_time'] = (string)$observedSeconds[$mac];
$client['connected_time_source'] = 'observed';
}
unset($client);
return $clients;
}
function wifi_data(): array
{
$leases = dnsmasq_leases();
$aliases = wifi_client_aliases();
$clients = [];
foreach (['wlan0' => '2.4G', 'wlan1' => '5G'] as $iface => $band) {
$clients = array_merge(
$clients,
parse_live_wifi_rows($iface, $band, iw_station_dump($iface), $leases, $aliases)
);
}
$clients = dedupe_wifi_clients($clients);
$clients = apply_wifi_estimates($clients);
$clients = apply_observed_wifi_connected_time($clients);
return [
'clients' => $clients,
'count24' => count(array_filter($clients, fn($c) => $c['band'] === '2.4G')),
'count5' => count(array_filter($clients, fn($c) => $c['band'] === '5G')),
'leases' => array_values($leases),
];
}
function action_rows(int $limit = 80): array
{
$limit = max(1, min(300, $limit));
@@ -2870,6 +3339,7 @@ function collect_snapshot(bool $applyFan = true): array
'system' => $system,
'battery' => $battery,
'wifi' => wifi_data(),
'history' => $history,
'processes' => $processes,
'custom_services' => custom_systemd_services(),
@@ -3018,6 +3488,60 @@ function control_api_dispatch(): void
]);
}
if ($action === 'wifi') {
$verb = (string)($_POST['verb'] ?? '');
$unit = (string)($_POST['unit'] ?? '');
$allowedUnits = [
'hostapd-24g.service',
'hostapd-5g.service',
'dnsmasq.service',
];
if (
!in_array($unit, $allowedUnits, true)
|| !in_array($verb, ['restart', 'reload'], true)
|| ($verb === 'restart' && !(bool)setting_value('security.allow_wifi_restart'))
|| ($verb === 'reload' && !(bool)setting_value('security.allow_wifi_reload'))
) {
json_out([
'ok' => false,
'error' => 'bad_wifi_request',
], 400);
}
$result = sh(['/usr/bin/systemctl', $verb, $unit], true, (int)setting_value('security.wifi_command_timeout_seconds'));
$out = $result['out'];
add_fan_action(
'wifi_' . $verb,
null,
null,
$unit . "\n" . mb_substr($out, 0, 1000),
true
);
json_out([
'ok' => true,
'output' => $out,
'data' => collect_snapshot(false),
]);
}
if ($action === 'wifi_alias') {
$alias = save_wifi_client_alias(
(string)($_POST['mac'] ?? ''),
(string)($_POST['hostname'] ?? '')
);
json_out([
'ok' => true,
'data' => [
'alias' => $alias,
'status' => collect_snapshot(false),
],
]);
}
if ($action === 'collect') {
json_out([
'ok' => true,