feat: show all network interfaces with type badges (LAN/VPN/Public)
- readNetworkStatus() now returns array of all interfaces instead of single string - Parses 'ip addr' output (with ifconfig fallback) for all IPv4 interfaces - Each interface classified as LAN / VPN / Public by name and IP range - VPN detected by interface prefix: wg*, tun*, tap*, vpn* - RFC1918 ranges (192.168.x, 10.x, 172.16-31.x) shown as LAN - Display: icon + IP + interface name badge + type label per row - Handles NAT servers showing both local LAN and VPN IPs simultaneously
This commit is contained in:
parent
1073ab8aca
commit
f349b33001
|
|
@ -689,67 +689,101 @@ class SystemOperations
|
|||
};
|
||||
}
|
||||
|
||||
private function readNetworkStatus(): string
|
||||
private function readNetworkStatus(): array
|
||||
{
|
||||
// Method 1: $_SERVER['SERVER_ADDR'] — set by Apache/Nginx when available
|
||||
if (!empty($_SERVER['SERVER_ADDR'])) {
|
||||
$interfaces = [];
|
||||
|
||||
// Parse /proc/net/fib_trie (most reliable on Linux — no shell needed)
|
||||
if (file_exists('/proc/net/fib_trie')) {
|
||||
$this->parseInterfacesFromIp($interfaces);
|
||||
}
|
||||
|
||||
// Fallback: ifconfig
|
||||
if (empty($interfaces)) {
|
||||
$this->parseInterfacesFromIfconfig($interfaces);
|
||||
}
|
||||
|
||||
// Fallback: hostname -I (no interface names available)
|
||||
if (empty($interfaces)) {
|
||||
$result = $this->safeCommand('hostname -I');
|
||||
if ($result !== 'Unavailable') {
|
||||
foreach (preg_split('/\s+/', trim($result)) as $ip) {
|
||||
$ip = trim($ip);
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP) && !$this->isLoopbackIp($ip)) {
|
||||
$interfaces[] = ['name' => '?', 'ip' => $ip, 'type' => $this->guessIfaceType('?', $ip)];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ultimate fallback: SERVER_ADDR
|
||||
if (empty($interfaces) && !empty($_SERVER['SERVER_ADDR'])) {
|
||||
$ip = $_SERVER['SERVER_ADDR'];
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP) && !$this->isLoopbackIp($ip)) {
|
||||
return $ip;
|
||||
$interfaces[] = ['name' => 'server', 'ip' => $ip, 'type' => 'lan'];
|
||||
}
|
||||
}
|
||||
|
||||
// Method 2: Try /sbin/ip with absolute path (may not be in PATH)
|
||||
$result = $this->safeCommand('/sbin/ip -4 addr show scope global 2>/dev/null | grep -oP "inet \K[0-9.]+"');
|
||||
if ($result !== 'Unavailable' && trim($result) !== '') {
|
||||
$ips = preg_split('/\s+/', trim($result));
|
||||
foreach ($ips as $ip) {
|
||||
$ip = trim($ip);
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP) && !$this->isLoopbackIp($ip)) {
|
||||
return $ip;
|
||||
}
|
||||
return $interfaces;
|
||||
}
|
||||
|
||||
private function parseInterfacesFromIp(array &$interfaces): void
|
||||
{
|
||||
$result = $this->safeCommand('/sbin/ip -4 addr show 2>/dev/null');
|
||||
if ($result === 'Unavailable' || trim($result) === '') {
|
||||
$result = $this->safeCommand('ip -4 addr show 2>/dev/null');
|
||||
}
|
||||
if ($result === 'Unavailable') return;
|
||||
|
||||
$currentIface = null;
|
||||
foreach (explode("\n", $result) as $line) {
|
||||
if (preg_match('/^\d+:\s+(\S+):/', $line, $m)) {
|
||||
$currentIface = rtrim($m[1], ':');
|
||||
} elseif ($currentIface && preg_match('/inet\s+(\d+\.\d+\.\d+\.\d+)/', $line, $m)) {
|
||||
$ip = $m[1];
|
||||
if (!filter_var($ip, FILTER_VALIDATE_IP) || $this->isLoopbackIp($ip)) continue;
|
||||
$interfaces[] = [
|
||||
'name' => $currentIface,
|
||||
'ip' => $ip,
|
||||
'type' => $this->guessIfaceType($currentIface, $ip),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Method 3: Parse /proc/net/route to get gateway interface and its IP
|
||||
if (file_exists('/proc/net/route')) {
|
||||
$defaultIp = $this->getIpFromProcRoute();
|
||||
if ($defaultIp && filter_var($defaultIp, FILTER_VALIDATE_IP) && !$this->isLoopbackIp($defaultIp)) {
|
||||
return $defaultIp;
|
||||
private function parseInterfacesFromIfconfig(array &$interfaces): void
|
||||
{
|
||||
$result = $this->safeCommand('ifconfig 2>/dev/null');
|
||||
if ($result === 'Unavailable') return;
|
||||
|
||||
$currentIface = null;
|
||||
foreach (explode("\n", $result) as $line) {
|
||||
if (preg_match('/^(\S+):?\s/', $line, $m)) {
|
||||
$currentIface = rtrim($m[1], ':');
|
||||
} elseif ($currentIface && preg_match('/inet\s+(?:addr:)?(\d+\.\d+\.\d+\.\d+)/', $line, $m)) {
|
||||
$ip = $m[1];
|
||||
if (!filter_var($ip, FILTER_VALIDATE_IP) || $this->isLoopbackIp($ip)) continue;
|
||||
$interfaces[] = [
|
||||
'name' => $currentIface,
|
||||
'ip' => $ip,
|
||||
'type' => $this->guessIfaceType($currentIface, $ip),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Method 4: hostname -I (simple space-separated list)
|
||||
$result = $this->safeCommand('hostname -I');
|
||||
if ($result !== 'Unavailable' && trim($result) !== '') {
|
||||
$ips = preg_split('/\s+/', trim($result));
|
||||
foreach ($ips as $ip) {
|
||||
$ip = trim($ip);
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP) && !$this->isLoopbackIp($ip)) {
|
||||
return $ip;
|
||||
}
|
||||
}
|
||||
// Classify interface as lan/vpn/public based on name and IP range
|
||||
private function guessIfaceType(string $name, string $ip): string
|
||||
{
|
||||
$name = strtolower($name);
|
||||
if (str_starts_with($name, 'wg') || str_starts_with($name, 'tun') || str_starts_with($name, 'tap') || str_starts_with($name, 'vpn')) {
|
||||
return 'vpn';
|
||||
}
|
||||
|
||||
// Method 5: Try ifconfig output (older systems)
|
||||
$result = $this->safeCommand('ifconfig 2>/dev/null | grep -oP "inet \K[0-9.]+" | grep -v "^127\."');
|
||||
if ($result !== 'Unavailable' && trim($result) !== '') {
|
||||
$ips = preg_split('/\s+/', trim($result));
|
||||
foreach ($ips as $ip) {
|
||||
$ip = trim($ip);
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP)) {
|
||||
return $ip;
|
||||
}
|
||||
}
|
||||
// RFC1918 private ranges
|
||||
if (str_starts_with($ip, '192.168.') || str_starts_with($ip, '10.') || preg_match('/^172\.(1[6-9]|2\d|3[01])\./', $ip)) {
|
||||
return 'lan';
|
||||
}
|
||||
|
||||
// Method 6: Socket-based approach — connect to external host to get local IP
|
||||
$localIp = $this->getLocalIpViaSocket();
|
||||
if ($localIp && filter_var($localIp, FILTER_VALIDATE_IP) && !$this->isLoopbackIp($localIp)) {
|
||||
return $localIp;
|
||||
}
|
||||
|
||||
return 'Unavailable';
|
||||
return 'public';
|
||||
}
|
||||
|
||||
private function getIpFromProcRoute(): ?string
|
||||
|
|
|
|||
|
|
@ -31,7 +31,37 @@
|
|||
<div class="col-md-6">
|
||||
<div class="border rounded p-3 h-100">
|
||||
<div class="text-muted small">Network</div>
|
||||
<div class="fs-5 fw-semibold"><?= esc($status['network'] ?? 'Unavailable') ?></div>
|
||||
<?php
|
||||
$ifaces = $status['network'] ?? [];
|
||||
if (empty($ifaces)): ?>
|
||||
<div class="fs-5 fw-semibold text-muted">Unavailable</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($ifaces as $iface): ?>
|
||||
<?php
|
||||
$typeColor = match($iface['type']) {
|
||||
'vpn' => 'text-info',
|
||||
'public' => 'text-success',
|
||||
default => 'text-dark',
|
||||
};
|
||||
$typeIcon = match($iface['type']) {
|
||||
'vpn' => 'bi-shield-lock',
|
||||
'public' => 'bi-globe',
|
||||
default => 'bi-hdd-network',
|
||||
};
|
||||
$typeBadge = match($iface['type']) {
|
||||
'vpn' => 'bg-info',
|
||||
'public' => 'bg-success',
|
||||
default => 'bg-secondary',
|
||||
};
|
||||
?>
|
||||
<div class="d-flex align-items-center gap-2 mb-1">
|
||||
<i class="bi <?= $typeIcon ?> <?= $typeColor ?>"></i>
|
||||
<span class="fw-semibold"><?= esc($iface['ip']) ?></span>
|
||||
<span class="badge <?= $typeBadge ?> ms-1" style="font-size:10px;"><?= esc($iface['name']) ?></span>
|
||||
<span class="text-muted" style="font-size:10px;"><?= esc(strtoupper($iface['type'])) ?></span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user