The portal derives the device MAC instead of trusting the caller
This commit is contained in:
104
src/Services/DeviceResolver.php
Normal file
104
src/Services/DeviceResolver.php
Normal file
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace Dashboard\Unifi\Services;
|
||||
|
||||
use Dashboard\Unifi\Support\MacAddress;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/** The outcome of trying to identify the device making a portal request. */
|
||||
class ResolvedDevice
|
||||
{
|
||||
public function __construct(
|
||||
public bool $ok,
|
||||
public ?string $mac = null,
|
||||
public ?string $reason = null,
|
||||
public bool $isRandomized = false,
|
||||
public bool $macParameterDisagreed = false,
|
||||
public array $client = [],
|
||||
) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identify the device that made a captive-portal request, or refuse.
|
||||
*
|
||||
* The `?mac=` parameter is NOT trusted. It is read only so a disagreement with
|
||||
* the derived value can be logged — that disagreement is somebody trying to
|
||||
* authorize a device they do not own, and before this nothing anywhere noticed.
|
||||
*
|
||||
* Every path refuses rather than guessing. The cost of that is real: if
|
||||
* TrustProxies is misconfigured, `proxy_untrusted` is returned for everyone and
|
||||
* nobody gets online, which is why it is a distinct reason the portal page can
|
||||
* report instead of a generic miss.
|
||||
*/
|
||||
class DeviceResolver
|
||||
{
|
||||
public function __construct(private UnifiApiClient $unifi) {}
|
||||
|
||||
/** Attempts, and the pause between them, for a device that has just associated. */
|
||||
private const ATTEMPTS = 3;
|
||||
private const PAUSE_US = 750_000;
|
||||
|
||||
public function resolve(Request $request, string $apMac): ResolvedDevice
|
||||
{
|
||||
$ip = $request->ip();
|
||||
|
||||
/*
|
||||
* A device that has only just associated may have no IP yet, so UniFi's
|
||||
* client table does not know it. Retry briefly rather than telling a
|
||||
* pupil their device is not connected when it is about to be.
|
||||
*
|
||||
* Bounded on purpose: this runs inside a web request, and a longer wait
|
||||
* would hold a worker per connecting device at exactly the moment a
|
||||
* class of them arrives at once.
|
||||
*/
|
||||
$client = null;
|
||||
for ($attempt = 1; $attempt <= self::ATTEMPTS; $attempt++) {
|
||||
$client = $this->unifi->findClientByIp($ip);
|
||||
if ($client) {
|
||||
break;
|
||||
}
|
||||
if ($attempt < self::ATTEMPTS) {
|
||||
usleep(self::PAUSE_US);
|
||||
}
|
||||
}
|
||||
|
||||
if (! $client) {
|
||||
// A forwarded header present while the request IP is still the raw
|
||||
// socket address means TrustProxies is not applying, so every lookup
|
||||
// here is doomed for the same reason. Say which failure this is.
|
||||
$forwarded = $request->headers->get('X-Forwarded-For');
|
||||
$raw = $request->server('REMOTE_ADDR');
|
||||
$reason = ($forwarded && $ip === $raw) ? 'proxy_untrusted' : 'no_client';
|
||||
|
||||
return new ResolvedDevice(ok: false, reason: $reason);
|
||||
}
|
||||
|
||||
$mac = MacAddress::normalise($client['mac'] ?? null);
|
||||
if (! $mac) {
|
||||
return new ResolvedDevice(ok: false, reason: 'no_client');
|
||||
}
|
||||
|
||||
$clientAp = MacAddress::normalise($client['ap_mac'] ?? null);
|
||||
if (! $clientAp || $clientAp !== MacAddress::normalise($apMac)) {
|
||||
return new ResolvedDevice(ok: false, reason: 'ap_mismatch');
|
||||
}
|
||||
|
||||
$supplied = MacAddress::normalise($request->input('mac'));
|
||||
$disagreed = $supplied !== null && $supplied !== $mac;
|
||||
|
||||
if ($disagreed) {
|
||||
Log::warning('unifi.portal: supplied MAC disagreed with the derived one', [
|
||||
'derived' => $mac, 'supplied' => $supplied, 'ip' => $ip, 'ap' => $apMac,
|
||||
]);
|
||||
}
|
||||
|
||||
return new ResolvedDevice(
|
||||
ok: true,
|
||||
mac: $mac,
|
||||
isRandomized: MacAddress::isRandomized($mac),
|
||||
macParameterDisagreed: $disagreed,
|
||||
client: $client,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user