Add proxy routing and sing-box support
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
enum SingboxFieldKind {
|
||||
text,
|
||||
secret,
|
||||
integer,
|
||||
integerList,
|
||||
port,
|
||||
boolean,
|
||||
stringList,
|
||||
}
|
||||
|
||||
class SingboxProxyFormField {
|
||||
final String key;
|
||||
final String label;
|
||||
final String? helperText;
|
||||
final String? defaultValue;
|
||||
final bool required;
|
||||
final SingboxFieldKind kind;
|
||||
final int? exactListLength;
|
||||
final int? minValue;
|
||||
final int? maxValue;
|
||||
|
||||
const SingboxProxyFormField({
|
||||
required this.key,
|
||||
required this.label,
|
||||
this.helperText,
|
||||
this.defaultValue,
|
||||
this.required = false,
|
||||
this.kind = SingboxFieldKind.text,
|
||||
this.exactListLength,
|
||||
this.minValue,
|
||||
this.maxValue,
|
||||
});
|
||||
|
||||
bool get isSecret => kind == SingboxFieldKind.secret;
|
||||
bool get isNumber =>
|
||||
kind == SingboxFieldKind.integer || kind == SingboxFieldKind.port;
|
||||
bool get isIntegerList => kind == SingboxFieldKind.integerList;
|
||||
bool get isPort => kind == SingboxFieldKind.port;
|
||||
bool get isBoolean => kind == SingboxFieldKind.boolean;
|
||||
bool get isStringList => kind == SingboxFieldKind.stringList;
|
||||
}
|
||||
|
||||
/// Shared parsing of boolean form values. Returns null when the text doesn't
|
||||
/// look like a boolean (treated as "unset"), so callers can distinguish absent
|
||||
/// from explicitly-false.
|
||||
bool? parseFormBool(String value) {
|
||||
return switch (value.trim().toLowerCase()) {
|
||||
'true' || 'yes' || '1' || 'on' => true,
|
||||
'false' || 'no' || '0' || 'off' => false,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
/// Splits a multi-value form input on newlines or commas, trims, drops blanks.
|
||||
List<String> splitFormStringList(String value) {
|
||||
return value
|
||||
.split(RegExp(r'[\n,]'))
|
||||
.map((item) => item.trim())
|
||||
.where((item) => item.isNotEmpty)
|
||||
.toList();
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart';
|
||||
import 'package:weblibre/features/proxy/data/forms/singbox_form_field.dart';
|
||||
|
||||
class SingboxProxyFormSpec {
|
||||
final SingboxProxyProfileType type;
|
||||
final String outboundType;
|
||||
final List<SingboxProxyFormField> fields;
|
||||
|
||||
const SingboxProxyFormSpec({
|
||||
required this.type,
|
||||
required this.outboundType,
|
||||
required this.fields,
|
||||
});
|
||||
|
||||
String? validate(Map<String, String> values) {
|
||||
for (final field in fields) {
|
||||
final value = values[field.key]?.trim() ?? '';
|
||||
if (field.required && value.isEmpty) {
|
||||
return '${field.label} is required.';
|
||||
}
|
||||
if (field.isNumber && value.isNotEmpty) {
|
||||
final parsed = int.tryParse(value);
|
||||
if (parsed == null || parsed < 1) {
|
||||
return '${field.label} must be a positive number.';
|
||||
}
|
||||
if (field.isPort && parsed > 65535) {
|
||||
return '${field.label} must be between 1 and 65535.';
|
||||
}
|
||||
}
|
||||
if (field.isIntegerList && value.isNotEmpty) {
|
||||
final items = splitFormStringList(value);
|
||||
final exactListLength = field.exactListLength;
|
||||
if (exactListLength != null && items.length != exactListLength) {
|
||||
return '${field.label} must contain $exactListLength numbers.';
|
||||
}
|
||||
for (final item in items) {
|
||||
final parsed = int.tryParse(item);
|
||||
if (parsed == null) {
|
||||
return '${field.label} must contain only numbers.';
|
||||
}
|
||||
final minValue = field.minValue;
|
||||
if (minValue != null && parsed < minValue) {
|
||||
return '${field.label} must contain numbers greater than or equal to $minValue.';
|
||||
}
|
||||
final maxValue = field.maxValue;
|
||||
if (maxValue != null && parsed > maxValue) {
|
||||
return '${field.label} must contain numbers less than or equal to $maxValue.';
|
||||
}
|
||||
}
|
||||
}
|
||||
if (field.isBoolean && value.isNotEmpty && parseFormBool(value) == null) {
|
||||
return '${field.label} must be true or false.';
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
String toConfigJson(Map<String, String> values) {
|
||||
final config = <String, dynamic>{'type': outboundType};
|
||||
for (final field in fields.where((field) => !field.isSecret)) {
|
||||
final value = values[field.key]?.trim() ?? '';
|
||||
if (value.isEmpty) continue;
|
||||
_setJsonValue(config, field.key, _fieldJsonValue(field, value));
|
||||
}
|
||||
|
||||
return const JsonEncoder.withIndent(' ').convert(config);
|
||||
}
|
||||
|
||||
String? toSecretJson(Map<String, String> values) {
|
||||
final secrets = <String, dynamic>{};
|
||||
for (final field in fields.where((field) => field.isSecret)) {
|
||||
final value = values[field.key]?.trim() ?? '';
|
||||
if (value.isEmpty) continue;
|
||||
_setJsonValue(secrets, field.key, _fieldJsonValue(field, value));
|
||||
}
|
||||
|
||||
if (secrets.isEmpty) return null;
|
||||
return const JsonEncoder.withIndent(' ').convert(secrets);
|
||||
}
|
||||
|
||||
Map<String, String> valuesFromJson({
|
||||
required String configJson,
|
||||
String? secretJson,
|
||||
}) {
|
||||
final config = _decodeJsonObject(configJson) ?? const <String, dynamic>{};
|
||||
final secrets = secretJson == null
|
||||
? const <String, dynamic>{}
|
||||
: _decodeJsonObject(secretJson) ?? const <String, dynamic>{};
|
||||
final merged = {...config, ...secrets};
|
||||
|
||||
return {
|
||||
for (final field in fields)
|
||||
field.key: _fieldTextValue(
|
||||
_jsonValueAt(merged, field.key),
|
||||
field.defaultValue ?? '',
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic>? _decodeJsonObject(String rawJson) {
|
||||
try {
|
||||
final decoded = jsonDecode(rawJson) as Object?;
|
||||
if (decoded is Map<String, dynamic>) return decoded;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Object _fieldJsonValue(SingboxProxyFormField field, String value) {
|
||||
if (field.isNumber) return int.parse(value);
|
||||
if (field.isIntegerList) {
|
||||
return splitFormStringList(value).map(int.parse).toList();
|
||||
}
|
||||
if (field.isBoolean) return parseFormBool(value)!;
|
||||
if (field.isStringList) return splitFormStringList(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
String _fieldTextValue(Object? value, String fallback) {
|
||||
if (value == null) return fallback;
|
||||
if (value is List) return value.map((item) => item.toString()).join('\n');
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
void _setJsonValue(Map<String, dynamic> target, String path, Object value) {
|
||||
final segments = path.split('.');
|
||||
var current = target;
|
||||
for (final segment in segments.take(segments.length - 1)) {
|
||||
current =
|
||||
current.putIfAbsent(segment, () => <String, dynamic>{})
|
||||
as Map<String, dynamic>;
|
||||
}
|
||||
current[segments.last] = value;
|
||||
}
|
||||
|
||||
Object? _jsonValueAt(Map<String, dynamic> source, String path) {
|
||||
Object? current = source;
|
||||
for (final segment in path.split('.')) {
|
||||
if (current is! Map<String, dynamic>) return null;
|
||||
current = current[segment];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart';
|
||||
import 'package:weblibre/features/proxy/data/forms/singbox_form_field.dart';
|
||||
import 'package:weblibre/features/proxy/data/forms/singbox_form_spec.dart';
|
||||
|
||||
const _serverField = SingboxProxyFormField(
|
||||
key: 'server',
|
||||
label: 'Server Address',
|
||||
required: true,
|
||||
);
|
||||
const _serverPortField = SingboxProxyFormField(
|
||||
key: 'server_port',
|
||||
label: 'Server Port',
|
||||
required: true,
|
||||
kind: SingboxFieldKind.port,
|
||||
);
|
||||
const _usernameField = SingboxProxyFormField(
|
||||
key: 'username',
|
||||
label: 'Username',
|
||||
kind: SingboxFieldKind.secret,
|
||||
);
|
||||
const _passwordField = SingboxProxyFormField(
|
||||
key: 'password',
|
||||
label: 'Password',
|
||||
required: true,
|
||||
kind: SingboxFieldKind.secret,
|
||||
);
|
||||
const _optionalPasswordField = SingboxProxyFormField(
|
||||
key: 'password',
|
||||
label: 'Password',
|
||||
kind: SingboxFieldKind.secret,
|
||||
);
|
||||
const _uuidField = SingboxProxyFormField(
|
||||
key: 'uuid',
|
||||
label: 'UUID',
|
||||
required: true,
|
||||
kind: SingboxFieldKind.secret,
|
||||
);
|
||||
const _tlsFields = [
|
||||
SingboxProxyFormField(
|
||||
key: 'tls.enabled',
|
||||
label: 'TLS Enabled',
|
||||
helperText: 'true or false.',
|
||||
kind: SingboxFieldKind.boolean,
|
||||
),
|
||||
SingboxProxyFormField(key: 'tls.server_name', label: 'TLS Server Name'),
|
||||
SingboxProxyFormField(
|
||||
key: 'tls.insecure',
|
||||
label: 'Allow Invalid TLS Certificates',
|
||||
helperText: 'true or false.',
|
||||
kind: SingboxFieldKind.boolean,
|
||||
),
|
||||
SingboxProxyFormField(
|
||||
key: 'tls.alpn',
|
||||
label: 'TLS ALPN',
|
||||
helperText: 'Comma-separated or one value per line.',
|
||||
kind: SingboxFieldKind.stringList,
|
||||
),
|
||||
];
|
||||
const _transportFields = [
|
||||
SingboxProxyFormField(
|
||||
key: 'transport.type',
|
||||
label: 'Transport Type',
|
||||
helperText: 'For example ws, http, grpc, or quic.',
|
||||
),
|
||||
SingboxProxyFormField(key: 'transport.path', label: 'Transport Path'),
|
||||
SingboxProxyFormField(
|
||||
key: 'transport.service_name',
|
||||
label: 'gRPC Service Name',
|
||||
),
|
||||
];
|
||||
const _multiplexFields = [
|
||||
SingboxProxyFormField(
|
||||
key: 'multiplex.enabled',
|
||||
label: 'Multiplex Enabled',
|
||||
helperText: 'true or false.',
|
||||
kind: SingboxFieldKind.boolean,
|
||||
),
|
||||
SingboxProxyFormField(key: 'multiplex.protocol', label: 'Multiplex Protocol'),
|
||||
SingboxProxyFormField(
|
||||
key: 'multiplex.max_connections',
|
||||
label: 'Multiplex Max Connections',
|
||||
kind: SingboxFieldKind.integer,
|
||||
),
|
||||
];
|
||||
const _dialFields = [
|
||||
SingboxProxyFormField(key: 'detour', label: 'Dial Detour'),
|
||||
SingboxProxyFormField(key: 'bind_interface', label: 'Bind Interface'),
|
||||
SingboxProxyFormField(
|
||||
key: 'routing_mark',
|
||||
label: 'Routing Mark',
|
||||
kind: SingboxFieldKind.integer,
|
||||
),
|
||||
SingboxProxyFormField(
|
||||
key: 'domain_strategy',
|
||||
label: 'Domain Strategy',
|
||||
helperText: 'For example prefer_ipv4 or prefer_ipv6.',
|
||||
),
|
||||
SingboxProxyFormField(
|
||||
key: 'connect_timeout',
|
||||
label: 'Connect Timeout',
|
||||
helperText: 'For example 5s.',
|
||||
),
|
||||
];
|
||||
const _v2rayAdvancedFields = [
|
||||
..._tlsFields,
|
||||
..._transportFields,
|
||||
..._multiplexFields,
|
||||
..._dialFields,
|
||||
];
|
||||
const _commonAdvancedFields = [..._multiplexFields, ..._dialFields];
|
||||
|
||||
const singboxProxyFormSpecs = <SingboxProxyProfileType, SingboxProxyFormSpec>{
|
||||
SingboxProxyProfileType.socks: SingboxProxyFormSpec(
|
||||
type: SingboxProxyProfileType.socks,
|
||||
outboundType: 'socks',
|
||||
fields: [
|
||||
_serverField,
|
||||
_serverPortField,
|
||||
SingboxProxyFormField(
|
||||
key: 'version',
|
||||
label: 'SOCKS Version',
|
||||
defaultValue: '5',
|
||||
kind: SingboxFieldKind.integer,
|
||||
),
|
||||
_usernameField,
|
||||
_optionalPasswordField,
|
||||
..._commonAdvancedFields,
|
||||
],
|
||||
),
|
||||
SingboxProxyProfileType.http: SingboxProxyFormSpec(
|
||||
type: SingboxProxyProfileType.http,
|
||||
outboundType: 'http',
|
||||
fields: [
|
||||
_serverField,
|
||||
_serverPortField,
|
||||
_usernameField,
|
||||
_optionalPasswordField,
|
||||
..._tlsFields,
|
||||
..._commonAdvancedFields,
|
||||
],
|
||||
),
|
||||
SingboxProxyProfileType.shadowsocks: SingboxProxyFormSpec(
|
||||
type: SingboxProxyProfileType.shadowsocks,
|
||||
outboundType: 'shadowsocks',
|
||||
fields: [
|
||||
_serverField,
|
||||
_serverPortField,
|
||||
SingboxProxyFormField(
|
||||
key: 'method',
|
||||
label: 'Method',
|
||||
defaultValue: '2022-blake3-aes-128-gcm',
|
||||
required: true,
|
||||
),
|
||||
_passwordField,
|
||||
..._commonAdvancedFields,
|
||||
],
|
||||
),
|
||||
SingboxProxyProfileType.vmess: SingboxProxyFormSpec(
|
||||
type: SingboxProxyProfileType.vmess,
|
||||
outboundType: 'vmess',
|
||||
fields: [
|
||||
_serverField,
|
||||
_serverPortField,
|
||||
_uuidField,
|
||||
SingboxProxyFormField(
|
||||
key: 'security',
|
||||
label: 'Security',
|
||||
defaultValue: 'auto',
|
||||
),
|
||||
SingboxProxyFormField(
|
||||
key: 'alter_id',
|
||||
label: 'Alter ID',
|
||||
kind: SingboxFieldKind.integer,
|
||||
),
|
||||
..._v2rayAdvancedFields,
|
||||
],
|
||||
),
|
||||
SingboxProxyProfileType.vless: SingboxProxyFormSpec(
|
||||
type: SingboxProxyProfileType.vless,
|
||||
outboundType: 'vless',
|
||||
fields: [
|
||||
_serverField,
|
||||
_serverPortField,
|
||||
_uuidField,
|
||||
SingboxProxyFormField(key: 'flow', label: 'Flow'),
|
||||
..._v2rayAdvancedFields,
|
||||
],
|
||||
),
|
||||
SingboxProxyProfileType.trojan: SingboxProxyFormSpec(
|
||||
type: SingboxProxyProfileType.trojan,
|
||||
outboundType: 'trojan',
|
||||
fields: [
|
||||
_serverField,
|
||||
_serverPortField,
|
||||
_passwordField,
|
||||
..._v2rayAdvancedFields,
|
||||
],
|
||||
),
|
||||
SingboxProxyProfileType.naive: SingboxProxyFormSpec(
|
||||
type: SingboxProxyProfileType.naive,
|
||||
outboundType: 'naive',
|
||||
fields: [
|
||||
_serverField,
|
||||
_serverPortField,
|
||||
_usernameField,
|
||||
_passwordField,
|
||||
..._tlsFields,
|
||||
..._commonAdvancedFields,
|
||||
],
|
||||
),
|
||||
SingboxProxyProfileType.hysteria: SingboxProxyFormSpec(
|
||||
type: SingboxProxyProfileType.hysteria,
|
||||
outboundType: 'hysteria',
|
||||
fields: [
|
||||
_serverField,
|
||||
_serverPortField,
|
||||
SingboxProxyFormField(
|
||||
key: 'auth_str',
|
||||
label: 'Auth String',
|
||||
kind: SingboxFieldKind.secret,
|
||||
),
|
||||
SingboxProxyFormField(key: 'up', label: 'Upload Bandwidth'),
|
||||
SingboxProxyFormField(key: 'down', label: 'Download Bandwidth'),
|
||||
SingboxProxyFormField(
|
||||
key: 'obfs',
|
||||
label: 'Obfuscation',
|
||||
kind: SingboxFieldKind.secret,
|
||||
),
|
||||
SingboxProxyFormField(
|
||||
key: 'recv_window_conn',
|
||||
label: 'Receive Window Conn',
|
||||
kind: SingboxFieldKind.integer,
|
||||
),
|
||||
SingboxProxyFormField(
|
||||
key: 'recv_window',
|
||||
label: 'Receive Window',
|
||||
kind: SingboxFieldKind.integer,
|
||||
),
|
||||
SingboxProxyFormField(
|
||||
key: 'disable_mtu_discovery',
|
||||
label: 'Disable MTU Discovery',
|
||||
helperText: 'true or false.',
|
||||
kind: SingboxFieldKind.boolean,
|
||||
),
|
||||
..._tlsFields,
|
||||
..._commonAdvancedFields,
|
||||
],
|
||||
),
|
||||
SingboxProxyProfileType.hysteria2: SingboxProxyFormSpec(
|
||||
type: SingboxProxyProfileType.hysteria2,
|
||||
outboundType: 'hysteria2',
|
||||
fields: [
|
||||
_serverField,
|
||||
_serverPortField,
|
||||
_passwordField,
|
||||
SingboxProxyFormField(
|
||||
key: 'up_mbps',
|
||||
label: 'Upload Mbps',
|
||||
kind: SingboxFieldKind.integer,
|
||||
),
|
||||
SingboxProxyFormField(
|
||||
key: 'down_mbps',
|
||||
label: 'Download Mbps',
|
||||
kind: SingboxFieldKind.integer,
|
||||
),
|
||||
SingboxProxyFormField(key: 'obfs.type', label: 'Obfuscation Type'),
|
||||
SingboxProxyFormField(
|
||||
key: 'obfs.password',
|
||||
label: 'Obfuscation Password',
|
||||
kind: SingboxFieldKind.secret,
|
||||
),
|
||||
..._tlsFields,
|
||||
..._commonAdvancedFields,
|
||||
],
|
||||
),
|
||||
SingboxProxyProfileType.tuic: SingboxProxyFormSpec(
|
||||
type: SingboxProxyProfileType.tuic,
|
||||
outboundType: 'tuic',
|
||||
fields: [
|
||||
_serverField,
|
||||
_serverPortField,
|
||||
_uuidField,
|
||||
_passwordField,
|
||||
SingboxProxyFormField(
|
||||
key: 'congestion_control',
|
||||
label: 'Congestion Control',
|
||||
defaultValue: 'cubic',
|
||||
),
|
||||
SingboxProxyFormField(
|
||||
key: 'udp_relay_mode',
|
||||
label: 'UDP Relay Mode',
|
||||
defaultValue: 'native',
|
||||
),
|
||||
SingboxProxyFormField(
|
||||
key: 'zero_rtt_handshake',
|
||||
label: 'Zero RTT Handshake',
|
||||
helperText: 'true or false.',
|
||||
kind: SingboxFieldKind.boolean,
|
||||
),
|
||||
..._tlsFields,
|
||||
..._commonAdvancedFields,
|
||||
],
|
||||
),
|
||||
SingboxProxyProfileType.ssh: SingboxProxyFormSpec(
|
||||
type: SingboxProxyProfileType.ssh,
|
||||
outboundType: 'ssh',
|
||||
fields: [
|
||||
_serverField,
|
||||
_serverPortField,
|
||||
SingboxProxyFormField(key: 'user', label: 'User', required: true),
|
||||
_optionalPasswordField,
|
||||
SingboxProxyFormField(
|
||||
key: 'private_key',
|
||||
label: 'Private Key',
|
||||
kind: SingboxFieldKind.secret,
|
||||
),
|
||||
SingboxProxyFormField(
|
||||
key: 'private_key_passphrase',
|
||||
label: 'Private Key Passphrase',
|
||||
kind: SingboxFieldKind.secret,
|
||||
),
|
||||
..._commonAdvancedFields,
|
||||
],
|
||||
),
|
||||
SingboxProxyProfileType.wireguard: SingboxProxyFormSpec(
|
||||
type: SingboxProxyProfileType.wireguard,
|
||||
outboundType: 'wireguard',
|
||||
fields: [
|
||||
_serverField,
|
||||
_serverPortField,
|
||||
SingboxProxyFormField(
|
||||
key: 'local_address',
|
||||
label: 'Local Address',
|
||||
helperText: 'One CIDR per line, for example 10.0.0.2/32.',
|
||||
required: true,
|
||||
kind: SingboxFieldKind.stringList,
|
||||
),
|
||||
SingboxProxyFormField(
|
||||
key: 'peer_public_key',
|
||||
label: 'Peer Public Key',
|
||||
required: true,
|
||||
),
|
||||
SingboxProxyFormField(
|
||||
key: 'private_key',
|
||||
label: 'Private Key',
|
||||
helperText: 'Stored in secure storage, not profile JSON.',
|
||||
required: true,
|
||||
kind: SingboxFieldKind.secret,
|
||||
),
|
||||
SingboxProxyFormField(
|
||||
key: 'pre_shared_key',
|
||||
label: 'Pre-shared Key',
|
||||
helperText: 'Optional. Stored in secure storage.',
|
||||
kind: SingboxFieldKind.secret,
|
||||
),
|
||||
SingboxProxyFormField(
|
||||
key: 'mtu',
|
||||
label: 'MTU',
|
||||
helperText: 'Optional. sing-box uses 1408 by default.',
|
||||
defaultValue: '1408',
|
||||
kind: SingboxFieldKind.integer,
|
||||
),
|
||||
SingboxProxyFormField(
|
||||
key: 'reserved',
|
||||
label: 'Reserved Bytes',
|
||||
helperText: 'Optional. Three comma-separated numbers, e.g. 0,0,0.',
|
||||
kind: SingboxFieldKind.integerList,
|
||||
exactListLength: 3,
|
||||
minValue: 0,
|
||||
maxValue: 255,
|
||||
),
|
||||
],
|
||||
),
|
||||
SingboxProxyProfileType.shadowTls: SingboxProxyFormSpec(
|
||||
type: SingboxProxyProfileType.shadowTls,
|
||||
outboundType: 'shadowtls',
|
||||
fields: [
|
||||
_serverField,
|
||||
_serverPortField,
|
||||
SingboxProxyFormField(
|
||||
key: 'version',
|
||||
label: 'Version',
|
||||
defaultValue: '3',
|
||||
kind: SingboxFieldKind.integer,
|
||||
),
|
||||
_passwordField,
|
||||
..._tlsFields,
|
||||
..._commonAdvancedFields,
|
||||
],
|
||||
),
|
||||
SingboxProxyProfileType.anyTls: SingboxProxyFormSpec(
|
||||
type: SingboxProxyProfileType.anyTls,
|
||||
outboundType: 'anytls',
|
||||
fields: [_serverField, _serverPortField, _passwordField, ..._dialFields],
|
||||
),
|
||||
};
|
||||
Reference in New Issue
Block a user