From 64690dd616df89097e735aed74e19428eaa4a4d0 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Mon, 29 Jun 2026 06:17:57 +0200 Subject: [PATCH] fix socks version issue --- .../proxy/data/forms/singbox_form_field.dart | 12 +++++ .../proxy/data/forms/singbox_form_spec.dart | 6 +++ .../proxy/data/forms/singbox_form_specs.dart | 4 +- .../structured_profile_form.dart | 47 +++++++++++++++++++ .../data/models/singbox_proxy_form_test.dart | 28 +++++++++++ 5 files changed, 96 insertions(+), 1 deletion(-) diff --git a/apps/weblibre/lib/features/proxy/data/forms/singbox_form_field.dart b/apps/weblibre/lib/features/proxy/data/forms/singbox_form_field.dart index 3433fa29..05c3aa8f 100644 --- a/apps/weblibre/lib/features/proxy/data/forms/singbox_form_field.dart +++ b/apps/weblibre/lib/features/proxy/data/forms/singbox_form_field.dart @@ -26,6 +26,12 @@ enum SingboxFieldKind { port, boolean, stringList, + + /// A value constrained to [SingboxProxyFormField.allowedValues] and always + /// serialized as a JSON string. Use for sing-box string enums such as the + /// SOCKS `version` field, whose accepted values ("4", "4a", "5") are + /// string-typed even when numeric-looking. + choice, } class SingboxProxyFormField { @@ -39,6 +45,10 @@ class SingboxProxyFormField { final int? minValue; final int? maxValue; + /// Permitted values for a [SingboxFieldKind.choice] field, rendered as a + /// dropdown and enforced during validation. + final List? allowedValues; + const SingboxProxyFormField({ required this.key, required this.label, @@ -49,6 +59,7 @@ class SingboxProxyFormField { this.exactListLength, this.minValue, this.maxValue, + this.allowedValues, }); bool get isSecret => kind == SingboxFieldKind.secret; @@ -58,6 +69,7 @@ class SingboxProxyFormField { bool get isPort => kind == SingboxFieldKind.port; bool get isBoolean => kind == SingboxFieldKind.boolean; bool get isStringList => kind == SingboxFieldKind.stringList; + bool get isChoice => kind == SingboxFieldKind.choice; } /// Shared parsing of boolean form values. Returns null when the text doesn't diff --git a/apps/weblibre/lib/features/proxy/data/forms/singbox_form_spec.dart b/apps/weblibre/lib/features/proxy/data/forms/singbox_form_spec.dart index 132042ec..ed1aabab 100644 --- a/apps/weblibre/lib/features/proxy/data/forms/singbox_form_spec.dart +++ b/apps/weblibre/lib/features/proxy/data/forms/singbox_form_spec.dart @@ -72,6 +72,12 @@ class SingboxProxyFormSpec { if (field.isBoolean && value.isNotEmpty && parseFormBool(value) == null) { return '${field.label} must be true or false.'; } + if (field.isChoice && value.isNotEmpty) { + final allowedValues = field.allowedValues; + if (allowedValues != null && !allowedValues.contains(value)) { + return '${field.label} must be one of: ${allowedValues.join(', ')}.'; + } + } } return null; diff --git a/apps/weblibre/lib/features/proxy/data/forms/singbox_form_specs.dart b/apps/weblibre/lib/features/proxy/data/forms/singbox_form_specs.dart index bb11cfc2..48ff7304 100644 --- a/apps/weblibre/lib/features/proxy/data/forms/singbox_form_specs.dart +++ b/apps/weblibre/lib/features/proxy/data/forms/singbox_form_specs.dart @@ -139,7 +139,9 @@ const singboxProxyFormSpecs = { key: 'version', label: 'SOCKS Version', defaultValue: '5', - kind: SingboxFieldKind.integer, + // sing-box expects this as a JSON string enum, not a number. + kind: SingboxFieldKind.choice, + allowedValues: ['5', '4a', '4'], ), _usernameField, _optionalPasswordField, diff --git a/apps/weblibre/lib/features/proxy/presentation/widgets/profile_editor/structured_profile_form.dart b/apps/weblibre/lib/features/proxy/presentation/widgets/profile_editor/structured_profile_form.dart index e3048ea9..a60c07aa 100644 --- a/apps/weblibre/lib/features/proxy/presentation/widgets/profile_editor/structured_profile_form.dart +++ b/apps/weblibre/lib/features/proxy/presentation/widgets/profile_editor/structured_profile_form.dart @@ -117,6 +117,12 @@ class _SectionFields extends StatelessWidget { controller: controllers[field.key]!, onChanged: (value) => onChanged(field.key, value), ) + else if (field.isChoice) + _ChoiceField( + field: field, + controller: controllers[field.key]!, + onChanged: (value) => onChanged(field.key, value), + ) else if (field.isSecret) ObscurableTextField( controller: controllers[field.key], @@ -295,3 +301,44 @@ class _BooleanField extends HookWidget { ); } } + +class _ChoiceField extends HookWidget { + final SingboxProxyFormField field; + final TextEditingController controller; + final ValueChanged onChanged; + + const _ChoiceField({ + required this.field, + required this.controller, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + final allowedValues = field.allowedValues ?? const []; + final value = useListenableSelector( + controller, + () => allowedValues.contains(controller.text) ? controller.text : null, + ); + + return DropdownButtonFormField( + initialValue: value, + decoration: InputDecoration( + labelText: field.required ? '${field.label} *' : field.label, + helperText: field.helperText, + helperMaxLines: 3, + border: const OutlineInputBorder(), + ), + items: [ + for (final option in allowedValues) + DropdownMenuItem(value: option, child: Text(option)), + ], + onChanged: (selected) { + if (selected != null) { + controller.text = selected; + onChanged(selected); + } + }, + ); + } +} diff --git a/apps/weblibre/test/features/proxy/data/models/singbox_proxy_form_test.dart b/apps/weblibre/test/features/proxy/data/models/singbox_proxy_form_test.dart index 62863cd8..e648a564 100644 --- a/apps/weblibre/test/features/proxy/data/models/singbox_proxy_form_test.dart +++ b/apps/weblibre/test/features/proxy/data/models/singbox_proxy_form_test.dart @@ -30,6 +30,34 @@ void main() { expect(secrets, {'password': 'secret'}); }); + test('serializes SOCKS version as a JSON string', () { + final spec = singboxProxyFormSpecs[SingboxProxyProfileType.socks]!; + final values = { + 'server': '127.0.0.1', + 'server_port': '9050', + 'version': '5', + }; + + expect(spec.validate(values), isNull); + + final config = jsonDecode(spec.toConfigJson(values)); + + // sing-box rejects a numeric version; it must be the string "5". + expect(config['version'], '5'); + expect(config['version'], isA()); + }); + + test('rejects an out-of-range SOCKS version', () { + final spec = singboxProxyFormSpecs[SingboxProxyProfileType.socks]!; + final values = { + 'server': '127.0.0.1', + 'server_port': '9050', + 'version': '6', + }; + + expect(spec.validate(values), isNotNull); + }); + test('hydrates structured values from public and secret JSON', () { final spec = singboxProxyFormSpecs[SingboxProxyProfileType.vless]!;