172 lines
5.7 KiB
PHP
172 lines
5.7 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
declare(strict_types=1);
|
||
|
|
|
||
|
|
namespace Mod\Gateway\Controllers;
|
||
|
|
|
||
|
|
use Illuminate\Http\JsonResponse;
|
||
|
|
use Illuminate\Http\Request;
|
||
|
|
use Illuminate\Routing\Controller;
|
||
|
|
use Mod\Chain\Services\DaemonRpc;
|
||
|
|
use Mod\Gateway\Services\GatewayRegistry;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Gateway pairing API — LetheanGateway nodes connect here.
|
||
|
|
*
|
||
|
|
* POST /v1/gateway/pair — pair a gateway with lthn.io
|
||
|
|
* POST /v1/gateway/heartbeat — report alive + stats
|
||
|
|
* GET /v1/gateway/live — list live paired gateways
|
||
|
|
* POST /v1/gateway/dispatch — assign a customer connection to a gateway
|
||
|
|
*/
|
||
|
|
class GatewayController extends Controller
|
||
|
|
{
|
||
|
|
public function __construct(
|
||
|
|
private readonly GatewayRegistry $registry,
|
||
|
|
private readonly DaemonRpc $rpc,
|
||
|
|
) {}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* POST /v1/gateway/pair
|
||
|
|
* {
|
||
|
|
* "name": "charon",
|
||
|
|
* "signature": "...", // sign a challenge with alias wallet key
|
||
|
|
* "capabilities": ["vpn", "dns", "proxy"],
|
||
|
|
* "region": "eu-west",
|
||
|
|
* "bandwidth_mbps": 100,
|
||
|
|
* "max_connections": 50,
|
||
|
|
* "wireguard_endpoint": "10.69.69.165:51820",
|
||
|
|
* "proxy_endpoint": "10.69.69.165:3128"
|
||
|
|
* }
|
||
|
|
*/
|
||
|
|
public function pair(Request $request): JsonResponse
|
||
|
|
{
|
||
|
|
$name = strtolower(trim((string) $request->input('name')));
|
||
|
|
|
||
|
|
if (empty($name)) {
|
||
|
|
return response()->json(['error' => 'Name required'], 422);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Verify the name exists on chain
|
||
|
|
$alias = $this->rpc->getAliasByName($name);
|
||
|
|
if (! $alias) {
|
||
|
|
return response()->json([
|
||
|
|
'error' => 'Name not registered on chain',
|
||
|
|
'name' => $name,
|
||
|
|
], 404);
|
||
|
|
}
|
||
|
|
|
||
|
|
// TODO: verify signature proves ownership of the alias wallet
|
||
|
|
// For now, trust the name exists on chain as proof of identity
|
||
|
|
|
||
|
|
$this->registry->register($name, [
|
||
|
|
'capabilities' => $request->input('capabilities', []),
|
||
|
|
'region' => $request->input('region', 'unknown'),
|
||
|
|
'bandwidth_mbps' => (int) $request->input('bandwidth_mbps', 0),
|
||
|
|
'max_connections' => (int) $request->input('max_connections', 0),
|
||
|
|
'wireguard_endpoint' => $request->input('wireguard_endpoint', ''),
|
||
|
|
'proxy_endpoint' => $request->input('proxy_endpoint', ''),
|
||
|
|
]);
|
||
|
|
|
||
|
|
return response()->json([
|
||
|
|
'status' => 'paired',
|
||
|
|
'name' => $name . '.lthn',
|
||
|
|
'heartbeat_interval' => 60,
|
||
|
|
'ttl' => 300,
|
||
|
|
'message' => 'Gateway paired. Send heartbeat every 60s to stay live.',
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* POST /v1/gateway/heartbeat
|
||
|
|
* {"name": "charon", "connections": 12, "load": 45, "bytes_since_last": 104857600}
|
||
|
|
*/
|
||
|
|
public function heartbeat(Request $request): JsonResponse
|
||
|
|
{
|
||
|
|
$name = strtolower(trim((string) $request->input('name')));
|
||
|
|
|
||
|
|
$ok = $this->registry->heartbeat($name, [
|
||
|
|
'connections' => (int) $request->input('connections', 0),
|
||
|
|
'load' => (int) $request->input('load', 0),
|
||
|
|
'bytes_since_last' => (int) $request->input('bytes_since_last', 0),
|
||
|
|
]);
|
||
|
|
|
||
|
|
if (! $ok) {
|
||
|
|
return response()->json([
|
||
|
|
'error' => 'Gateway not paired. Call /pair first.',
|
||
|
|
'name' => $name,
|
||
|
|
], 404);
|
||
|
|
}
|
||
|
|
|
||
|
|
return response()->json([
|
||
|
|
'status' => 'ok',
|
||
|
|
'name' => $name . '.lthn',
|
||
|
|
'next_heartbeat' => 60,
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* GET /v1/gateway/live?cap=proxy
|
||
|
|
*
|
||
|
|
* List all live paired gateways with real-time stats.
|
||
|
|
*/
|
||
|
|
public function live(Request $request): JsonResponse
|
||
|
|
{
|
||
|
|
$capability = strtolower(trim((string) $request->get('cap', '')));
|
||
|
|
$gateways = $this->registry->liveGateways($capability);
|
||
|
|
|
||
|
|
return response()->json([
|
||
|
|
'gateways' => array_map(fn ($gw) => [
|
||
|
|
'name' => $gw['name'] . '.lthn',
|
||
|
|
'region' => $gw['region'],
|
||
|
|
'capabilities' => $gw['capabilities'],
|
||
|
|
'load' => $gw['current_load'] ?? 0,
|
||
|
|
'connections' => $gw['current_connections'] ?? 0,
|
||
|
|
'bandwidth_mbps' => $gw['bandwidth_mbps'] ?? 0,
|
||
|
|
'wireguard' => ! empty($gw['wireguard_endpoint']),
|
||
|
|
'proxy' => ! empty($gw['proxy_endpoint']),
|
||
|
|
'status' => $gw['status'],
|
||
|
|
'last_heartbeat' => $gw['last_heartbeat'],
|
||
|
|
], $gateways),
|
||
|
|
'count' => count($gateways),
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* POST /v1/gateway/dispatch
|
||
|
|
* {"type": "residential", "region": "eu"}
|
||
|
|
*
|
||
|
|
* Select the best live gateway for a customer connection.
|
||
|
|
*/
|
||
|
|
public function dispatch(Request $request): JsonResponse
|
||
|
|
{
|
||
|
|
$type = strtolower(trim((string) $request->input('type', 'proxy')));
|
||
|
|
|
||
|
|
$capMap = [
|
||
|
|
'mobile' => 'proxy',
|
||
|
|
'residential' => 'proxy',
|
||
|
|
'seo' => 'exit',
|
||
|
|
'vpn' => 'vpn',
|
||
|
|
];
|
||
|
|
|
||
|
|
$capability = $capMap[$type] ?? 'proxy';
|
||
|
|
$gateway = $this->registry->selectBest($capability);
|
||
|
|
|
||
|
|
if (! $gateway) {
|
||
|
|
// Fall back to chain-discovered nodes (unpaired but registered)
|
||
|
|
return response()->json([
|
||
|
|
'error' => 'No live paired gateways available. Try again later.',
|
||
|
|
'fallback' => 'chain',
|
||
|
|
], 503);
|
||
|
|
}
|
||
|
|
|
||
|
|
return response()->json([
|
||
|
|
'gateway' => $gateway['name'] . '.lthn',
|
||
|
|
'region' => $gateway['region'],
|
||
|
|
'wireguard_endpoint' => $gateway['wireguard_endpoint'] ?? '',
|
||
|
|
'proxy_endpoint' => $gateway['proxy_endpoint'] ?? '',
|
||
|
|
'load' => $gateway['current_load'],
|
||
|
|
'type' => $type,
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
}
|