diff --git a/lib/config/supabase_config.dart b/lib/config/supabase_config.dart new file mode 100644 index 0000000..b579666 --- /dev/null +++ b/lib/config/supabase_config.dart @@ -0,0 +1,19 @@ +import 'dart:io'; +import 'package:dotenv/dotenv.dart'; +import 'package:supabase/supabase.dart'; +import 'package:waylume_server/core/utils.dart'; + +var env = DotEnv()..load(); + +dynamic fromEnivronment(String key) { + if (kIsRunningInDocker) { + return Platform.environment[key]; + } else { + return env[key]; + } +} + +final SupabaseClient SUPABASE_CLIENT = SupabaseClient( + "https://lsdrctuvnwdrzrdyoqzu.supabase.co", + fromEnivronment('SUPABASE_KEY')! +); \ No newline at end of file diff --git a/lib/core/utils.dart b/lib/core/utils.dart new file mode 100644 index 0000000..bb764bc --- /dev/null +++ b/lib/core/utils.dart @@ -0,0 +1,29 @@ + +import 'dart:convert'; +import 'dart:io'; +import 'package:http/http.dart' as http; + +/// Get the current WAN IP address of the device. +Future getWanIp() async { + // Get the IP address of the server. https://api.ipify.org?format=json + var ipResponse = await http.get(Uri.parse('https://api.ipify.org?format=json')); + if (ipResponse.statusCode == 200) { + var ipData = jsonDecode(ipResponse.body); + return ipData['ip'] as String; + } else { + throw Exception('Failed to get server IP address'); + } +} + +/// Check if Wireguard is installed on the system. +Future isWireguardInstalled() async { + try { + var result = await Process.run('wg', ['--version']); + return result.exitCode == 0; + } catch (e) { + print('Error checking WireGuard installation: $e'); + return false; + } +} + +bool get kIsRunningInDocker => File('/.dockerenv').existsSync(); \ No newline at end of file diff --git a/lib/services/server_service.dart b/lib/services/server_service.dart new file mode 100644 index 0000000..0d09e32 --- /dev/null +++ b/lib/services/server_service.dart @@ -0,0 +1,30 @@ +import 'dart:io'; +import 'package:waylume_server/config/supabase_config.dart'; +import 'package:waylume_server/core/utils.dart'; + +class ServerService { + static Future registerServer() async { + String ip = await getWanIp(); + + var existsCheck = await SUPABASE_CLIENT + .from("waylume_servers") + .select() + .eq("id", fromEnivronment('SERVER_ID')) + .eq("host_ip", ip); + + if (existsCheck.isEmpty) { + await SUPABASE_CLIENT + .from("waylume_servers") + .insert({ + "id": fromEnivronment('SERVER_ID')!, + "last_heartbeat": DateTime.now().toUtc().toIso8601String(), + "host_ip": ip, + }); + } + } + + static Future isolatePeers() async { + await Process.run('iptables', ['-I', 'FORWARD', '-i', 'wg0', '-o', 'wg0', '-j', 'DROP']); + await Process.run('iptables', ['-I', 'FORWARD', '-i', 'wg0', '-o', 'wg0', '-m', 'state', '--state', 'ESTABLISHED,RELATED', '-j', 'ACCEPT']); + } +} \ No newline at end of file diff --git a/lib/services/supabase_heartbeat.dart b/lib/services/supabase_heartbeat.dart new file mode 100644 index 0000000..fb56744 --- /dev/null +++ b/lib/services/supabase_heartbeat.dart @@ -0,0 +1,35 @@ + +// Run a heartbeat to let supabase know that the client is still active. + +import 'dart:io'; +import 'dart:isolate'; +import 'dart:math'; + +import 'package:supabase/supabase.dart'; +import 'package:waylume_server/config/supabase_config.dart'; + +void initHeartbeat() { + + // Run this on a separate thread. + Isolate.spawn((_) async { + + // To avoid server deadlock + await Future.delayed(Duration(seconds: Random().nextInt(5))); + + while (true) { + DateTime now = DateTime.now().toUtc(); + + await SUPABASE_CLIENT + .from("waylume_servers") + .update({ "last_heartbeat": now.toIso8601String() }) + .eq("id", fromEnivronment("SERVER_ID")!); + + print("Heartbeat sent to Supabase at ${now.toIso8601String()}"); + + // Wait 30 seconds before sending the next heartbeat + await Future.delayed(Duration(seconds: 30)); + } + + }, null); + +} \ No newline at end of file diff --git a/lib/services/wireguard_service.dart b/lib/services/wireguard_service.dart new file mode 100644 index 0000000..da8b596 --- /dev/null +++ b/lib/services/wireguard_service.dart @@ -0,0 +1,119 @@ +import 'dart:convert'; +import 'dart:io'; + +class WireGuardService { + static const String interfaceName = 'wg0'; + static const String configPath = '/etc/wireguard/wg0.conf'; + static const String serverIP = '10.0.0.1/24'; + static const int serverPort = 51820; + + static Future initializeServer() async { + try { + // Check if wg0 interface already exists + if (await _interfaceExists()) { + print('WireGuard interface $interfaceName already exists.'); + return; + } + + print('Initializing WireGuard server interface...'); + + // Generate server keys if they don't exist + final serverKeys = await _getOrCreateServerKeys(); + + // Create server config + await _createServerConfig(serverKeys['privateKey']!); + + // Bring up the interface + await _bringUpInterface(); + + print('WireGuard server interface $interfaceName initialized successfully.'); + } catch (e) { + throw Exception('Failed to initialize WireGuard server: $e'); + } + } + + static Future _interfaceExists() async { + try { + final result = await Process.run('wg', ['show', interfaceName]); + return result.exitCode == 0; + } catch (e) { + return false; + } + } + + static Future> _getOrCreateServerKeys() async { + try { + // Try to get existing keys from running interface + final pubResult = await Process.run('wg', ['show', interfaceName, 'public-key']); + final privResult = await Process.run('wg', ['show', interfaceName, 'private-key']); + + if (pubResult.exitCode == 0 && privResult.exitCode == 0) { + return { + 'publicKey': pubResult.stdout.toString().trim(), + 'privateKey': privResult.stdout.toString().trim(), + }; + } + } catch (e) { + // Keys don't exist, generate new ones + } + + // Generate new keys + final privateKeyResult = await Process.run('wg', ['genkey']); + if (privateKeyResult.exitCode != 0) { + throw Exception('Failed to generate private key'); + } + + final privateKey = privateKeyResult.stdout.toString().trim(); + + // Generate public key from private key + final pubProcess = await Process.start('wg', ['pubkey']); + pubProcess.stdin.writeln(privateKey); + await pubProcess.stdin.close(); + final publicKey = await pubProcess.stdout.transform(utf8.decoder).join(); + + return { + 'privateKey': privateKey, + 'publicKey': publicKey.trim(), + }; + } + + static Future _createServerConfig(String privateKey) async { + final config = ''' +[Interface] +PrivateKey = $privateKey +Address = $serverIP +ListenPort = $serverPort +PostUp = iptables -A FORWARD -i $interfaceName -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE +PostDown = iptables -D FORWARD -i $interfaceName -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE +'''; + + // Ensure config directory exists + final configDir = Directory('/etc/wireguard'); + if (!await configDir.exists()) { + await configDir.create(recursive: true); + } + + // Write config file + final configFile = File(configPath); + await configFile.writeAsString(config); + + // Set proper permissions (600) + await Process.run('chmod', ['600', configPath]); + } + + static Future _bringUpInterface() async { + // Start WireGuard interface + final result = await Process.run('wg-quick', ['up', interfaceName]); + if (result.exitCode != 0) { + throw Exception('Failed to bring up WireGuard interface: ${result.stderr}'); + } + } + + static Future stopInterface() async { + try { + await Process.run('wg-quick', ['down', interfaceName]); + } catch (e) { + // Interface might not be running, ignore + } + } +} \ No newline at end of file diff --git a/lib/web/peer_routes.dart b/lib/web/peer_routes.dart new file mode 100644 index 0000000..83d6704 --- /dev/null +++ b/lib/web/peer_routes.dart @@ -0,0 +1,165 @@ +import 'dart:convert'; + +import 'package:shelf/shelf.dart'; +import 'package:shelf_router/shelf_router.dart'; +import 'package:waylume_server/wireguard/peers.dart'; + +class PeerRoutes { + Router get router { + final router = Router(); + + router.post('/peers', _createPeer); + router.delete('/peers/', _deletePeer); + router.get('/peers//config', _getPeerConfig); + router.put('/peers//speed-limit', _setSpeedLimit); + router.put('/peers//data-cap', _setDataCap); + + return router; + } + + Future _createPeer(Request request) async { + try { + final peer = await createPeer(); + final config = await generateClientConfigForPeer(peer); + + return Response.ok( + jsonEncode({ + 'success': true, + 'peer': peer.toJson(), + 'config': config, + }), + headers: {'Content-Type': 'application/json'}, + ); + } catch (e) { + return Response.internalServerError( + body: jsonEncode({ + 'success': false, + 'error': e.toString(), + }), + headers: {'Content-Type': 'application/json'}, + ); + } + } + + Future _deletePeer(Request request) async { + final publicKey = request.params['publicKey']!; + + try { + final success = await deletePeer(publicKey); + + return Response.ok( + jsonEncode({ + 'success': success, + 'message': success ? 'Peer deleted successfully' : 'Failed to delete peer', + }), + headers: {'Content-Type': 'application/json'}, + ); + } catch (e) { + return Response.internalServerError( + body: jsonEncode({ + 'success': false, + 'error': e.toString(), + }), + headers: {'Content-Type': 'application/json'}, + ); + } + } + + Future _getPeerConfig(Request request) async { + final publicKey = request.params['publicKey']!; + + try { + return Response.notFound( + jsonEncode({ + 'success': false, + 'error': 'Config retrieval not implemented - peer info not stored', + }), + headers: {'Content-Type': 'application/json'}, + ); + } catch (e) { + return Response.internalServerError( + body: jsonEncode({ + 'success': false, + 'error': e.toString(), + }), + headers: {'Content-Type': 'application/json'}, + ); + } + } + + Future _setSpeedLimit(Request request) async { + final publicKey = request.params['publicKey']!; + + try { + final body = await request.readAsString(); + final data = jsonDecode(body) as Map; + final speedKbps = data['speedKbps'] as int?; + + if (speedKbps == null) { + return Response.badRequest( + body: jsonEncode({ + 'success': false, + 'error': 'speedKbps parameter is required', + }), + headers: {'Content-Type': 'application/json'}, + ); + } + + await setSpeedLimit(publicKey, speedKbps); + + return Response.ok( + jsonEncode({ + 'success': true, + 'message': 'Speed limit set successfully', + }), + headers: {'Content-Type': 'application/json'}, + ); + } catch (e) { + return Response.internalServerError( + body: jsonEncode({ + 'success': false, + 'error': e.toString(), + }), + headers: {'Content-Type': 'application/json'}, + ); + } + } + + Future _setDataCap(Request request) async { + final publicKey = request.params['publicKey']!; + + try { + final body = await request.readAsString(); + final data = jsonDecode(body) as Map; + final dataCapMB = data['dataCapMB'] as int?; + + if (dataCapMB == null) { + return Response.badRequest( + body: jsonEncode({ + 'success': false, + 'error': 'dataCapMB parameter is required', + }), + headers: {'Content-Type': 'application/json'}, + ); + } + + await setDataCap(publicKey, dataCapMB); + + return Response.ok( + jsonEncode({ + 'success': true, + 'message': 'Data cap set successfully', + }), + headers: {'Content-Type': 'application/json'}, + ); + } catch (e) { + return Response.internalServerError( + body: jsonEncode({ + 'success': false, + 'error': e.toString(), + }), + headers: {'Content-Type': 'application/json'}, + ); + } + } +} \ No newline at end of file diff --git a/lib/wireguard/traffic_control.dart b/lib/wireguard/traffic_control.dart new file mode 100644 index 0000000..a0c5d9d --- /dev/null +++ b/lib/wireguard/traffic_control.dart @@ -0,0 +1,40 @@ +import 'dart:io'; + +class TrafficControlService { + static Future setSpeedLimit(String peerIP, int speedKbps) async { + final mark = _getMarkForIP(peerIP); + + await _runIptablesCommand(['-I', 'FORWARD', '-s', peerIP, '-j', 'MARK', '--set-mark', mark.toString()]); + await _runIptablesCommand(['-I', 'FORWARD', '-d', peerIP, '-j', 'MARK', '--set-mark', mark.toString()]); + + await _runTcCommand(['class', 'add', 'dev', 'wg0', 'parent', '1:1', 'classid', '1:$mark', 'htb', 'rate', '${speedKbps}kbit', 'ceil', '${speedKbps}kbit']); + await _runTcCommand(['filter', 'add', 'dev', 'wg0', 'protocol', 'ip', 'parent', '1:', 'prio', '1', 'handle', mark.toString(), 'fw', 'flowid', '1:$mark']); + } + + static Future setDataCap(String peerIP, int dataCapMB) async { + final quotaBytes = dataCapMB * 1024 * 1024; + + await _runIptablesCommand(['-I', 'FORWARD', '-s', peerIP, '-m', 'quota', '--quota', quotaBytes.toString(), '-j', 'ACCEPT']); + await _runIptablesCommand(['-I', 'FORWARD', '-d', peerIP, '-m', 'quota', '--quota', quotaBytes.toString(), '-j', 'ACCEPT']); + await _runIptablesCommand(['-A', 'FORWARD', '-s', peerIP, '-j', 'DROP']); + await _runIptablesCommand(['-A', 'FORWARD', '-d', peerIP, '-j', 'DROP']); + } + + static int _getMarkForIP(String ip) { + return ip.split('.').last.hashCode % 65535 + 1; + } + + static Future _runIptablesCommand(List args) async { + final result = await Process.run('iptables', args); + if (result.exitCode != 0) { + throw Exception('iptables command failed: ${result.stderr}'); + } + } + + static Future _runTcCommand(List args) async { + final result = await Process.run('tc', args); + if (result.exitCode != 0) { + throw Exception('tc command failed: ${result.stderr}'); + } + } +} \ No newline at end of file