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:
DevSoftTech Server 2026-08-04 03:45:51 +05:30
parent 1073ab8aca
commit f349b33001
2 changed files with 112 additions and 48 deletions

View File

@ -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

View File

@ -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>