WiFi 라우터 기능 복원
This commit is contained in:
+524
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user