Enhance traffic control by dynamically detecting outgoing interface and updating iptables rules for upload and download traffic

This commit is contained in:
ImBenji
2025-08-05 14:15:29 +01:00
parent 48e241d99f
commit 171dd1d77e
5 changed files with 110 additions and 1 deletions

13
.idea/deviceManager.xml generated Normal file
View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@@ -70,11 +70,13 @@ Set bidirectional bandwidth limits for a peer (controls both upload and download
} }
``` ```
*Note: 125000 bytes/s = 1000 kbps = 1 Mbps* *Note: 125000 bytes/s = 1000 kbps = 1 Mbps*
*Use -1 for unlimited speed*
**Common Speed Conversions:** **Common Speed Conversions:**
- 125 KB/s = 1 Mbps - 125 KB/s = 1 Mbps
- 1,250 KB/s = 10 Mbps - 1,250 KB/s = 10 Mbps
- 12,500 KB/s = 100 Mbps - 12,500 KB/s = 100 Mbps
- -1 = Unlimited
#### `POST /api/peers/data-cap` #### `POST /api/peers/data-cap`
Set total data usage limits for a peer using iptables quota rules. Set total data usage limits for a peer using iptables quota rules.
@@ -87,11 +89,13 @@ Set total data usage limits for a peer using iptables quota rules.
} }
``` ```
*Note: 1073741824 bytes = 1 GB* *Note: 1073741824 bytes = 1 GB*
*Use -1 for unlimited data*
**Common Data Conversions:** **Common Data Conversions:**
- 1 GB = 1,073,741,824 bytes - 1 GB = 1,073,741,824 bytes
- 10 GB = 10,737,418,240 bytes - 10 GB = 10,737,418,240 bytes
- 100 GB = 107,374,182,400 bytes - 100 GB = 107,374,182,400 bytes
- -1 = Unlimited
#### `POST /api/peers/config` #### `POST /api/peers/config`
Retrieve peer configuration (currently not implemented - returns 404). Retrieve peer configuration (currently not implemented - returns 404).

View File

@@ -4,7 +4,7 @@ import 'package:waylume_server/core/utils.dart';
class ServerService { class ServerService {
static Future<void> registerServer() async { static Future<void> registerServer() async {
String ip = await getWanIp(); String ip = "${await getWanIp()}:${fromEnivronment("EXTERNAL_PORT") ?? "3000"}";
var existsCheck = await SUPABASE_CLIENT var existsCheck = await SUPABASE_CLIENT
.from("waylume_servers") .from("waylume_servers")

View File

@@ -3,6 +3,14 @@ import 'dart:io';
class TrafficControlService { class TrafficControlService {
static Future<void> setSpeedLimit(String peerIP, int bytesPerSecond) async { static Future<void> setSpeedLimit(String peerIP, int bytesPerSecond) async {
final mark = _getMarkForIP(peerIP); final mark = _getMarkForIP(peerIP);
// Handle unlimited speed (-1)
if (bytesPerSecond == -1) {
print('Removing speed limit for peer $peerIP (unlimited)');
await _removeSpeedLimit(peerIP, mark);
return;
}
final kbps = (bytesPerSecond * 8 / 1000).round(); // Convert bytes/s to kbps 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)'); print('Setting speed limit for peer $peerIP to ${bytesPerSecond} bytes/s (${kbps}kbps, mark: $mark)');
@@ -125,8 +133,18 @@ class TrafficControlService {
} }
static Future<void> setDataCap(String peerIP, int quotaBytes) async { static Future<void> setDataCap(String peerIP, int quotaBytes) async {
// Handle unlimited data cap (-1)
if (quotaBytes == -1) {
print('Removing data cap for peer $peerIP (unlimited)');
await _removeDataCap(peerIP);
return;
}
print('Setting data cap for peer $peerIP to $quotaBytes bytes'); print('Setting data cap for peer $peerIP to $quotaBytes bytes');
// Remove existing data cap rules first
await _removeDataCap(peerIP);
await _runIptablesCommand(['-I', 'FORWARD', '-s', peerIP, '-m', 'quota', '--quota', quotaBytes.toString(), '-j', 'ACCEPT']); 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(['-I', 'FORWARD', '-d', peerIP, '-m', 'quota', '--quota', quotaBytes.toString(), '-j', 'ACCEPT']);
await _runIptablesCommand(['-A', 'FORWARD', '-s', peerIP, '-j', 'DROP']); await _runIptablesCommand(['-A', 'FORWARD', '-s', peerIP, '-j', 'DROP']);
@@ -157,6 +175,74 @@ class TrafficControlService {
return 'eth0'; return 'eth0';
} }
static Future<void> _removeSpeedLimit(String peerIP, int mark) async {
final uploadMark = mark;
final downloadMark = mark + 1000;
print('Removing speed limit rules for $peerIP...');
// Remove iptables mangle rules
try {
await _runIptablesCommand(['-t', 'mangle', '-D', 'POSTROUTING', '-s', peerIP, '-j', 'MARK', '--set-mark', uploadMark.toString()]);
} catch (e) { /* Rule doesn't exist, ignore */ }
try {
await _runIptablesCommand(['-t', 'mangle', '-D', 'FORWARD', '-d', peerIP, '-j', 'MARK', '--set-mark', downloadMark.toString()]);
} catch (e) { /* Rule doesn't exist, ignore */ }
// Remove tc classes and filters
final outgoingInterface = await _detectOutgoingInterface();
try {
await _runTcCommand(['filter', 'del', 'dev', outgoingInterface, 'protocol', 'ip', 'parent', '2:', 'prio', '1', 'handle', uploadMark.toString(), 'fw']);
} catch (e) { /* Filter doesn't exist, ignore */ }
try {
await _runTcCommand(['filter', 'del', 'dev', 'wg0', 'protocol', 'ip', 'parent', '1:', 'prio', '1', 'handle', downloadMark.toString(), 'fw']);
} catch (e) { /* Filter doesn't exist, ignore */ }
try {
await _runTcCommand(['class', 'del', 'dev', outgoingInterface, 'classid', '2:$uploadMark']);
} catch (e) { /* Class doesn't exist, ignore */ }
try {
await _runTcCommand(['class', 'del', 'dev', 'wg0', 'classid', '1:$downloadMark']);
} catch (e) { /* Class doesn't exist, ignore */ }
print('Speed limit removed for $peerIP');
}
static Future<void> _removeDataCap(String peerIP) async {
print('Removing data cap rules for $peerIP...');
// Remove quota rules (these are harder to target specifically, so we try common patterns)
try {
await _runIptablesCommand(['-D', 'FORWARD', '-s', peerIP, '-j', 'DROP']);
} catch (e) { /* Rule doesn't exist, ignore */ }
try {
await _runIptablesCommand(['-D', 'FORWARD', '-d', peerIP, '-j', 'DROP']);
} catch (e) { /* Rule doesn't exist, ignore */ }
// Remove quota rules by flushing and re-adding basic rules
// This is a bit aggressive but necessary since quota rules are hard to target specifically
try {
final result = await Process.run('iptables', ['-S', 'FORWARD']);
if (result.exitCode == 0) {
final lines = result.stdout.toString().split('\n');
for (final line in lines) {
if (line.contains(peerIP) && line.contains('quota')) {
// Extract the rule and convert -A to -D to delete it
final deleteRule = line.replaceFirst('-A', '-D');
try {
await Process.run('iptables', deleteRule.split(' ').skip(1).toList());
} catch (e) { /* Ignore individual failures */ }
}
}
}
} catch (e) {
print('Failed to remove specific quota rules: $e');
}
print('Data cap removed for $peerIP');
}
static Future<void> _runIptablesCommand(List<String> args) async { static Future<void> _runIptablesCommand(List<String> args) async {
final command = 'iptables ${args.join(' ')}'; final command = 'iptables ${args.join(' ')}';
print('Executing: $command'); print('Executing: $command');