Add proxy routing and sing-box support

This commit is contained in:
Fabian Freund
2026-05-22 18:16:31 +02:00
parent 51289f1266
commit a5974617aa
262 changed files with 32003 additions and 3962 deletions
@@ -0,0 +1,345 @@
import 'dart:convert';
import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:weblibre/features/proxy/data/forms/singbox_form_specs.dart';
import 'package:weblibre/features/proxy/data/parsers/singbox_proxy_uri.dart';
void main() {
group('SingboxProxyFormSpec', () {
test('builds public and secret JSON for Shadowsocks', () {
final spec = singboxProxyFormSpecs[SingboxProxyProfileType.shadowsocks]!;
final values = {
'server': 'ss.example.com',
'server_port': '8388',
'method': '2022-blake3-aes-128-gcm',
'password': 'secret',
};
expect(spec.validate(values), isNull);
final config = jsonDecode(spec.toConfigJson(values));
final secrets = jsonDecode(spec.toSecretJson(values)!);
expect(config, {
'type': 'shadowsocks',
'server': 'ss.example.com',
'server_port': 8388,
'method': '2022-blake3-aes-128-gcm',
});
expect(secrets, {'password': 'secret'});
});
test('hydrates structured values from public and secret JSON', () {
final spec = singboxProxyFormSpecs[SingboxProxyProfileType.vless]!;
final values = spec.valuesFromJson(
configJson: '''
{
"type": "vless",
"server": "vless.example.com",
"server_port": 443,
"flow": "xtls-rprx-vision"
}''',
secretJson: '{"uuid":"00000000-0000-0000-0000-000000000000"}',
);
expect(values['server'], 'vless.example.com');
expect(values['server_port'], '443');
expect(values['uuid'], '00000000-0000-0000-0000-000000000000');
expect(values['flow'], 'xtls-rprx-vision');
});
test('rejects invalid server ports', () {
final spec = singboxProxyFormSpecs[SingboxProxyProfileType.http]!;
expect(
spec.validate({'server': 'proxy.example.com', 'server_port': '70000'}),
'Server Port must be between 1 and 65535.',
);
});
test('builds nested advanced config fields', () {
final spec = singboxProxyFormSpecs[SingboxProxyProfileType.vless]!;
final values = {
'server': 'vless.example.com',
'server_port': '443',
'uuid': '00000000-0000-0000-0000-000000000000',
'flow': 'xtls-rprx-vision',
'tls.enabled': 'true',
'tls.server_name': 'front.example.com',
'tls.alpn': 'h2, http/1.1',
'transport.type': 'ws',
'transport.path': '/proxy',
'multiplex.enabled': 'true',
'multiplex.max_connections': '4',
'domain_strategy': 'prefer_ipv4',
};
expect(spec.validate(values), isNull);
final config =
jsonDecode(spec.toConfigJson(values)) as Map<String, dynamic>;
final secrets =
jsonDecode(spec.toSecretJson(values)!) as Map<String, dynamic>;
expect(config['tls'], {
'enabled': true,
'server_name': 'front.example.com',
'alpn': ['h2', 'http/1.1'],
});
expect(config['transport'], {'type': 'ws', 'path': '/proxy'});
expect(config['multiplex'], {'enabled': true, 'max_connections': 4});
expect(config['domain_strategy'], 'prefer_ipv4');
expect(secrets, {'uuid': '00000000-0000-0000-0000-000000000000'});
});
test('builds public and secret JSON for WireGuard', () {
final spec = singboxProxyFormSpecs[SingboxProxyProfileType.wireguard]!;
final values = {
'server': 'wg.example.com',
'server_port': '51820',
'local_address': '10.0.0.2/32\nfd00::2/128',
'private_key': 'private-key',
'peer_public_key': 'peer-public-key',
'pre_shared_key': 'pre-shared-key',
'mtu': '1420',
'reserved': '1, 2, 3',
};
expect(spec.validate(values), isNull);
final config = jsonDecode(spec.toConfigJson(values));
final secrets = jsonDecode(spec.toSecretJson(values)!);
expect(config, {
'type': 'wireguard',
'server': 'wg.example.com',
'server_port': 51820,
'local_address': ['10.0.0.2/32', 'fd00::2/128'],
'peer_public_key': 'peer-public-key',
'mtu': 1420,
'reserved': [1, 2, 3],
});
expect(secrets, {
'private_key': 'private-key',
'pre_shared_key': 'pre-shared-key',
});
});
test('hydrates WireGuard values from public and secret JSON', () {
final spec = singboxProxyFormSpecs[SingboxProxyProfileType.wireguard]!;
final values = spec.valuesFromJson(
configJson: '''
{
"type": "wireguard",
"server": "wg.example.com",
"server_port": 51820,
"local_address": ["10.0.0.2/32"],
"peer_public_key": "peer-public-key",
"mtu": 1420,
"reserved": [1, 2, 3]
}''',
secretJson: '''
{
"private_key": "private-key",
"pre_shared_key": "pre-shared-key"
}''',
);
expect(values['server'], 'wg.example.com');
expect(values['server_port'], '51820');
expect(values['local_address'], '10.0.0.2/32');
expect(values['private_key'], 'private-key');
expect(values['peer_public_key'], 'peer-public-key');
expect(values['pre_shared_key'], 'pre-shared-key');
expect(values['mtu'], '1420');
expect(values['reserved'], '1\n2\n3');
});
test('rejects invalid WireGuard reserved bytes', () {
final spec = singboxProxyFormSpecs[SingboxProxyProfileType.wireguard]!;
expect(
spec.validate({
'server': 'wg.example.com',
'server_port': '51820',
'local_address': '10.0.0.2/32',
'private_key': 'private-key',
'peer_public_key': 'peer-public-key',
'mtu': '1420',
'reserved': '1, 2',
}),
'Reserved Bytes must contain 3 numbers.',
);
});
});
group('importSingboxProxyUri', () {
test('imports Shadowsocks SIP002 URI', () {
final credentials = base64Url.encode(
utf8.encode('aes-256-gcm:secret-password'),
);
final imported = importSingboxProxyUri(
'ss://$credentials@ss.example.com:8388#Work%20SS',
);
expect(imported.type, SingboxProxyProfileType.shadowsocks);
expect(imported.name, 'Work SS');
expect(imported.values, {
'server': 'ss.example.com',
'server_port': '8388',
'method': 'aes-256-gcm',
'password': 'secret-password',
});
});
test('imports legacy base64 Shadowsocks URI', () {
final payload = base64Url.encode(
utf8.encode('chacha20-ietf-poly1305:secret@[2001:db8::1]:8388'),
);
final imported = importSingboxProxyUri('ss://$payload');
expect(imported.type, SingboxProxyProfileType.shadowsocks);
expect(imported.values['server'], '2001:db8::1');
expect(imported.values['server_port'], '8388');
expect(imported.values['method'], 'chacha20-ietf-poly1305');
expect(imported.values['password'], 'secret');
});
test('imports Trojan URI', () {
final imported = importSingboxProxyUri(
'trojan://secret%20password@trojan.example.com:443#Trojan',
);
expect(imported.type, SingboxProxyProfileType.trojan);
expect(imported.name, 'Trojan');
expect(imported.values, {
'server': 'trojan.example.com',
'server_port': '443',
'password': 'secret password',
'tls.enabled': 'true',
});
});
test('imports VLESS URI', () {
final imported = importSingboxProxyUri(
'vless://00000000-0000-0000-0000-000000000000@vless.example.com:443?flow=xtls-rprx-vision#VLESS',
);
expect(imported.type, SingboxProxyProfileType.vless);
expect(imported.name, 'VLESS');
expect(imported.values, {
'server': 'vless.example.com',
'server_port': '443',
'uuid': '00000000-0000-0000-0000-000000000000',
'flow': 'xtls-rprx-vision',
});
});
test('imports VMess URI', () {
final payload = base64Url.encode(
utf8.encode(
jsonEncode({
'v': '2',
'ps': 'VMess',
'add': 'vmess.example.com',
'port': '443',
'id': '00000000-0000-0000-0000-000000000000',
'aid': '0',
'scy': 'auto',
}),
),
);
final imported = importSingboxProxyUri('vmess://$payload');
expect(imported.type, SingboxProxyProfileType.vmess);
expect(imported.name, 'VMess');
expect(imported.values, {
'server': 'vmess.example.com',
'server_port': '443',
'uuid': '00000000-0000-0000-0000-000000000000',
'security': 'auto',
'alter_id': '0',
'tls.enabled': '',
'tls.server_name': '',
'transport.type': '',
'transport.path': '',
});
});
test('imports SOCKS URI', () {
final imported = importSingboxProxyUri(
'socks://user:secret@socks.example.com:1080#SOCKS',
);
expect(imported.type, SingboxProxyProfileType.socks);
expect(imported.name, 'SOCKS');
expect(imported.values, {
'server': 'socks.example.com',
'server_port': '1080',
'version': '5',
'username': 'user',
'password': 'secret',
});
});
test('imports HTTPS proxy URI', () {
final imported = importSingboxProxyUri(
'https://user:secret@proxy.example.com:443?sni=proxy.example.com#HTTP',
);
expect(imported.type, SingboxProxyProfileType.http);
expect(imported.values['server'], 'proxy.example.com');
expect(imported.values['server_port'], '443');
expect(imported.values['username'], 'user');
expect(imported.values['password'], 'secret');
expect(imported.values['tls.enabled'], 'true');
expect(imported.values['tls.server_name'], 'proxy.example.com');
});
test('imports Hysteria2 URI', () {
final imported = importSingboxProxyUri(
'hy2://secret@hy2.example.com:443?obfs=salamander&obfs-password=obfs-secret&sni=front.example.com#HY2',
);
expect(imported.type, SingboxProxyProfileType.hysteria2);
expect(imported.name, 'HY2');
expect(imported.values['server'], 'hy2.example.com');
expect(imported.values['server_port'], '443');
expect(imported.values['password'], 'secret');
expect(imported.values['obfs.type'], 'salamander');
expect(imported.values['obfs.password'], 'obfs-secret');
expect(imported.values['tls.enabled'], 'true');
expect(imported.values['tls.server_name'], 'front.example.com');
});
test('imports TUIC URI', () {
final imported = importSingboxProxyUri(
'tuic://00000000-0000-0000-0000-000000000000:secret@tuic.example.com:443?congestion_control=bbr&udp_relay_mode=quic&sni=front.example.com#TUIC',
);
expect(imported.type, SingboxProxyProfileType.tuic);
expect(imported.name, 'TUIC');
expect(imported.values['server'], 'tuic.example.com');
expect(imported.values['server_port'], '443');
expect(imported.values['uuid'], '00000000-0000-0000-0000-000000000000');
expect(imported.values['password'], 'secret');
expect(imported.values['congestion_control'], 'bbr');
expect(imported.values['udp_relay_mode'], 'quic');
expect(imported.values['tls.enabled'], 'true');
expect(imported.values['tls.server_name'], 'front.example.com');
});
test('rejects unsupported URI schemes', () {
expect(
() => importSingboxProxyUri('hysteria2://example.com:443'),
throwsFormatException,
);
});
});
}
@@ -0,0 +1,56 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:weblibre/features/proxy/data/models/wireguard_config_import.dart';
void main() {
group('WireguardConfigImport', () {
test('imports standard WireGuard config text', () {
final imported = WireguardConfigImport.fromConfigText('''
[Interface]
PrivateKey = private-key
Address = 10.0.0.2/32, fd00::2/128
MTU = 1420
DNS = 1.1.1.1
[Peer]
PublicKey = peer-public-key
PresharedKey = pre-shared-key
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = wg.example.com:51820
''');
expect(imported.values, {
'server': 'wg.example.com',
'server_port': '51820',
'local_address': '10.0.0.2/32\nfd00::2/128',
'private_key': 'private-key',
'peer_public_key': 'peer-public-key',
'pre_shared_key': 'pre-shared-key',
'mtu': '1420',
});
expect(imported.primaryDnsAddress, 'udp://1.1.1.1');
});
test('imports bracketed IPv6 WireGuard endpoint', () {
final imported = WireguardConfigImport.fromConfigText('''
[Interface]
PrivateKey = private-key
Address = fd00::2/128
[Peer]
PublicKey = peer-public-key
Endpoint = [2001:db8::1]:51820
''');
expect(imported.values['server'], '2001:db8::1');
expect(imported.values['server_port'], '51820');
expect(imported.values['mtu'], '1408');
});
test('rejects configs without interface and peer sections', () {
expect(
() => WireguardConfigImport.fromConfigText('PrivateKey = private-key'),
throwsFormatException,
);
});
});
}