Rename speedKbps and dataCapMB parameters to bytesPerSecond and quotaBytes, respectively, in API and traffic control logic
This commit is contained in:
73
README.md
73
README.md
@@ -31,11 +31,70 @@ Client applications communicate through Supabase to discover available Waylume s
|
||||
- **Health Monitoring**: Continuous heartbeat system for server availability
|
||||
|
||||
### API Endpoints
|
||||
- `POST /api/peers` - Create new VPN peer with generated keys and configuration
|
||||
- `POST /api/peers/delete` - Remove VPN peer (publicKey in request body)
|
||||
- `POST /api/peers/speed-limit` - Set bandwidth limits for peer (publicKey + speedKbps in request body)
|
||||
- `POST /api/peers/data-cap` - Set data usage limits for peer (publicKey + dataCapMB in request body)
|
||||
- `POST /api/peers/config` - Retrieve peer configuration (publicKey in request body, not implemented)
|
||||
|
||||
#### `POST /api/peers`
|
||||
Create a new VPN peer with automatically generated WireGuard keys and IP assignment.
|
||||
|
||||
**Request:** No body required
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"peer": {
|
||||
"privateKey": "base64_private_key",
|
||||
"publicKey": "base64_public_key",
|
||||
"ip": "10.0.0.x"
|
||||
},
|
||||
"config": "complete_wireguard_client_config"
|
||||
}
|
||||
```
|
||||
|
||||
#### `POST /api/peers/delete`
|
||||
Remove a VPN peer and clean up associated WireGuard configuration.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"publicKey": "peer_public_key_here"
|
||||
}
|
||||
```
|
||||
|
||||
#### `POST /api/peers/speed-limit`
|
||||
Set bidirectional bandwidth limits for a peer (controls both upload and download speeds).
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"publicKey": "peer_public_key_here",
|
||||
"bytesPerSecond": 125000
|
||||
}
|
||||
```
|
||||
*Note: 125000 bytes/s = 1000 kbps = 1 Mbps*
|
||||
|
||||
**Common Speed Conversions:**
|
||||
- 125 KB/s = 1 Mbps
|
||||
- 1,250 KB/s = 10 Mbps
|
||||
- 12,500 KB/s = 100 Mbps
|
||||
|
||||
#### `POST /api/peers/data-cap`
|
||||
Set total data usage limits for a peer using iptables quota rules.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"publicKey": "peer_public_key_here",
|
||||
"quotaBytes": 1073741824
|
||||
}
|
||||
```
|
||||
*Note: 1073741824 bytes = 1 GB*
|
||||
|
||||
**Common Data Conversions:**
|
||||
- 1 GB = 1,073,741,824 bytes
|
||||
- 10 GB = 10,737,418,240 bytes
|
||||
- 100 GB = 107,374,182,400 bytes
|
||||
|
||||
#### `POST /api/peers/config`
|
||||
Retrieve peer configuration (currently not implemented - returns 404).
|
||||
|
||||
### Security Features
|
||||
- Peer isolation using iptables rules (prevents peer-to-peer communication)
|
||||
@@ -130,14 +189,14 @@ curl -X POST http://localhost:3000/api/peers/delete \
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/peers/speed-limit \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"publicKey": "PEER_PUBLIC_KEY_HERE", "speedKbps": 1000}'
|
||||
-d '{"publicKey": "PEER_PUBLIC_KEY_HERE", "bytesPerSecond": 125000}' # 125000 bytes/s = 1000 kbps
|
||||
```
|
||||
|
||||
### Set data cap for peer
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/peers/data-cap \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"publicKey": "PEER_PUBLIC_KEY_HERE", "dataCapMB": 1024}'
|
||||
-d '{"publicKey": "PEER_PUBLIC_KEY_HERE", "quotaBytes": 1073741824}' # 1073741824 bytes = 1024 MB
|
||||
```
|
||||
|
||||
### Get peer config
|
||||
|
||||
@@ -79,10 +79,10 @@ class ApiTester {
|
||||
print(' IP: $peerIP');
|
||||
|
||||
// Test 2: Set speed limit
|
||||
print('\n📋 TEST 2: Set speed limit (500 kbps)');
|
||||
e print('\n📋 TEST 2: Set speed limit (62500 bytes/s = 500 kbps)');
|
||||
final speedResponse = await makeRequest('POST', '/api/peers/speed-limit', {
|
||||
'publicKey': publicKey,
|
||||
'speedKbps': 500,
|
||||
'bytesPerSecond': 62500, // 500 kbps = 500000 bits/s = 62500 bytes/s
|
||||
});
|
||||
|
||||
if (speedResponse['statusCode'] == 200 && speedResponse['body']['success'] == true) {
|
||||
@@ -92,10 +92,10 @@ class ApiTester {
|
||||
}
|
||||
|
||||
// Test 3: Set data cap
|
||||
print('\n📋 TEST 3: Set data cap (1024 MB)');
|
||||
print('\n📋 TEST 3: Set data cap (1073741824 bytes = 1024 MB)');
|
||||
final dataCapResponse = await makeRequest('POST', '/api/peers/data-cap', {
|
||||
'publicKey': publicKey,
|
||||
'dataCapMB': 1024,
|
||||
'quotaBytes': 1073741824, // 1024 * 1024 * 1024 bytes
|
||||
});
|
||||
|
||||
if (dataCapResponse['statusCode'] == 200 && dataCapResponse['body']['success'] == true) {
|
||||
|
||||
@@ -116,7 +116,7 @@ class PeerRoutes {
|
||||
final body = await request.readAsString();
|
||||
final data = jsonDecode(body) as Map<String, dynamic>;
|
||||
final publicKey = data['publicKey'] as String?;
|
||||
final speedKbps = data['speedKbps'] as int?;
|
||||
final bytesPerSecond = data['bytesPerSecond'] as int?;
|
||||
|
||||
if (publicKey == null) {
|
||||
return Response.badRequest(
|
||||
@@ -128,17 +128,17 @@ class PeerRoutes {
|
||||
);
|
||||
}
|
||||
|
||||
if (speedKbps == null) {
|
||||
if (bytesPerSecond == null) {
|
||||
return Response.badRequest(
|
||||
body: jsonEncode({
|
||||
'success': false,
|
||||
'error': 'speedKbps parameter is required',
|
||||
'error': 'bytesPerSecond parameter is required',
|
||||
}),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
);
|
||||
}
|
||||
|
||||
await setSpeedLimit(publicKey, speedKbps);
|
||||
await setSpeedLimit(publicKey, bytesPerSecond);
|
||||
|
||||
return Response.ok(
|
||||
jsonEncode({
|
||||
@@ -163,7 +163,7 @@ class PeerRoutes {
|
||||
final body = await request.readAsString();
|
||||
final data = jsonDecode(body) as Map<String, dynamic>;
|
||||
final publicKey = data['publicKey'] as String?;
|
||||
final dataCapMB = data['dataCapMB'] as int?;
|
||||
final quotaBytes = data['quotaBytes'] as int?;
|
||||
|
||||
if (publicKey == null) {
|
||||
return Response.badRequest(
|
||||
@@ -175,17 +175,17 @@ class PeerRoutes {
|
||||
);
|
||||
}
|
||||
|
||||
if (dataCapMB == null) {
|
||||
if (quotaBytes == null) {
|
||||
return Response.badRequest(
|
||||
body: jsonEncode({
|
||||
'success': false,
|
||||
'error': 'dataCapMB parameter is required',
|
||||
'error': 'quotaBytes parameter is required',
|
||||
}),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
);
|
||||
}
|
||||
|
||||
await setDataCap(publicKey, dataCapMB);
|
||||
await setDataCap(publicKey, quotaBytes);
|
||||
|
||||
return Response.ok(
|
||||
jsonEncode({
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import 'dart:io';
|
||||
|
||||
class TrafficControlService {
|
||||
static Future<void> setSpeedLimit(String peerIP, int speedKbps) async {
|
||||
static Future<void> setSpeedLimit(String peerIP, int bytesPerSecond) async {
|
||||
final mark = _getMarkForIP(peerIP);
|
||||
print('Setting speed limit for peer $peerIP to ${speedKbps}kbps (mark: $mark)');
|
||||
final kbps = (bytesPerSecond * 8 / 1000).round(); // Convert bytes/s to kbps
|
||||
print('Setting speed limit for peer $peerIP to ${bytesPerSecond} bytes/s (${kbps}kbps, mark: $mark)');
|
||||
|
||||
try {
|
||||
// Ensure HTB qdisc exists on wg0
|
||||
@@ -29,23 +30,55 @@ class TrafficControlService {
|
||||
}
|
||||
|
||||
print('Running iptables MARK commands for $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()]);
|
||||
// Mark upload traffic (FROM peer) with uploadMark
|
||||
await _runIptablesCommand(['-I', 'FORWARD', '-s', peerIP, '-j', 'MARK', '--set-mark', uploadMark.toString()]);
|
||||
// Mark download traffic (TO peer) with downloadMark
|
||||
await _runIptablesCommand(['-I', 'FORWARD', '-d', peerIP, '-j', 'MARK', '--set-mark', downloadMark.toString()]);
|
||||
|
||||
print('Running tc class add/change command...');
|
||||
// Create separate classes for upload and download
|
||||
final uploadMark = mark;
|
||||
final downloadMark = mark + 1000; // Offset to avoid conflicts
|
||||
|
||||
print('Running tc class add/change commands for upload and download...');
|
||||
|
||||
// Upload class (traffic FROM peer)
|
||||
try {
|
||||
await _runTcCommand(['class', 'add', 'dev', 'wg0', 'parent', '1:1', 'classid', '1:$mark', 'htb', 'rate', '${speedKbps}kbit', 'ceil', '${speedKbps}kbit']);
|
||||
await _runTcCommand(['class', 'add', 'dev', 'wg0', 'parent', '1:1', 'classid', '1:$uploadMark', 'htb', 'rate', '${kbps}kbit', 'ceil', '${kbps}kbit']);
|
||||
} catch (e) {
|
||||
if (e.toString().contains('File exists')) {
|
||||
print('Class exists, updating with change command...');
|
||||
await _runTcCommand(['class', 'change', 'dev', 'wg0', 'classid', '1:$mark', 'htb', 'rate', '${speedKbps}kbit', 'ceil', '${speedKbps}kbit']);
|
||||
print('Upload class exists, updating...');
|
||||
await _runTcCommand(['class', 'change', 'dev', 'wg0', 'classid', '1:$uploadMark', 'htb', 'rate', '${kbps}kbit', 'ceil', '${kbps}kbit']);
|
||||
} else {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
print('Running tc filter add command...');
|
||||
await _runTcCommand(['filter', 'add', 'dev', 'wg0', 'protocol', 'ip', 'parent', '1:', 'prio', '1', 'handle', mark.toString(), 'fw', 'flowid', '1:$mark']);
|
||||
// Download class (traffic TO peer)
|
||||
try {
|
||||
await _runTcCommand(['class', 'add', 'dev', 'wg0', 'parent', '1:1', 'classid', '1:$downloadMark', 'htb', 'rate', '${kbps}kbit', 'ceil', '${kbps}kbit']);
|
||||
} catch (e) {
|
||||
if (e.toString().contains('File exists')) {
|
||||
print('Download class exists, updating...');
|
||||
await _runTcCommand(['class', 'change', 'dev', 'wg0', 'classid', '1:$downloadMark', 'htb', 'rate', '${kbps}kbit', 'ceil', '${kbps}kbit']);
|
||||
} else {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
print('Running tc filter add commands...');
|
||||
// Upload filter
|
||||
try {
|
||||
await _runTcCommand(['filter', 'add', 'dev', 'wg0', 'protocol', 'ip', 'parent', '1:', 'prio', '1', 'handle', uploadMark.toString(), 'fw', 'flowid', '1:$uploadMark']);
|
||||
} catch (e) {
|
||||
if (!e.toString().contains('File exists')) rethrow;
|
||||
}
|
||||
|
||||
// Download filter
|
||||
try {
|
||||
await _runTcCommand(['filter', 'add', 'dev', 'wg0', 'protocol', 'ip', 'parent', '1:', 'prio', '1', 'handle', downloadMark.toString(), 'fw', 'flowid', '1:$downloadMark']);
|
||||
} catch (e) {
|
||||
if (!e.toString().contains('File exists')) rethrow;
|
||||
}
|
||||
|
||||
print('Speed limit set successfully for $peerIP');
|
||||
} catch (e) {
|
||||
@@ -57,8 +90,8 @@ class TrafficControlService {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> setDataCap(String peerIP, int dataCapMB) async {
|
||||
final quotaBytes = dataCapMB * 1024 * 1024;
|
||||
static Future<void> setDataCap(String peerIP, int quotaBytes) async {
|
||||
print('Setting data cap for peer $peerIP to $quotaBytes bytes');
|
||||
|
||||
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']);
|
||||
|
||||
Reference in New Issue
Block a user