fix(core,helper): only let the owner drive the elevated Core

Changelog: The elevated Core only answers its owner
This commit is contained in:
chen08209
2026-10-05 08:24:15 +08:00
parent 4b59eca853
commit d780cb863d
40 changed files with 1561 additions and 153 deletions
+76 -8
View File
@@ -79,7 +79,26 @@ for `settimeofday`.
`_makeRealProfileTask` in `lib/common/task.dart` writes `mixed-port`, `allow-lan`, and `external-controller` into every
generated profile. With `allow-lan` off, mihomo's `genAddr` binds each listener to `127.0.0.1`; the external controller is
`ExternalControllerStatus.close` by default and never binds anywhere but loopback.
`ExternalControllerStatus.close` by default and never binds anywhere but loopback. The task also clears
`external-controller-tls`, `-unix` and `-pipe` in every mode: the unix and pipe controllers answer without a secret, and
the unix one unlinks whatever file sits at its path before opening it to everyone, so a profile may not choose them.
Outside safe mode, profiles retain `listeners`, `tunnels`, `ss-config`, `vmess-config`, `tuic-server`, `iptables`,
`dns.listen` and `ntp.write-to-system`. Extra inbounds can bypass the app's LAN and authentication settings, and a TUN
listener can change routing independently of the app's TUN toggle. These network and clock effects are an accepted
boundary; they are not treated as arbitrary filesystem modification. The task disables `iptables` when its
`inbound-interface` contains a space, because mihomo interpolates it into commands and splits their arguments on spaces.
Safe mode clears these extra inbounds and iptables, closes DNS listening and disables NTP system time writes even after
local overrides. The editor omits disabled alternate controllers and keeps listener completion.
An open external controller always has a secret, because mihomo serves a controller with an empty one to every local
user and, through CORS, to any page the user visits. `PatchClashConfig.secret` is that secret and it belongs to FlClash,
as the address does: the task writes it over whatever the profile carries, and `UpdateParams.secret` hands the same value
to a running Core, which restarts the controller when it changes. `ensureControllerSecret` keeps the pair consistent
(an open controller gets a generated secret, a closed one drops it, so every opening rotates it) and runs where a
config enters the app: the settings toggle, startup, and `writeConfig` for a restored backup. `_effectivePatchConfig`
reports a controller without a secret as closed, and `_makeRealProfileTask` independently closes it without a secret
or in safe mode, so a writer that skips `ensureControllerSecret` opens nothing. The settings page shows the secret in
an editable row, which refuses an empty value.
The loopback listener takes local connections without authentication by default: mandatory credentials would lock out
every external local consumer a proxy client exists for. The binding only guarantees traffic originates on the device;
@@ -92,6 +111,32 @@ withholds the Android VPN system proxy declaration because `ProxyInfo` cannot ca
traffic). Desktop system proxy is deliberately not gated — dropping it would leak traffic direct, so the setting's
description tells users to supply credentials manually instead.
### Local Trust Boundary
The Windows Helper answers only a connection whose client process runs under the owner SID it was installed for;
the Linux Helper only the owner uid. A setuid Core is executable by `root:admin` (macOS) or the installing user's primary
group, so its boundary also includes every member of that group, not just the installing account. Profiles cannot choose
a controller address, a secret or alternate controllers. Another account outside those permissions, a subscription,
or a remote party reaching the privileged Core's control or filesystem APIs is a vulnerability.
A process running as the same account is not stopped. It can ask the Helper to start the Core
against a socket or pipe it owns, or run a setuid Core against a port it listens on, and so become the host that drives
a privileged Core: `initClash` chooses the home directory every path check anchors on, and `getConfig` reads any path.
The Core connects to whatever address it is given and cannot tell that host from FlClash. An account check cannot
distinguish it from a same-account process that can already read FlClash's memory and files. This remains a user-to-root
or user-to-SYSTEM privilege escalation, not a complete security fix. An open external
controller is inside the same boundary: its secret is stored under the account, and `/upgrade` stays mounted (it fetches
fixed MetaCubeX release URLs, not caller-chosen code). mihomo's `IsSafePath` compares paths lexically, so a symlink inside
the home directory escapes it; that fix belongs upstream, not in the submodule.
Closing the same-account case requires limiting what a hijacked Core can do. Keeping the main Core unprivileged and
moving host changes into narrow platform helpers requires architectural work, but need not introduce repeated prompts
or remove ordinary proxy functionality. Features that require additional privileges, such as identifying other users'
processes, need platform-specific handling. Descriptor-based filesystem confinement can preserve ordinary managed
paths; intentional external symlink targets need explicit allowed roots. These changes have not been implemented.
Account authentication, a private socket directory or a symlink check at configuration time does not remove the
privileged filesystem operations available to the host. Changes must not widen access to those operations.
### Interface Name Modes
The same task also writes `interface-name` per `PatchClashConfig.interfaceNameMode`: `clear` (default) forces it empty so a
@@ -667,6 +712,8 @@ rewritten; each owner reads the flag where it would otherwise act:
user's values and the installed app keeps `127.0.0.1:9090`. `SetupAction.requestAdmin` skips authorization.
- System clock: `_makeRealProfileTask` sets `write-to-system: false` on whatever `ntp` section the generated profile
ends up with, so neither the user's override nor the profile's own NTP config can move the host's time.
- Profile listeners and firewall rules: `_makeRealProfileTask` clears `listeners`, `tunnels`, `ss-config`,
`vmess-config`, `tuic-server` and `iptables`, and closes `dns.listen`, preventing profile-only changes during apply.
- System proxy: the `proxy` plugin handle in `lib/common/proxy.dart` is `null`, so `ProxyManager` and exit cleanup
never write the OS setting, not even the `stopProxy` that would clear what the installed app configured.
- System DNS: `systemDnsCoordinator` is `null` and `shouldPatchSystemDnsProvider` is `false`, so neither the apply nor
@@ -1020,8 +1067,10 @@ after calculating the SHA256 of the Core produced for the active Flutter configu
The helper owns its Windows Service Control Manager lifecycle through two elevated commands:
- `FlClashHelperService.exe install` stops and removes any stale registration, creates the auto-start service for the
current executable path, starts it, and waits for the running state.
- `FlClashHelperService.exe install --owner-pid <pid>` reads the account of that process (the app, which Dart names with
its own PID because the elevated installer may be another administrator), stops and removes any stale registration,
creates the auto-start service for the current executable path with `--owner-sid <SID>` as its launch argument,
starts it, and waits for the running state. A service started without an owner exits instead of serving anyone.
- `FlClashHelperService.exe uninstall` stops the service, waits for shutdown, removes its registration, and is also used
by the Windows package uninstaller.
@@ -1060,22 +1109,41 @@ is registered at package install, and `Linux.registerService` asks for elevation
- A Linux host without systemd (`/run/systemd/system` absent) has no Helper: `system.hasHelperService` is false there,
readiness is the `stat` check, and `pkexec` sets the setuid bit on the bundled Core as before.
A setuid Core is a root process for whoever gives it an address to dial (`argv[1]`), so its mode never lets others
execute it: macOS runs `chown root:admin` and `chmod 4750` (and refuses an account outside the `admin` group, which
could not run the result), and a Linux host without systemd hands it to the requesting user's primary group under
`pkexec`. `System.isPrivilegedStatOutput` reads a Core that others can execute as unauthorized, so one that an earlier
version marked `+sx` is re-authorized on the next TUN enable.
In every Flutter build mode `/start` opens the fixed Core executable beside the Helper without write/delete sharing,
validates it against the SHA256 embedded only in the Helper, and keeps that handle open through process creation.
`/ping` only compares the requested `coreSha256` with the Helper's embedded value and checks the fixed Core path exists;
it never hashes the Core. Protocol version 6 uses 32-character lowercase-hex session ownership:
it never hashes the Core. Protocol version 6 (8 on Windows, where the owner check and the browser-origin check arrived
and an older Helper must be reinstalled) uses 32-character lowercase-hex session ownership:
- `GET /ping?coreSha256=...` returns the current Helper executable path with `x-flclash-helper-protocol` when the
requested SHA matches.
- `POST /start` rejects unknown JSON fields, validates `{address, sessionId}`, then releases any previously managed Core
before verifying the Core — so every outcome, including a rejected one, leaves the Helper owning no Core — and returns
- `POST /start` rejects unknown JSON fields and validates `{address, sessionId}` and, on Linux, the socket owner, which
leaves a running Core alone. It then releases any previously managed Core before verifying the Core — so every
outcome of a request that passed validation, including a rejected one, leaves the Helper owning no Core — and returns
`{sessionId, pid}`.
- `POST /stop` validates `{sessionId}` and only stops the matching managed Core. A session mismatch is HTTP 409.
- `GET /logs` exposes the bounded recent Helper/Core stderr buffer with `no-store` caching.
`/start` and `/stop` take at most 4096 bytes and need a `Content-Length` (HTTP 413 and 411 otherwise); `HelperClient`
always sends one. All endpoints reject `Origin` headers and non-local `Host` values, preventing browser requests and DNS
rebinding through an owner-account browser, which would otherwise pass the Windows account check. The accepted
authorities are `127.0.0.1`, `localhost` and the Linux Unix-socket placeholder `FlClashHelperService`. The Helper grants
no CORS access.
Endpoints bind only to `127.0.0.1:47890` on Windows and to `/run/flclash/helper.sock` on Linux, and do not use
request-token authentication. Lifecycle safety comes from the fixed executable/hash, the strict address namespace
(`\\.\pipe\FlClashCore_<32 hex>` on Windows, `/tmp/FlClashSocket_<digits>.sock` on Linux), the session-scoped stop
request-token authentication. Who may call them is decided per connection, before any request is read: the Linux
socket drops a peer whose `SO_PEERCRED` UID is not the owner's, and the Windows listener (`service/peer.rs`) traces the
connection through the TCP table to the process holding its client end and drops it unless that process runs as the
owner SID the service was registered with. The Windows Core runs as SYSTEM and dials whatever pipe `/start` names, so
without that check any local account would host a SYSTEM Core. What no per-connection check closes is a process that
runs as the owner itself, on either platform. Lifecycle safety also comes from the fixed executable/hash, the strict
address namespace (`\\.\pipe\FlClashCore_<32 hex>` on Windows, `/tmp/FlClashSocket_<digits>.sock` on Linux), the session-scoped stop
contract, Dart-side peer-PID verification on Windows and, on Unix, the Core socket that `plugins/rust_api` sets to
mode `0600` so only the owning user (and the root-effective Core) can connect. When the Helper service itself shuts
down, it unconditionally stops the Core process it owns; under systemd the unit's control group does the same.
+1
View File
@@ -150,6 +150,7 @@
"allowBypass": "Allow apps to bypass VPN",
"externalController": "External controller",
"externalControllerDesc": "When enabled, the Clash core can be controlled on port 9090",
"externalControllerSecret": "External controller secret",
"ipv6Desc": "When enabled, IPv6 traffic can be received",
"app": "App",
"general": "General",
+1
View File
@@ -150,6 +150,7 @@
"allowBypass": "アプリによるVPNバイパスを許可",
"externalController": "外部コントローラー",
"externalControllerDesc": "有効にすると、ポート9090でClashコアを制御できます",
"externalControllerSecret": "外部コントローラーのシークレット",
"ipv6Desc": "有効にすると、IPv6トラフィックを受信できます",
"app": "アプリ",
"general": "一般",
+1
View File
@@ -150,6 +150,7 @@
"allowBypass": "Разрешить приложениям обходить VPN",
"externalController": "Внешний контроллер",
"externalControllerDesc": "При включении ядром Clash можно управлять через порт 9090",
"externalControllerSecret": "Секрет внешнего контроллера",
"ipv6Desc": "При включении можно принимать трафик IPv6",
"app": "Приложение",
"general": "Общие",
+1
View File
@@ -150,6 +150,7 @@
"allowBypass": "允许应用绕过VPN",
"externalController": "外部控制器",
"externalControllerDesc": "开启后将可以通过9090端口控制Clash内核",
"externalControllerSecret": "外部控制器密钥",
"ipv6Desc": "开启后将可以接收IPv6流量",
"app": "应用",
"general": "常规",
+17 -5
View File
@@ -308,11 +308,7 @@ func updateConfig(params *UpdateParams) error {
if params.Tun != nil {
patchTun(&general.Tun, params.Tun)
}
if params.ExternalController != nil &&
*params.ExternalController != currentConfig.Controller.ExternalController {
currentConfig.Controller.ExternalController = *params.ExternalController
route.ReCreateServer(routeConfig(currentConfig))
}
patchController(params)
if params.Authentication != nil {
applyAuthentication(currentConfig, *params.Authentication)
}
@@ -328,6 +324,22 @@ func updateConfig(params *UpdateParams) error {
return nil
}
func patchController(params *UpdateParams) {
controller := currentConfig.Controller
address, secret := controller.ExternalController, controller.Secret
if params.ExternalController != nil {
address = *params.ExternalController
}
if params.Secret != nil {
secret = *params.Secret
}
if address == controller.ExternalController && secret == controller.Secret {
return
}
controller.ExternalController, controller.Secret = address, secret
route.ReCreateServer(routeConfig(currentConfig))
}
func applyAuthentication(cfg *config.Config, authentication []string) {
users := make([]auth.AuthUser, 0, len(authentication))
for _, line := range authentication {
+59
View File
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"net"
"net/http"
"net/netip"
"os"
"path/filepath"
@@ -16,6 +17,7 @@ import (
"github.com/metacubex/mihomo/component/updater"
"github.com/metacubex/mihomo/config"
"github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/hub/route"
authStore "github.com/metacubex/mihomo/listener/auth"
"github.com/metacubex/mihomo/log"
"github.com/metacubex/mihomo/tunnel"
@@ -242,6 +244,63 @@ func TestUpdateConfigLeavesTheControllerAloneWhenTheAddressIsUnchanged(t *testin
}
}
func TestUpdateConfigReplacesTheSecretTheProfileCarried(t *testing.T) {
withCurrentConfig(t, &config.Config{
General: &config.General{},
Controller: &config.Controller{Secret: "from-the-profile"},
})
closed, secret := "", "generated"
if err := updateConfig(&UpdateParams{ExternalController: &closed, Secret: &secret}); err != nil {
t.Fatalf("updateConfig error: %v", err)
}
if currentConfig.Controller.Secret != secret {
t.Errorf("Secret = %q, want %q", currentConfig.Controller.Secret, secret)
}
}
func TestUpdateConfigServesTheControllerBehindTheSecretItWasGiven(t *testing.T) {
probe, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("reserve a port: %v", err)
}
address := probe.Addr().String()
_ = probe.Close()
withCurrentConfig(t, &config.Config{General: &config.General{}, Controller: &config.Controller{}})
t.Cleanup(func() { route.ReCreateServer(&route.Config{}) })
secret := "generated"
if err := updateConfig(&UpdateParams{ExternalController: &address, Secret: &secret}); err != nil {
t.Fatalf("updateConfig error: %v", err)
}
status := func(authorization string) int {
request, _ := http.NewRequest(http.MethodGet, "http://"+address+"/version", nil)
if authorization != "" {
request.Header.Set("Authorization", authorization)
}
deadline := time.Now().Add(3 * time.Second)
for {
response, err := http.DefaultClient.Do(request)
if err == nil {
_ = response.Body.Close()
return response.StatusCode
}
if time.Now().After(deadline) {
t.Fatalf("the controller never came up: %v", err)
}
time.Sleep(50 * time.Millisecond)
}
}
if got := status(""); got != http.StatusUnauthorized {
t.Errorf("without the secret: status %d, want %d", got, http.StatusUnauthorized)
}
if got := status("Bearer " + secret); got != http.StatusOK {
t.Errorf("with the secret: status %d, want %d", got, http.StatusOK)
}
}
// geoUpdaterCalls counts the reconciliation the code under test asked for. The
// real calls spawn a goroutine that downloads the GEO databases, which is not
// something a unit test should reach for.
+1
View File
@@ -32,6 +32,7 @@ type UpdateParams struct {
IPv6 *bool `json:"ipv6"`
TCPConcurrent *bool `json:"tcp-concurrent"`
ExternalController *string `json:"external-controller"`
Secret *string `json:"secret"`
UnifiedDelay *bool `json:"unified-delay"`
Authentication *[]string `json:"authentication"`
GeoAutoUpdate *bool `json:"geo-auto-update"`
+5
View File
@@ -100,6 +100,11 @@ class Bootstrap {
config = config.copyWith(currentProfileId: null);
await preferences.saveConfig(config);
}
final patchClashConfig = config.patchClashConfig.ensureControllerSecret();
if (patchClashConfig != config.patchClashConfig) {
config = config.copyWith(patchClashConfig: patchClashConfig);
await preferences.saveConfig(config);
}
final appState = AppState(
brightness: WidgetsBinding.instance.platformDispatcher.platformBrightness,
version: version,
+2 -1
View File
@@ -1,5 +1,6 @@
// ignore_for_file: constant_identifier_names
import 'dart:io';
import 'dart:math';
import 'package:collection/collection.dart';
@@ -20,7 +21,7 @@ final windowsPipeName = '\\\\.\\pipe\\FlClashCore_${_randomPipeId()}';
const helperPort = 47890;
const helperSocketPath = '/run/flclash/helper.sock';
const helperProtocolVersionHeader = 'x-flclash-helper-protocol';
const helperProtocolVersion = '6';
final helperProtocolVersion = Platform.isWindows ? '8' : '6';
const maxTextScale = 1.4;
const minTextScale = 0.8;
final baseInfoEdgeInsets = EdgeInsets.symmetric(
+52 -6
View File
@@ -80,13 +80,48 @@ class System {
: ['-c', '%U:%G %A', corePath];
}
/// Execute for others fails it, so an older `+sx` Core is re-authorized.
@visibleForTesting
static bool isPrivilegedStatOutput(
String output, {
required String ownerPrefix,
}) {
final trimmed = output.trim();
return trimmed.startsWith(ownerPrefix) && trimmed.contains('rws');
if (!trimmed.startsWith(ownerPrefix)) {
return false;
}
final mode = trimmed.split(RegExp(r'\s+')).last;
return mode.length >= 10 &&
mode[3] == 's' &&
mode[9] != 'x' &&
mode[9] != 't';
}
@visibleForTesting
static String macOSElevationShell(String corePath) {
final path = _shellEscape(corePath);
return 'chown root:admin $path && chmod 4750 $path';
}
@visibleForTesting
static String linuxElevationShell(String corePath) {
final path = _shellEscape(corePath);
return 'group=\$(id -g "\$PKEXEC_UID") && chown "root:\$group" $path && '
'chmod 4750 $path';
}
Future<bool> _isInAdminGroup() async {
try {
final result = await runProcess('id', ['-Gn']);
return result.exitCode == 0 &&
result.stdout
.toString()
.trim()
.split(RegExp(r'\s+'))
.contains('admin');
} on ProcessException {
return false;
}
}
/// A read-only nosuid mount at a path that changes every run: no elevation sticks.
@@ -194,8 +229,15 @@ class System {
}
if (system.isMacOS) {
final escapedPath = _shellEscape(appPath.corePath);
final shell = 'chown root:admin $escapedPath && chmod +sx $escapedPath';
if (!await _isInAdminGroup()) {
commonPrint.log(
'TUN needs an administrator account: only the admin group may run '
'the elevated Core',
logLevel: LogLevel.error,
);
return AuthorizeCode.error;
}
final shell = macOSElevationShell(appPath.corePath);
final arguments = [
'-e',
'do shell script "$shell" with administrator privileges',
@@ -206,13 +248,12 @@ class System {
}
return AuthorizeCode.success;
} else if (system.isLinux) {
final escapedCorePath = _shellEscape(appPath.corePath);
final ProcessResult result;
try {
result = await runProcess('pkexec', [
'/bin/sh',
'-c',
'chown root:root $escapedCorePath && chmod +sx $escapedCorePath',
linuxElevationShell(appPath.corePath),
]);
} on ProcessException catch (error) {
commonPrint.log(
@@ -308,9 +349,14 @@ class Windows {
return true;
}
/// The installer may be another administrator, so this process names the owner.
@visibleForTesting
static String installArguments(int ownerPid) =>
'install --owner-pid $ownerPid';
Future<AuthorizeCode> registerService() {
return registerHelperService(
() async => runas(appPath.helperPath, 'install'),
() async => runas(appPath.helperPath, installArguments(pid)),
);
}
}
+23 -4
View File
@@ -164,7 +164,23 @@ Future<({String yaml, String md5})> _makeRealProfileTask(
}
}
rawConfig['external-controller'] = realPatchConfig.externalController.value;
rawConfig['external-controller'] =
data.safeMode || realPatchConfig.secret.isEmpty
? ''
: realPatchConfig.externalController.value;
rawConfig['secret'] = realPatchConfig.secret;
// mihomo serves the unix and pipe controllers without a secret, and the unix
// one unlinks whatever sits at its path before opening it to everyone.
rawConfig['external-controller-tls'] = '';
rawConfig['external-controller-unix'] = '';
rawConfig['external-controller-pipe'] = '';
final iptables = rawConfig['iptables'];
if (iptables is Map) {
final inboundInterface = iptables['inbound-interface'];
if (inboundInterface is String && inboundInterface.contains(' ')) {
rawConfig['iptables'] = {'enable': false};
}
}
rawConfig['external-ui'] = '';
switch (realPatchConfig.interfaceNameMode) {
case InterfaceNameMode.clear:
@@ -280,10 +296,13 @@ Future<({String yaml, String md5})> _makeRealProfileTask(
}
}
if (data.safeMode) {
rawConfig['listeners'] = [];
rawConfig['tunnels'] = [];
rawConfig['ss-config'] = '';
rawConfig['vmess-config'] = '';
rawConfig['tuic-server'] = {'enable': false};
rawConfig['iptables'] = {'enable': false};
rawConfig['dns']['listen'] = '';
rawConfig['external-controller-tls'] = '';
rawConfig['external-controller-unix'] = '';
rawConfig['external-controller-pipe'] = '';
final rawNtp = rawConfig['ntp'];
if (rawNtp is Map) {
rawConfig['ntp'] = {
-3
View File
@@ -842,9 +842,6 @@ const clashConfigSchema = YamlSchema.map({
'routing-mark': _str,
'etag-support': _bool,
'external-controller': YamlSchema.scalar(['127.0.0.1:9090']),
'external-controller-tls': _str,
'external-controller-unix': _str,
'external-controller-pipe': _str,
'external-controller-cors': YamlSchema.map({
'allow-origins': _strList,
'allow-private-network': _bool,
+3
View File
@@ -608,6 +608,9 @@ class MessageLookup extends MessageLookupByLibrary {
"externalControllerDesc": MessageLookupByLibrary.simpleMessage(
"When enabled, the Clash core can be controlled on port 9090",
),
"externalControllerSecret": MessageLookupByLibrary.simpleMessage(
"External controller secret",
),
"externalLink": MessageLookupByLibrary.simpleMessage("External link"),
"extraLarge": MessageLookupByLibrary.simpleMessage("Extra large"),
"fade": MessageLookupByLibrary.simpleMessage("Fade"),
+3
View File
@@ -511,6 +511,9 @@ class MessageLookup extends MessageLookupByLibrary {
"externalControllerDesc": MessageLookupByLibrary.simpleMessage(
"有効にすると、ポート9090でClashコアを制御できます",
),
"externalControllerSecret": MessageLookupByLibrary.simpleMessage(
"外部コントローラーのシークレット",
),
"externalLink": MessageLookupByLibrary.simpleMessage("外部リンク"),
"extraLarge": MessageLookupByLibrary.simpleMessage("特大"),
"fade": MessageLookupByLibrary.simpleMessage("フェード"),
+3
View File
@@ -633,6 +633,9 @@ class MessageLookup extends MessageLookupByLibrary {
"externalControllerDesc": MessageLookupByLibrary.simpleMessage(
"При включении ядром Clash можно управлять через порт 9090",
),
"externalControllerSecret": MessageLookupByLibrary.simpleMessage(
"Секрет внешнего контроллера",
),
"externalLink": MessageLookupByLibrary.simpleMessage("Внешняя ссылка"),
"extraLarge": MessageLookupByLibrary.simpleMessage("Очень крупный"),
"fade": MessageLookupByLibrary.simpleMessage("Растворение"),
+1
View File
@@ -462,6 +462,7 @@ class MessageLookup extends MessageLookupByLibrary {
"externalControllerDesc": MessageLookupByLibrary.simpleMessage(
"开启后将可以通过9090端口控制Clash内核",
),
"externalControllerSecret": MessageLookupByLibrary.simpleMessage("外部控制器密钥"),
"externalLink": MessageLookupByLibrary.simpleMessage("外部链接"),
"extraLarge": MessageLookupByLibrary.simpleMessage("超大"),
"fade": MessageLookupByLibrary.simpleMessage("淡入"),
+10
View File
@@ -1195,6 +1195,16 @@ class AppLocalizations {
);
}
/// `External controller secret`
String get externalControllerSecret {
return Intl.message(
'External controller secret',
name: 'externalControllerSecret',
desc: '',
args: [],
);
}
/// `When enabled, IPv6 traffic can be received`
String get ipv6Desc {
return Intl.message(
+9
View File
@@ -1110,6 +1110,7 @@ abstract class PatchClashConfig with _$PatchClashConfig {
@Default(ExternalControllerStatus.close)
@JsonKey(name: 'external-controller')
ExternalControllerStatus externalController,
@Default('') String secret,
@Default({}) Map<String, String> hosts,
@Default(false) @JsonKey(name: 'geo-auto-update') bool geoAutoUpdate,
@Default(24) @JsonKey(name: 'geo-update-interval') int geoUpdateInterval,
@@ -1145,6 +1146,7 @@ extension PatchClashConfigExt on PatchClashConfig {
ipv6: ipv6,
tcpConcurrent: tcpConcurrent,
externalController: externalController,
secret: secret,
unifiedDelay: unifiedDelay,
mixedPort: mixedPort,
geoAutoUpdate: geoAutoUpdate,
@@ -1152,4 +1154,11 @@ extension PatchClashConfigExt on PatchClashConfig {
geoXUrl: geoXUrl.raw,
);
}
PatchClashConfig ensureControllerSecret() {
if (externalController == ExternalControllerStatus.close) {
return secret.isEmpty ? this : copyWith(secret: '');
}
return secret.isEmpty ? copyWith(secret: generateRandomSecret(32)) : this;
}
}
+1
View File
@@ -30,6 +30,7 @@ abstract class UpdateParams with _$UpdateParams {
@JsonKey(name: 'tcp-concurrent') required bool tcpConcurrent,
@JsonKey(name: 'external-controller')
required ExternalControllerStatus externalController,
@Default('') String secret,
@JsonKey(name: 'unified-delay') required bool unifiedDelay,
@Default([]) List<String> authentication,
@Default(false) @JsonKey(name: 'geo-auto-update') bool geoAutoUpdate,
+23 -20
View File
@@ -6272,7 +6272,7 @@ as Map<String, String>,
/// @nodoc
mixin _$PatchClashConfig {
@JsonKey(name: 'mixed-port') int get mixedPort;@JsonKey(name: 'socks-port') int get socksPort;@JsonKey(name: 'port') int get port;@JsonKey(name: 'redir-port') int get redirPort;@JsonKey(name: 'tproxy-port') int get tproxyPort; Mode get mode;@JsonKey(name: 'allow-lan') bool get allowLan;@JsonKey(name: 'log-level') LogLevel get logLevel; bool get ipv6;@JsonKey(name: 'find-process-mode', unknownEnumValue: FindProcessMode.off) FindProcessMode get findProcessMode;@JsonKey(name: 'interface-name-mode', unknownEnumValue: InterfaceNameMode.clear) InterfaceNameMode get interfaceNameMode;@JsonKey(name: 'interface-name') String get interfaceName;@JsonKey(name: 'keep-alive-interval') int get keepAliveInterval;@JsonKey(name: 'unified-delay') bool get unifiedDelay;@JsonKey(name: 'tcp-concurrent') bool get tcpConcurrent;@JsonKey(fromJson: Tun.safeFormJson) Tun get tun;@JsonKey(fromJson: Dns.safeDnsFromJson) Dns get dns;@JsonKey(name: _dnsOverrideKeysJsonKey, fromJson: _dnsOverrideKeysFromJson) Set<DnsOverrideKey> get dnsOverrideKeys;@JsonKey(fromJson: Ntp.safeNtpFromJson) Ntp get ntp;@JsonKey(name: _ntpOverrideKeysJsonKey, fromJson: _ntpOverrideKeysFromJson) Set<NtpOverrideKey> get ntpOverrideKeys;@JsonKey(name: 'geox-url', fromJson: _geoXUrlFromJson, toJson: _geoXUrlToJson) Map<GeoResource, String> get geoXUrl;@JsonKey(name: 'geodata-loader') GeodataLoader get geodataLoader;@JsonKey(name: 'global-ua') String? get globalUa;@JsonKey(name: 'external-controller') ExternalControllerStatus get externalController; Map<String, String> get hosts;@JsonKey(name: 'geo-auto-update') bool get geoAutoUpdate;@JsonKey(name: 'geo-update-interval') int get geoUpdateInterval;
@JsonKey(name: 'mixed-port') int get mixedPort;@JsonKey(name: 'socks-port') int get socksPort;@JsonKey(name: 'port') int get port;@JsonKey(name: 'redir-port') int get redirPort;@JsonKey(name: 'tproxy-port') int get tproxyPort; Mode get mode;@JsonKey(name: 'allow-lan') bool get allowLan;@JsonKey(name: 'log-level') LogLevel get logLevel; bool get ipv6;@JsonKey(name: 'find-process-mode', unknownEnumValue: FindProcessMode.off) FindProcessMode get findProcessMode;@JsonKey(name: 'interface-name-mode', unknownEnumValue: InterfaceNameMode.clear) InterfaceNameMode get interfaceNameMode;@JsonKey(name: 'interface-name') String get interfaceName;@JsonKey(name: 'keep-alive-interval') int get keepAliveInterval;@JsonKey(name: 'unified-delay') bool get unifiedDelay;@JsonKey(name: 'tcp-concurrent') bool get tcpConcurrent;@JsonKey(fromJson: Tun.safeFormJson) Tun get tun;@JsonKey(fromJson: Dns.safeDnsFromJson) Dns get dns;@JsonKey(name: _dnsOverrideKeysJsonKey, fromJson: _dnsOverrideKeysFromJson) Set<DnsOverrideKey> get dnsOverrideKeys;@JsonKey(fromJson: Ntp.safeNtpFromJson) Ntp get ntp;@JsonKey(name: _ntpOverrideKeysJsonKey, fromJson: _ntpOverrideKeysFromJson) Set<NtpOverrideKey> get ntpOverrideKeys;@JsonKey(name: 'geox-url', fromJson: _geoXUrlFromJson, toJson: _geoXUrlToJson) Map<GeoResource, String> get geoXUrl;@JsonKey(name: 'geodata-loader') GeodataLoader get geodataLoader;@JsonKey(name: 'global-ua') String? get globalUa;@JsonKey(name: 'external-controller') ExternalControllerStatus get externalController; String get secret; Map<String, String> get hosts;@JsonKey(name: 'geo-auto-update') bool get geoAutoUpdate;@JsonKey(name: 'geo-update-interval') int get geoUpdateInterval;
/// Create a copy of PatchClashConfig
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -6286,20 +6286,20 @@ $PatchClashConfigCopyWith<PatchClashConfig> get copyWith => _$PatchClashConfigCo
@override
bool operator ==(Object other) {
final _this = this as PatchClashConfig;
return identical(this, other) || (other.runtimeType == runtimeType&&other is PatchClashConfig&&(identical(other.mixedPort, _this.mixedPort) || other.mixedPort == _this.mixedPort)&&(identical(other.socksPort, _this.socksPort) || other.socksPort == _this.socksPort)&&(identical(other.port, _this.port) || other.port == _this.port)&&(identical(other.redirPort, _this.redirPort) || other.redirPort == _this.redirPort)&&(identical(other.tproxyPort, _this.tproxyPort) || other.tproxyPort == _this.tproxyPort)&&(identical(other.mode, _this.mode) || other.mode == _this.mode)&&(identical(other.allowLan, _this.allowLan) || other.allowLan == _this.allowLan)&&(identical(other.logLevel, _this.logLevel) || other.logLevel == _this.logLevel)&&(identical(other.ipv6, _this.ipv6) || other.ipv6 == _this.ipv6)&&(identical(other.findProcessMode, _this.findProcessMode) || other.findProcessMode == _this.findProcessMode)&&(identical(other.interfaceNameMode, _this.interfaceNameMode) || other.interfaceNameMode == _this.interfaceNameMode)&&(identical(other.interfaceName, _this.interfaceName) || other.interfaceName == _this.interfaceName)&&(identical(other.keepAliveInterval, _this.keepAliveInterval) || other.keepAliveInterval == _this.keepAliveInterval)&&(identical(other.unifiedDelay, _this.unifiedDelay) || other.unifiedDelay == _this.unifiedDelay)&&(identical(other.tcpConcurrent, _this.tcpConcurrent) || other.tcpConcurrent == _this.tcpConcurrent)&&(identical(other.tun, _this.tun) || other.tun == _this.tun)&&(identical(other.dns, _this.dns) || other.dns == _this.dns)&&const DeepCollectionEquality().equals(other.dnsOverrideKeys, _this.dnsOverrideKeys)&&(identical(other.ntp, _this.ntp) || other.ntp == _this.ntp)&&const DeepCollectionEquality().equals(other.ntpOverrideKeys, _this.ntpOverrideKeys)&&const DeepCollectionEquality().equals(other.geoXUrl, _this.geoXUrl)&&(identical(other.geodataLoader, _this.geodataLoader) || other.geodataLoader == _this.geodataLoader)&&(identical(other.globalUa, _this.globalUa) || other.globalUa == _this.globalUa)&&(identical(other.externalController, _this.externalController) || other.externalController == _this.externalController)&&const DeepCollectionEquality().equals(other.hosts, _this.hosts)&&(identical(other.geoAutoUpdate, _this.geoAutoUpdate) || other.geoAutoUpdate == _this.geoAutoUpdate)&&(identical(other.geoUpdateInterval, _this.geoUpdateInterval) || other.geoUpdateInterval == _this.geoUpdateInterval));
return identical(this, other) || (other.runtimeType == runtimeType&&other is PatchClashConfig&&(identical(other.mixedPort, _this.mixedPort) || other.mixedPort == _this.mixedPort)&&(identical(other.socksPort, _this.socksPort) || other.socksPort == _this.socksPort)&&(identical(other.port, _this.port) || other.port == _this.port)&&(identical(other.redirPort, _this.redirPort) || other.redirPort == _this.redirPort)&&(identical(other.tproxyPort, _this.tproxyPort) || other.tproxyPort == _this.tproxyPort)&&(identical(other.mode, _this.mode) || other.mode == _this.mode)&&(identical(other.allowLan, _this.allowLan) || other.allowLan == _this.allowLan)&&(identical(other.logLevel, _this.logLevel) || other.logLevel == _this.logLevel)&&(identical(other.ipv6, _this.ipv6) || other.ipv6 == _this.ipv6)&&(identical(other.findProcessMode, _this.findProcessMode) || other.findProcessMode == _this.findProcessMode)&&(identical(other.interfaceNameMode, _this.interfaceNameMode) || other.interfaceNameMode == _this.interfaceNameMode)&&(identical(other.interfaceName, _this.interfaceName) || other.interfaceName == _this.interfaceName)&&(identical(other.keepAliveInterval, _this.keepAliveInterval) || other.keepAliveInterval == _this.keepAliveInterval)&&(identical(other.unifiedDelay, _this.unifiedDelay) || other.unifiedDelay == _this.unifiedDelay)&&(identical(other.tcpConcurrent, _this.tcpConcurrent) || other.tcpConcurrent == _this.tcpConcurrent)&&(identical(other.tun, _this.tun) || other.tun == _this.tun)&&(identical(other.dns, _this.dns) || other.dns == _this.dns)&&const DeepCollectionEquality().equals(other.dnsOverrideKeys, _this.dnsOverrideKeys)&&(identical(other.ntp, _this.ntp) || other.ntp == _this.ntp)&&const DeepCollectionEquality().equals(other.ntpOverrideKeys, _this.ntpOverrideKeys)&&const DeepCollectionEquality().equals(other.geoXUrl, _this.geoXUrl)&&(identical(other.geodataLoader, _this.geodataLoader) || other.geodataLoader == _this.geodataLoader)&&(identical(other.globalUa, _this.globalUa) || other.globalUa == _this.globalUa)&&(identical(other.externalController, _this.externalController) || other.externalController == _this.externalController)&&(identical(other.secret, _this.secret) || other.secret == _this.secret)&&const DeepCollectionEquality().equals(other.hosts, _this.hosts)&&(identical(other.geoAutoUpdate, _this.geoAutoUpdate) || other.geoAutoUpdate == _this.geoAutoUpdate)&&(identical(other.geoUpdateInterval, _this.geoUpdateInterval) || other.geoUpdateInterval == _this.geoUpdateInterval));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode {
final _this = this as PatchClashConfig;
return Object.hashAll([runtimeType,_this.mixedPort,_this.socksPort,_this.port,_this.redirPort,_this.tproxyPort,_this.mode,_this.allowLan,_this.logLevel,_this.ipv6,_this.findProcessMode,_this.interfaceNameMode,_this.interfaceName,_this.keepAliveInterval,_this.unifiedDelay,_this.tcpConcurrent,_this.tun,_this.dns,const DeepCollectionEquality().hash(_this.dnsOverrideKeys),_this.ntp,const DeepCollectionEquality().hash(_this.ntpOverrideKeys),const DeepCollectionEquality().hash(_this.geoXUrl),_this.geodataLoader,_this.globalUa,_this.externalController,const DeepCollectionEquality().hash(_this.hosts),_this.geoAutoUpdate,_this.geoUpdateInterval]);
return Object.hashAll([runtimeType,_this.mixedPort,_this.socksPort,_this.port,_this.redirPort,_this.tproxyPort,_this.mode,_this.allowLan,_this.logLevel,_this.ipv6,_this.findProcessMode,_this.interfaceNameMode,_this.interfaceName,_this.keepAliveInterval,_this.unifiedDelay,_this.tcpConcurrent,_this.tun,_this.dns,const DeepCollectionEquality().hash(_this.dnsOverrideKeys),_this.ntp,const DeepCollectionEquality().hash(_this.ntpOverrideKeys),const DeepCollectionEquality().hash(_this.geoXUrl),_this.geodataLoader,_this.globalUa,_this.externalController,_this.secret,const DeepCollectionEquality().hash(_this.hosts),_this.geoAutoUpdate,_this.geoUpdateInterval]);
}
@override
String toString() {
final _this = this as PatchClashConfig;
return 'PatchClashConfig(mixedPort: ${_this.mixedPort}, socksPort: ${_this.socksPort}, port: ${_this.port}, redirPort: ${_this.redirPort}, tproxyPort: ${_this.tproxyPort}, mode: ${_this.mode}, allowLan: ${_this.allowLan}, logLevel: ${_this.logLevel}, ipv6: ${_this.ipv6}, findProcessMode: ${_this.findProcessMode}, interfaceNameMode: ${_this.interfaceNameMode}, interfaceName: ${_this.interfaceName}, keepAliveInterval: ${_this.keepAliveInterval}, unifiedDelay: ${_this.unifiedDelay}, tcpConcurrent: ${_this.tcpConcurrent}, tun: ${_this.tun}, dns: ${_this.dns}, dnsOverrideKeys: ${_this.dnsOverrideKeys}, ntp: ${_this.ntp}, ntpOverrideKeys: ${_this.ntpOverrideKeys}, geoXUrl: ${_this.geoXUrl}, geodataLoader: ${_this.geodataLoader}, globalUa: ${_this.globalUa}, externalController: ${_this.externalController}, hosts: ${_this.hosts}, geoAutoUpdate: ${_this.geoAutoUpdate}, geoUpdateInterval: ${_this.geoUpdateInterval})';
return 'PatchClashConfig(mixedPort: ${_this.mixedPort}, socksPort: ${_this.socksPort}, port: ${_this.port}, redirPort: ${_this.redirPort}, tproxyPort: ${_this.tproxyPort}, mode: ${_this.mode}, allowLan: ${_this.allowLan}, logLevel: ${_this.logLevel}, ipv6: ${_this.ipv6}, findProcessMode: ${_this.findProcessMode}, interfaceNameMode: ${_this.interfaceNameMode}, interfaceName: ${_this.interfaceName}, keepAliveInterval: ${_this.keepAliveInterval}, unifiedDelay: ${_this.unifiedDelay}, tcpConcurrent: ${_this.tcpConcurrent}, tun: ${_this.tun}, dns: ${_this.dns}, dnsOverrideKeys: ${_this.dnsOverrideKeys}, ntp: ${_this.ntp}, ntpOverrideKeys: ${_this.ntpOverrideKeys}, geoXUrl: ${_this.geoXUrl}, geodataLoader: ${_this.geodataLoader}, globalUa: ${_this.globalUa}, externalController: ${_this.externalController}, secret: ${_this.secret}, hosts: ${_this.hosts}, geoAutoUpdate: ${_this.geoAutoUpdate}, geoUpdateInterval: ${_this.geoUpdateInterval})';
}
@@ -6310,7 +6310,7 @@ abstract mixin class $PatchClashConfigCopyWith<$Res> {
factory $PatchClashConfigCopyWith(PatchClashConfig value, $Res Function(PatchClashConfig) _then) = _$PatchClashConfigCopyWithImpl;
@useResult
$Res call({
@JsonKey(name: 'mixed-port') int mixedPort,@JsonKey(name: 'socks-port') int socksPort,@JsonKey(name: 'port') int port,@JsonKey(name: 'redir-port') int redirPort,@JsonKey(name: 'tproxy-port') int tproxyPort, Mode mode,@JsonKey(name: 'allow-lan') bool allowLan,@JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6,@JsonKey(name: 'find-process-mode', unknownEnumValue: FindProcessMode.off) FindProcessMode findProcessMode,@JsonKey(name: 'interface-name-mode', unknownEnumValue: InterfaceNameMode.clear) InterfaceNameMode interfaceNameMode,@JsonKey(name: 'interface-name') String interfaceName,@JsonKey(name: 'keep-alive-interval') int keepAliveInterval,@JsonKey(name: 'unified-delay') bool unifiedDelay,@JsonKey(name: 'tcp-concurrent') bool tcpConcurrent,@JsonKey(fromJson: Tun.safeFormJson) Tun tun,@JsonKey(fromJson: Dns.safeDnsFromJson) Dns dns,@JsonKey(name: _dnsOverrideKeysJsonKey, fromJson: _dnsOverrideKeysFromJson) Set<DnsOverrideKey> dnsOverrideKeys,@JsonKey(fromJson: Ntp.safeNtpFromJson) Ntp ntp,@JsonKey(name: _ntpOverrideKeysJsonKey, fromJson: _ntpOverrideKeysFromJson) Set<NtpOverrideKey> ntpOverrideKeys,@JsonKey(name: 'geox-url', fromJson: _geoXUrlFromJson, toJson: _geoXUrlToJson) Map<GeoResource, String> geoXUrl,@JsonKey(name: 'geodata-loader') GeodataLoader geodataLoader,@JsonKey(name: 'global-ua') String? globalUa,@JsonKey(name: 'external-controller') ExternalControllerStatus externalController, Map<String, String> hosts,@JsonKey(name: 'geo-auto-update') bool geoAutoUpdate,@JsonKey(name: 'geo-update-interval') int geoUpdateInterval
@JsonKey(name: 'mixed-port') int mixedPort,@JsonKey(name: 'socks-port') int socksPort,@JsonKey(name: 'port') int port,@JsonKey(name: 'redir-port') int redirPort,@JsonKey(name: 'tproxy-port') int tproxyPort, Mode mode,@JsonKey(name: 'allow-lan') bool allowLan,@JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6,@JsonKey(name: 'find-process-mode', unknownEnumValue: FindProcessMode.off) FindProcessMode findProcessMode,@JsonKey(name: 'interface-name-mode', unknownEnumValue: InterfaceNameMode.clear) InterfaceNameMode interfaceNameMode,@JsonKey(name: 'interface-name') String interfaceName,@JsonKey(name: 'keep-alive-interval') int keepAliveInterval,@JsonKey(name: 'unified-delay') bool unifiedDelay,@JsonKey(name: 'tcp-concurrent') bool tcpConcurrent,@JsonKey(fromJson: Tun.safeFormJson) Tun tun,@JsonKey(fromJson: Dns.safeDnsFromJson) Dns dns,@JsonKey(name: _dnsOverrideKeysJsonKey, fromJson: _dnsOverrideKeysFromJson) Set<DnsOverrideKey> dnsOverrideKeys,@JsonKey(fromJson: Ntp.safeNtpFromJson) Ntp ntp,@JsonKey(name: _ntpOverrideKeysJsonKey, fromJson: _ntpOverrideKeysFromJson) Set<NtpOverrideKey> ntpOverrideKeys,@JsonKey(name: 'geox-url', fromJson: _geoXUrlFromJson, toJson: _geoXUrlToJson) Map<GeoResource, String> geoXUrl,@JsonKey(name: 'geodata-loader') GeodataLoader geodataLoader,@JsonKey(name: 'global-ua') String? globalUa,@JsonKey(name: 'external-controller') ExternalControllerStatus externalController, String secret, Map<String, String> hosts,@JsonKey(name: 'geo-auto-update') bool geoAutoUpdate,@JsonKey(name: 'geo-update-interval') int geoUpdateInterval
});
@@ -6327,7 +6327,7 @@ class _$PatchClashConfigCopyWithImpl<$Res>
/// Create a copy of PatchClashConfig
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? mixedPort = null,Object? socksPort = null,Object? port = null,Object? redirPort = null,Object? tproxyPort = null,Object? mode = null,Object? allowLan = null,Object? logLevel = null,Object? ipv6 = null,Object? findProcessMode = null,Object? interfaceNameMode = null,Object? interfaceName = null,Object? keepAliveInterval = null,Object? unifiedDelay = null,Object? tcpConcurrent = null,Object? tun = null,Object? dns = null,Object? dnsOverrideKeys = null,Object? ntp = null,Object? ntpOverrideKeys = null,Object? geoXUrl = null,Object? geodataLoader = null,Object? globalUa = freezed,Object? externalController = null,Object? hosts = null,Object? geoAutoUpdate = null,Object? geoUpdateInterval = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? mixedPort = null,Object? socksPort = null,Object? port = null,Object? redirPort = null,Object? tproxyPort = null,Object? mode = null,Object? allowLan = null,Object? logLevel = null,Object? ipv6 = null,Object? findProcessMode = null,Object? interfaceNameMode = null,Object? interfaceName = null,Object? keepAliveInterval = null,Object? unifiedDelay = null,Object? tcpConcurrent = null,Object? tun = null,Object? dns = null,Object? dnsOverrideKeys = null,Object? ntp = null,Object? ntpOverrideKeys = null,Object? geoXUrl = null,Object? geodataLoader = null,Object? globalUa = freezed,Object? externalController = null,Object? secret = null,Object? hosts = null,Object? geoAutoUpdate = null,Object? geoUpdateInterval = null,}) {
return _then(PatchClashConfig(
mixedPort: null == mixedPort ? _self.mixedPort : mixedPort // ignore: cast_nullable_to_non_nullable
as int,socksPort: null == socksPort ? _self.socksPort : socksPort // ignore: cast_nullable_to_non_nullable
@@ -6353,7 +6353,8 @@ as Set<NtpOverrideKey>,geoXUrl: null == geoXUrl ? _self.geoXUrl : geoXUrl // ign
as Map<GeoResource, String>,geodataLoader: null == geodataLoader ? _self.geodataLoader : geodataLoader // ignore: cast_nullable_to_non_nullable
as GeodataLoader,globalUa: freezed == globalUa ? _self.globalUa : globalUa // ignore: cast_nullable_to_non_nullable
as String?,externalController: null == externalController ? _self.externalController : externalController // ignore: cast_nullable_to_non_nullable
as ExternalControllerStatus,hosts: null == hosts ? _self.hosts : hosts // ignore: cast_nullable_to_non_nullable
as ExternalControllerStatus,secret: null == secret ? _self.secret : secret // ignore: cast_nullable_to_non_nullable
as String,hosts: null == hosts ? _self.hosts : hosts // ignore: cast_nullable_to_non_nullable
as Map<String, String>,geoAutoUpdate: null == geoAutoUpdate ? _self.geoAutoUpdate : geoAutoUpdate // ignore: cast_nullable_to_non_nullable
as bool,geoUpdateInterval: null == geoUpdateInterval ? _self.geoUpdateInterval : geoUpdateInterval // ignore: cast_nullable_to_non_nullable
as int,
@@ -6468,10 +6469,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function(@JsonKey(name: 'mixed-port') int mixedPort, @JsonKey(name: 'socks-port') int socksPort, @JsonKey(name: 'port') int port, @JsonKey(name: 'redir-port') int redirPort, @JsonKey(name: 'tproxy-port') int tproxyPort, Mode mode, @JsonKey(name: 'allow-lan') bool allowLan, @JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6, @JsonKey(name: 'find-process-mode', unknownEnumValue: FindProcessMode.off) FindProcessMode findProcessMode, @JsonKey(name: 'interface-name-mode', unknownEnumValue: InterfaceNameMode.clear) InterfaceNameMode interfaceNameMode, @JsonKey(name: 'interface-name') String interfaceName, @JsonKey(name: 'keep-alive-interval') int keepAliveInterval, @JsonKey(name: 'unified-delay') bool unifiedDelay, @JsonKey(name: 'tcp-concurrent') bool tcpConcurrent, @JsonKey(fromJson: Tun.safeFormJson) Tun tun, @JsonKey(fromJson: Dns.safeDnsFromJson) Dns dns, @JsonKey(name: _dnsOverrideKeysJsonKey, fromJson: _dnsOverrideKeysFromJson) Set<DnsOverrideKey> dnsOverrideKeys, @JsonKey(fromJson: Ntp.safeNtpFromJson) Ntp ntp, @JsonKey(name: _ntpOverrideKeysJsonKey, fromJson: _ntpOverrideKeysFromJson) Set<NtpOverrideKey> ntpOverrideKeys, @JsonKey(name: 'geox-url', fromJson: _geoXUrlFromJson, toJson: _geoXUrlToJson) Map<GeoResource, String> geoXUrl, @JsonKey(name: 'geodata-loader') GeodataLoader geodataLoader, @JsonKey(name: 'global-ua') String? globalUa, @JsonKey(name: 'external-controller') ExternalControllerStatus externalController, Map<String, String> hosts, @JsonKey(name: 'geo-auto-update') bool geoAutoUpdate, @JsonKey(name: 'geo-update-interval') int geoUpdateInterval)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function(@JsonKey(name: 'mixed-port') int mixedPort, @JsonKey(name: 'socks-port') int socksPort, @JsonKey(name: 'port') int port, @JsonKey(name: 'redir-port') int redirPort, @JsonKey(name: 'tproxy-port') int tproxyPort, Mode mode, @JsonKey(name: 'allow-lan') bool allowLan, @JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6, @JsonKey(name: 'find-process-mode', unknownEnumValue: FindProcessMode.off) FindProcessMode findProcessMode, @JsonKey(name: 'interface-name-mode', unknownEnumValue: InterfaceNameMode.clear) InterfaceNameMode interfaceNameMode, @JsonKey(name: 'interface-name') String interfaceName, @JsonKey(name: 'keep-alive-interval') int keepAliveInterval, @JsonKey(name: 'unified-delay') bool unifiedDelay, @JsonKey(name: 'tcp-concurrent') bool tcpConcurrent, @JsonKey(fromJson: Tun.safeFormJson) Tun tun, @JsonKey(fromJson: Dns.safeDnsFromJson) Dns dns, @JsonKey(name: _dnsOverrideKeysJsonKey, fromJson: _dnsOverrideKeysFromJson) Set<DnsOverrideKey> dnsOverrideKeys, @JsonKey(fromJson: Ntp.safeNtpFromJson) Ntp ntp, @JsonKey(name: _ntpOverrideKeysJsonKey, fromJson: _ntpOverrideKeysFromJson) Set<NtpOverrideKey> ntpOverrideKeys, @JsonKey(name: 'geox-url', fromJson: _geoXUrlFromJson, toJson: _geoXUrlToJson) Map<GeoResource, String> geoXUrl, @JsonKey(name: 'geodata-loader') GeodataLoader geodataLoader, @JsonKey(name: 'global-ua') String? globalUa, @JsonKey(name: 'external-controller') ExternalControllerStatus externalController, String secret, Map<String, String> hosts, @JsonKey(name: 'geo-auto-update') bool geoAutoUpdate, @JsonKey(name: 'geo-update-interval') int geoUpdateInterval)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _PatchClashConfig() when $default != null:
return $default(_that.mixedPort,_that.socksPort,_that.port,_that.redirPort,_that.tproxyPort,_that.mode,_that.allowLan,_that.logLevel,_that.ipv6,_that.findProcessMode,_that.interfaceNameMode,_that.interfaceName,_that.keepAliveInterval,_that.unifiedDelay,_that.tcpConcurrent,_that.tun,_that.dns,_that.dnsOverrideKeys,_that.ntp,_that.ntpOverrideKeys,_that.geoXUrl,_that.geodataLoader,_that.globalUa,_that.externalController,_that.hosts,_that.geoAutoUpdate,_that.geoUpdateInterval);case _:
return $default(_that.mixedPort,_that.socksPort,_that.port,_that.redirPort,_that.tproxyPort,_that.mode,_that.allowLan,_that.logLevel,_that.ipv6,_that.findProcessMode,_that.interfaceNameMode,_that.interfaceName,_that.keepAliveInterval,_that.unifiedDelay,_that.tcpConcurrent,_that.tun,_that.dns,_that.dnsOverrideKeys,_that.ntp,_that.ntpOverrideKeys,_that.geoXUrl,_that.geodataLoader,_that.globalUa,_that.externalController,_that.secret,_that.hosts,_that.geoAutoUpdate,_that.geoUpdateInterval);case _:
return orElse();
}
@@ -6489,10 +6490,10 @@ return $default(_that.mixedPort,_that.socksPort,_that.port,_that.redirPort,_that
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function(@JsonKey(name: 'mixed-port') int mixedPort, @JsonKey(name: 'socks-port') int socksPort, @JsonKey(name: 'port') int port, @JsonKey(name: 'redir-port') int redirPort, @JsonKey(name: 'tproxy-port') int tproxyPort, Mode mode, @JsonKey(name: 'allow-lan') bool allowLan, @JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6, @JsonKey(name: 'find-process-mode', unknownEnumValue: FindProcessMode.off) FindProcessMode findProcessMode, @JsonKey(name: 'interface-name-mode', unknownEnumValue: InterfaceNameMode.clear) InterfaceNameMode interfaceNameMode, @JsonKey(name: 'interface-name') String interfaceName, @JsonKey(name: 'keep-alive-interval') int keepAliveInterval, @JsonKey(name: 'unified-delay') bool unifiedDelay, @JsonKey(name: 'tcp-concurrent') bool tcpConcurrent, @JsonKey(fromJson: Tun.safeFormJson) Tun tun, @JsonKey(fromJson: Dns.safeDnsFromJson) Dns dns, @JsonKey(name: _dnsOverrideKeysJsonKey, fromJson: _dnsOverrideKeysFromJson) Set<DnsOverrideKey> dnsOverrideKeys, @JsonKey(fromJson: Ntp.safeNtpFromJson) Ntp ntp, @JsonKey(name: _ntpOverrideKeysJsonKey, fromJson: _ntpOverrideKeysFromJson) Set<NtpOverrideKey> ntpOverrideKeys, @JsonKey(name: 'geox-url', fromJson: _geoXUrlFromJson, toJson: _geoXUrlToJson) Map<GeoResource, String> geoXUrl, @JsonKey(name: 'geodata-loader') GeodataLoader geodataLoader, @JsonKey(name: 'global-ua') String? globalUa, @JsonKey(name: 'external-controller') ExternalControllerStatus externalController, Map<String, String> hosts, @JsonKey(name: 'geo-auto-update') bool geoAutoUpdate, @JsonKey(name: 'geo-update-interval') int geoUpdateInterval) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function(@JsonKey(name: 'mixed-port') int mixedPort, @JsonKey(name: 'socks-port') int socksPort, @JsonKey(name: 'port') int port, @JsonKey(name: 'redir-port') int redirPort, @JsonKey(name: 'tproxy-port') int tproxyPort, Mode mode, @JsonKey(name: 'allow-lan') bool allowLan, @JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6, @JsonKey(name: 'find-process-mode', unknownEnumValue: FindProcessMode.off) FindProcessMode findProcessMode, @JsonKey(name: 'interface-name-mode', unknownEnumValue: InterfaceNameMode.clear) InterfaceNameMode interfaceNameMode, @JsonKey(name: 'interface-name') String interfaceName, @JsonKey(name: 'keep-alive-interval') int keepAliveInterval, @JsonKey(name: 'unified-delay') bool unifiedDelay, @JsonKey(name: 'tcp-concurrent') bool tcpConcurrent, @JsonKey(fromJson: Tun.safeFormJson) Tun tun, @JsonKey(fromJson: Dns.safeDnsFromJson) Dns dns, @JsonKey(name: _dnsOverrideKeysJsonKey, fromJson: _dnsOverrideKeysFromJson) Set<DnsOverrideKey> dnsOverrideKeys, @JsonKey(fromJson: Ntp.safeNtpFromJson) Ntp ntp, @JsonKey(name: _ntpOverrideKeysJsonKey, fromJson: _ntpOverrideKeysFromJson) Set<NtpOverrideKey> ntpOverrideKeys, @JsonKey(name: 'geox-url', fromJson: _geoXUrlFromJson, toJson: _geoXUrlToJson) Map<GeoResource, String> geoXUrl, @JsonKey(name: 'geodata-loader') GeodataLoader geodataLoader, @JsonKey(name: 'global-ua') String? globalUa, @JsonKey(name: 'external-controller') ExternalControllerStatus externalController, String secret, Map<String, String> hosts, @JsonKey(name: 'geo-auto-update') bool geoAutoUpdate, @JsonKey(name: 'geo-update-interval') int geoUpdateInterval) $default,) {final _that = this;
switch (_that) {
case _PatchClashConfig():
return $default(_that.mixedPort,_that.socksPort,_that.port,_that.redirPort,_that.tproxyPort,_that.mode,_that.allowLan,_that.logLevel,_that.ipv6,_that.findProcessMode,_that.interfaceNameMode,_that.interfaceName,_that.keepAliveInterval,_that.unifiedDelay,_that.tcpConcurrent,_that.tun,_that.dns,_that.dnsOverrideKeys,_that.ntp,_that.ntpOverrideKeys,_that.geoXUrl,_that.geodataLoader,_that.globalUa,_that.externalController,_that.hosts,_that.geoAutoUpdate,_that.geoUpdateInterval);case _:
return $default(_that.mixedPort,_that.socksPort,_that.port,_that.redirPort,_that.tproxyPort,_that.mode,_that.allowLan,_that.logLevel,_that.ipv6,_that.findProcessMode,_that.interfaceNameMode,_that.interfaceName,_that.keepAliveInterval,_that.unifiedDelay,_that.tcpConcurrent,_that.tun,_that.dns,_that.dnsOverrideKeys,_that.ntp,_that.ntpOverrideKeys,_that.geoXUrl,_that.geodataLoader,_that.globalUa,_that.externalController,_that.secret,_that.hosts,_that.geoAutoUpdate,_that.geoUpdateInterval);case _:
throw StateError('Unexpected subclass');
}
@@ -6509,10 +6510,10 @@ return $default(_that.mixedPort,_that.socksPort,_that.port,_that.redirPort,_that
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function(@JsonKey(name: 'mixed-port') int mixedPort, @JsonKey(name: 'socks-port') int socksPort, @JsonKey(name: 'port') int port, @JsonKey(name: 'redir-port') int redirPort, @JsonKey(name: 'tproxy-port') int tproxyPort, Mode mode, @JsonKey(name: 'allow-lan') bool allowLan, @JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6, @JsonKey(name: 'find-process-mode', unknownEnumValue: FindProcessMode.off) FindProcessMode findProcessMode, @JsonKey(name: 'interface-name-mode', unknownEnumValue: InterfaceNameMode.clear) InterfaceNameMode interfaceNameMode, @JsonKey(name: 'interface-name') String interfaceName, @JsonKey(name: 'keep-alive-interval') int keepAliveInterval, @JsonKey(name: 'unified-delay') bool unifiedDelay, @JsonKey(name: 'tcp-concurrent') bool tcpConcurrent, @JsonKey(fromJson: Tun.safeFormJson) Tun tun, @JsonKey(fromJson: Dns.safeDnsFromJson) Dns dns, @JsonKey(name: _dnsOverrideKeysJsonKey, fromJson: _dnsOverrideKeysFromJson) Set<DnsOverrideKey> dnsOverrideKeys, @JsonKey(fromJson: Ntp.safeNtpFromJson) Ntp ntp, @JsonKey(name: _ntpOverrideKeysJsonKey, fromJson: _ntpOverrideKeysFromJson) Set<NtpOverrideKey> ntpOverrideKeys, @JsonKey(name: 'geox-url', fromJson: _geoXUrlFromJson, toJson: _geoXUrlToJson) Map<GeoResource, String> geoXUrl, @JsonKey(name: 'geodata-loader') GeodataLoader geodataLoader, @JsonKey(name: 'global-ua') String? globalUa, @JsonKey(name: 'external-controller') ExternalControllerStatus externalController, Map<String, String> hosts, @JsonKey(name: 'geo-auto-update') bool geoAutoUpdate, @JsonKey(name: 'geo-update-interval') int geoUpdateInterval)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function(@JsonKey(name: 'mixed-port') int mixedPort, @JsonKey(name: 'socks-port') int socksPort, @JsonKey(name: 'port') int port, @JsonKey(name: 'redir-port') int redirPort, @JsonKey(name: 'tproxy-port') int tproxyPort, Mode mode, @JsonKey(name: 'allow-lan') bool allowLan, @JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6, @JsonKey(name: 'find-process-mode', unknownEnumValue: FindProcessMode.off) FindProcessMode findProcessMode, @JsonKey(name: 'interface-name-mode', unknownEnumValue: InterfaceNameMode.clear) InterfaceNameMode interfaceNameMode, @JsonKey(name: 'interface-name') String interfaceName, @JsonKey(name: 'keep-alive-interval') int keepAliveInterval, @JsonKey(name: 'unified-delay') bool unifiedDelay, @JsonKey(name: 'tcp-concurrent') bool tcpConcurrent, @JsonKey(fromJson: Tun.safeFormJson) Tun tun, @JsonKey(fromJson: Dns.safeDnsFromJson) Dns dns, @JsonKey(name: _dnsOverrideKeysJsonKey, fromJson: _dnsOverrideKeysFromJson) Set<DnsOverrideKey> dnsOverrideKeys, @JsonKey(fromJson: Ntp.safeNtpFromJson) Ntp ntp, @JsonKey(name: _ntpOverrideKeysJsonKey, fromJson: _ntpOverrideKeysFromJson) Set<NtpOverrideKey> ntpOverrideKeys, @JsonKey(name: 'geox-url', fromJson: _geoXUrlFromJson, toJson: _geoXUrlToJson) Map<GeoResource, String> geoXUrl, @JsonKey(name: 'geodata-loader') GeodataLoader geodataLoader, @JsonKey(name: 'global-ua') String? globalUa, @JsonKey(name: 'external-controller') ExternalControllerStatus externalController, String secret, Map<String, String> hosts, @JsonKey(name: 'geo-auto-update') bool geoAutoUpdate, @JsonKey(name: 'geo-update-interval') int geoUpdateInterval)? $default,) {final _that = this;
switch (_that) {
case _PatchClashConfig() when $default != null:
return $default(_that.mixedPort,_that.socksPort,_that.port,_that.redirPort,_that.tproxyPort,_that.mode,_that.allowLan,_that.logLevel,_that.ipv6,_that.findProcessMode,_that.interfaceNameMode,_that.interfaceName,_that.keepAliveInterval,_that.unifiedDelay,_that.tcpConcurrent,_that.tun,_that.dns,_that.dnsOverrideKeys,_that.ntp,_that.ntpOverrideKeys,_that.geoXUrl,_that.geodataLoader,_that.globalUa,_that.externalController,_that.hosts,_that.geoAutoUpdate,_that.geoUpdateInterval);case _:
return $default(_that.mixedPort,_that.socksPort,_that.port,_that.redirPort,_that.tproxyPort,_that.mode,_that.allowLan,_that.logLevel,_that.ipv6,_that.findProcessMode,_that.interfaceNameMode,_that.interfaceName,_that.keepAliveInterval,_that.unifiedDelay,_that.tcpConcurrent,_that.tun,_that.dns,_that.dnsOverrideKeys,_that.ntp,_that.ntpOverrideKeys,_that.geoXUrl,_that.geodataLoader,_that.globalUa,_that.externalController,_that.secret,_that.hosts,_that.geoAutoUpdate,_that.geoUpdateInterval);case _:
return null;
}
@@ -6524,7 +6525,7 @@ return $default(_that.mixedPort,_that.socksPort,_that.port,_that.redirPort,_that
@JsonSerializable()
class _PatchClashConfig implements PatchClashConfig {
const _PatchClashConfig({@JsonKey(name: 'mixed-port') this.mixedPort = defaultMixedPort, @JsonKey(name: 'socks-port') this.socksPort = 0, @JsonKey(name: 'port') this.port = 0, @JsonKey(name: 'redir-port') this.redirPort = 0, @JsonKey(name: 'tproxy-port') this.tproxyPort = 0, this.mode = Mode.rule, @JsonKey(name: 'allow-lan') this.allowLan = false, @JsonKey(name: 'log-level') this.logLevel = LogLevel.error, this.ipv6 = false, @JsonKey(name: 'find-process-mode', unknownEnumValue: FindProcessMode.off) this.findProcessMode = FindProcessMode.off, @JsonKey(name: 'interface-name-mode', unknownEnumValue: InterfaceNameMode.clear) this.interfaceNameMode = InterfaceNameMode.clear, @JsonKey(name: 'interface-name') this.interfaceName = '', @JsonKey(name: 'keep-alive-interval') this.keepAliveInterval = defaultKeepAliveInterval, @JsonKey(name: 'unified-delay') this.unifiedDelay = true, @JsonKey(name: 'tcp-concurrent') this.tcpConcurrent = true, @JsonKey(fromJson: Tun.safeFormJson) this.tun = defaultTun, @JsonKey(fromJson: Dns.safeDnsFromJson) this.dns = defaultDns, @JsonKey(name: _dnsOverrideKeysJsonKey, fromJson: _dnsOverrideKeysFromJson) Set<DnsOverrideKey> dnsOverrideKeys = const {}, @JsonKey(fromJson: Ntp.safeNtpFromJson) this.ntp = defaultNtp, @JsonKey(name: _ntpOverrideKeysJsonKey, fromJson: _ntpOverrideKeysFromJson) Set<NtpOverrideKey> ntpOverrideKeys = const {}, @JsonKey(name: 'geox-url', fromJson: _geoXUrlFromJson, toJson: _geoXUrlToJson) Map<GeoResource, String> geoXUrl = defaultGeoXUrl, @JsonKey(name: 'geodata-loader') this.geodataLoader = GeodataLoader.memconservative, @JsonKey(name: 'global-ua') this.globalUa, @JsonKey(name: 'external-controller') this.externalController = ExternalControllerStatus.close, Map<String, String> hosts = const {}, @JsonKey(name: 'geo-auto-update') this.geoAutoUpdate = false, @JsonKey(name: 'geo-update-interval') this.geoUpdateInterval = 24}): _dnsOverrideKeys = dnsOverrideKeys,_ntpOverrideKeys = ntpOverrideKeys,_geoXUrl = geoXUrl,_hosts = hosts;
const _PatchClashConfig({@JsonKey(name: 'mixed-port') this.mixedPort = defaultMixedPort, @JsonKey(name: 'socks-port') this.socksPort = 0, @JsonKey(name: 'port') this.port = 0, @JsonKey(name: 'redir-port') this.redirPort = 0, @JsonKey(name: 'tproxy-port') this.tproxyPort = 0, this.mode = Mode.rule, @JsonKey(name: 'allow-lan') this.allowLan = false, @JsonKey(name: 'log-level') this.logLevel = LogLevel.error, this.ipv6 = false, @JsonKey(name: 'find-process-mode', unknownEnumValue: FindProcessMode.off) this.findProcessMode = FindProcessMode.off, @JsonKey(name: 'interface-name-mode', unknownEnumValue: InterfaceNameMode.clear) this.interfaceNameMode = InterfaceNameMode.clear, @JsonKey(name: 'interface-name') this.interfaceName = '', @JsonKey(name: 'keep-alive-interval') this.keepAliveInterval = defaultKeepAliveInterval, @JsonKey(name: 'unified-delay') this.unifiedDelay = true, @JsonKey(name: 'tcp-concurrent') this.tcpConcurrent = true, @JsonKey(fromJson: Tun.safeFormJson) this.tun = defaultTun, @JsonKey(fromJson: Dns.safeDnsFromJson) this.dns = defaultDns, @JsonKey(name: _dnsOverrideKeysJsonKey, fromJson: _dnsOverrideKeysFromJson) Set<DnsOverrideKey> dnsOverrideKeys = const {}, @JsonKey(fromJson: Ntp.safeNtpFromJson) this.ntp = defaultNtp, @JsonKey(name: _ntpOverrideKeysJsonKey, fromJson: _ntpOverrideKeysFromJson) Set<NtpOverrideKey> ntpOverrideKeys = const {}, @JsonKey(name: 'geox-url', fromJson: _geoXUrlFromJson, toJson: _geoXUrlToJson) Map<GeoResource, String> geoXUrl = defaultGeoXUrl, @JsonKey(name: 'geodata-loader') this.geodataLoader = GeodataLoader.memconservative, @JsonKey(name: 'global-ua') this.globalUa, @JsonKey(name: 'external-controller') this.externalController = ExternalControllerStatus.close, this.secret = '', Map<String, String> hosts = const {}, @JsonKey(name: 'geo-auto-update') this.geoAutoUpdate = false, @JsonKey(name: 'geo-update-interval') this.geoUpdateInterval = 24}): _dnsOverrideKeys = dnsOverrideKeys,_ntpOverrideKeys = ntpOverrideKeys,_geoXUrl = geoXUrl,_hosts = hosts;
factory _PatchClashConfig.fromJson(Map<String, dynamic> json) => _$PatchClashConfigFromJson(json);
@override@JsonKey(name: 'mixed-port') final int mixedPort;
@@ -6569,6 +6570,7 @@ class _PatchClashConfig implements PatchClashConfig {
@override@JsonKey(name: 'geodata-loader') final GeodataLoader geodataLoader;
@override@JsonKey(name: 'global-ua') final String? globalUa;
@override@JsonKey(name: 'external-controller') final ExternalControllerStatus externalController;
@override@JsonKey() final String secret;
final Map<String, String> _hosts;
@override@JsonKey() Map<String, String> get hosts {
if (_hosts is EqualUnmodifiableMapView) return _hosts;
@@ -6592,18 +6594,18 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _PatchClashConfig&&(identical(other.mixedPort, mixedPort) || other.mixedPort == mixedPort)&&(identical(other.socksPort, socksPort) || other.socksPort == socksPort)&&(identical(other.port, port) || other.port == port)&&(identical(other.redirPort, redirPort) || other.redirPort == redirPort)&&(identical(other.tproxyPort, tproxyPort) || other.tproxyPort == tproxyPort)&&(identical(other.mode, mode) || other.mode == mode)&&(identical(other.allowLan, allowLan) || other.allowLan == allowLan)&&(identical(other.logLevel, logLevel) || other.logLevel == logLevel)&&(identical(other.ipv6, ipv6) || other.ipv6 == ipv6)&&(identical(other.findProcessMode, findProcessMode) || other.findProcessMode == findProcessMode)&&(identical(other.interfaceNameMode, interfaceNameMode) || other.interfaceNameMode == interfaceNameMode)&&(identical(other.interfaceName, interfaceName) || other.interfaceName == interfaceName)&&(identical(other.keepAliveInterval, keepAliveInterval) || other.keepAliveInterval == keepAliveInterval)&&(identical(other.unifiedDelay, unifiedDelay) || other.unifiedDelay == unifiedDelay)&&(identical(other.tcpConcurrent, tcpConcurrent) || other.tcpConcurrent == tcpConcurrent)&&(identical(other.tun, tun) || other.tun == tun)&&(identical(other.dns, dns) || other.dns == dns)&&const DeepCollectionEquality().equals(other.dnsOverrideKeys, _dnsOverrideKeys)&&(identical(other.ntp, ntp) || other.ntp == ntp)&&const DeepCollectionEquality().equals(other.ntpOverrideKeys, _ntpOverrideKeys)&&const DeepCollectionEquality().equals(other.geoXUrl, _geoXUrl)&&(identical(other.geodataLoader, geodataLoader) || other.geodataLoader == geodataLoader)&&(identical(other.globalUa, globalUa) || other.globalUa == globalUa)&&(identical(other.externalController, externalController) || other.externalController == externalController)&&const DeepCollectionEquality().equals(other.hosts, _hosts)&&(identical(other.geoAutoUpdate, geoAutoUpdate) || other.geoAutoUpdate == geoAutoUpdate)&&(identical(other.geoUpdateInterval, geoUpdateInterval) || other.geoUpdateInterval == geoUpdateInterval));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _PatchClashConfig&&(identical(other.mixedPort, mixedPort) || other.mixedPort == mixedPort)&&(identical(other.socksPort, socksPort) || other.socksPort == socksPort)&&(identical(other.port, port) || other.port == port)&&(identical(other.redirPort, redirPort) || other.redirPort == redirPort)&&(identical(other.tproxyPort, tproxyPort) || other.tproxyPort == tproxyPort)&&(identical(other.mode, mode) || other.mode == mode)&&(identical(other.allowLan, allowLan) || other.allowLan == allowLan)&&(identical(other.logLevel, logLevel) || other.logLevel == logLevel)&&(identical(other.ipv6, ipv6) || other.ipv6 == ipv6)&&(identical(other.findProcessMode, findProcessMode) || other.findProcessMode == findProcessMode)&&(identical(other.interfaceNameMode, interfaceNameMode) || other.interfaceNameMode == interfaceNameMode)&&(identical(other.interfaceName, interfaceName) || other.interfaceName == interfaceName)&&(identical(other.keepAliveInterval, keepAliveInterval) || other.keepAliveInterval == keepAliveInterval)&&(identical(other.unifiedDelay, unifiedDelay) || other.unifiedDelay == unifiedDelay)&&(identical(other.tcpConcurrent, tcpConcurrent) || other.tcpConcurrent == tcpConcurrent)&&(identical(other.tun, tun) || other.tun == tun)&&(identical(other.dns, dns) || other.dns == dns)&&const DeepCollectionEquality().equals(other.dnsOverrideKeys, _dnsOverrideKeys)&&(identical(other.ntp, ntp) || other.ntp == ntp)&&const DeepCollectionEquality().equals(other.ntpOverrideKeys, _ntpOverrideKeys)&&const DeepCollectionEquality().equals(other.geoXUrl, _geoXUrl)&&(identical(other.geodataLoader, geodataLoader) || other.geodataLoader == geodataLoader)&&(identical(other.globalUa, globalUa) || other.globalUa == globalUa)&&(identical(other.externalController, externalController) || other.externalController == externalController)&&(identical(other.secret, secret) || other.secret == secret)&&const DeepCollectionEquality().equals(other.hosts, _hosts)&&(identical(other.geoAutoUpdate, geoAutoUpdate) || other.geoAutoUpdate == geoAutoUpdate)&&(identical(other.geoUpdateInterval, geoUpdateInterval) || other.geoUpdateInterval == geoUpdateInterval));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode {
return Object.hashAll([runtimeType,mixedPort,socksPort,port,redirPort,tproxyPort,mode,allowLan,logLevel,ipv6,findProcessMode,interfaceNameMode,interfaceName,keepAliveInterval,unifiedDelay,tcpConcurrent,tun,dns,const DeepCollectionEquality().hash(_dnsOverrideKeys),ntp,const DeepCollectionEquality().hash(_ntpOverrideKeys),const DeepCollectionEquality().hash(_geoXUrl),geodataLoader,globalUa,externalController,const DeepCollectionEquality().hash(_hosts),geoAutoUpdate,geoUpdateInterval]);
return Object.hashAll([runtimeType,mixedPort,socksPort,port,redirPort,tproxyPort,mode,allowLan,logLevel,ipv6,findProcessMode,interfaceNameMode,interfaceName,keepAliveInterval,unifiedDelay,tcpConcurrent,tun,dns,const DeepCollectionEquality().hash(_dnsOverrideKeys),ntp,const DeepCollectionEquality().hash(_ntpOverrideKeys),const DeepCollectionEquality().hash(_geoXUrl),geodataLoader,globalUa,externalController,secret,const DeepCollectionEquality().hash(_hosts),geoAutoUpdate,geoUpdateInterval]);
}
@override
String toString() {
return 'PatchClashConfig(mixedPort: $mixedPort, socksPort: $socksPort, port: $port, redirPort: $redirPort, tproxyPort: $tproxyPort, mode: $mode, allowLan: $allowLan, logLevel: $logLevel, ipv6: $ipv6, findProcessMode: $findProcessMode, interfaceNameMode: $interfaceNameMode, interfaceName: $interfaceName, keepAliveInterval: $keepAliveInterval, unifiedDelay: $unifiedDelay, tcpConcurrent: $tcpConcurrent, tun: $tun, dns: $dns, dnsOverrideKeys: $dnsOverrideKeys, ntp: $ntp, ntpOverrideKeys: $ntpOverrideKeys, geoXUrl: $geoXUrl, geodataLoader: $geodataLoader, globalUa: $globalUa, externalController: $externalController, hosts: $hosts, geoAutoUpdate: $geoAutoUpdate, geoUpdateInterval: $geoUpdateInterval)';
return 'PatchClashConfig(mixedPort: $mixedPort, socksPort: $socksPort, port: $port, redirPort: $redirPort, tproxyPort: $tproxyPort, mode: $mode, allowLan: $allowLan, logLevel: $logLevel, ipv6: $ipv6, findProcessMode: $findProcessMode, interfaceNameMode: $interfaceNameMode, interfaceName: $interfaceName, keepAliveInterval: $keepAliveInterval, unifiedDelay: $unifiedDelay, tcpConcurrent: $tcpConcurrent, tun: $tun, dns: $dns, dnsOverrideKeys: $dnsOverrideKeys, ntp: $ntp, ntpOverrideKeys: $ntpOverrideKeys, geoXUrl: $geoXUrl, geodataLoader: $geodataLoader, globalUa: $globalUa, externalController: $externalController, secret: $secret, hosts: $hosts, geoAutoUpdate: $geoAutoUpdate, geoUpdateInterval: $geoUpdateInterval)';
}
@@ -6614,7 +6616,7 @@ abstract mixin class _$PatchClashConfigCopyWith<$Res> implements $PatchClashConf
factory _$PatchClashConfigCopyWith(_PatchClashConfig value, $Res Function(_PatchClashConfig) _then) = __$PatchClashConfigCopyWithImpl;
@override @useResult
$Res call({
@JsonKey(name: 'mixed-port') int mixedPort,@JsonKey(name: 'socks-port') int socksPort,@JsonKey(name: 'port') int port,@JsonKey(name: 'redir-port') int redirPort,@JsonKey(name: 'tproxy-port') int tproxyPort, Mode mode,@JsonKey(name: 'allow-lan') bool allowLan,@JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6,@JsonKey(name: 'find-process-mode', unknownEnumValue: FindProcessMode.off) FindProcessMode findProcessMode,@JsonKey(name: 'interface-name-mode', unknownEnumValue: InterfaceNameMode.clear) InterfaceNameMode interfaceNameMode,@JsonKey(name: 'interface-name') String interfaceName,@JsonKey(name: 'keep-alive-interval') int keepAliveInterval,@JsonKey(name: 'unified-delay') bool unifiedDelay,@JsonKey(name: 'tcp-concurrent') bool tcpConcurrent,@JsonKey(fromJson: Tun.safeFormJson) Tun tun,@JsonKey(fromJson: Dns.safeDnsFromJson) Dns dns,@JsonKey(name: _dnsOverrideKeysJsonKey, fromJson: _dnsOverrideKeysFromJson) Set<DnsOverrideKey> dnsOverrideKeys,@JsonKey(fromJson: Ntp.safeNtpFromJson) Ntp ntp,@JsonKey(name: _ntpOverrideKeysJsonKey, fromJson: _ntpOverrideKeysFromJson) Set<NtpOverrideKey> ntpOverrideKeys,@JsonKey(name: 'geox-url', fromJson: _geoXUrlFromJson, toJson: _geoXUrlToJson) Map<GeoResource, String> geoXUrl,@JsonKey(name: 'geodata-loader') GeodataLoader geodataLoader,@JsonKey(name: 'global-ua') String? globalUa,@JsonKey(name: 'external-controller') ExternalControllerStatus externalController, Map<String, String> hosts,@JsonKey(name: 'geo-auto-update') bool geoAutoUpdate,@JsonKey(name: 'geo-update-interval') int geoUpdateInterval
@JsonKey(name: 'mixed-port') int mixedPort,@JsonKey(name: 'socks-port') int socksPort,@JsonKey(name: 'port') int port,@JsonKey(name: 'redir-port') int redirPort,@JsonKey(name: 'tproxy-port') int tproxyPort, Mode mode,@JsonKey(name: 'allow-lan') bool allowLan,@JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6,@JsonKey(name: 'find-process-mode', unknownEnumValue: FindProcessMode.off) FindProcessMode findProcessMode,@JsonKey(name: 'interface-name-mode', unknownEnumValue: InterfaceNameMode.clear) InterfaceNameMode interfaceNameMode,@JsonKey(name: 'interface-name') String interfaceName,@JsonKey(name: 'keep-alive-interval') int keepAliveInterval,@JsonKey(name: 'unified-delay') bool unifiedDelay,@JsonKey(name: 'tcp-concurrent') bool tcpConcurrent,@JsonKey(fromJson: Tun.safeFormJson) Tun tun,@JsonKey(fromJson: Dns.safeDnsFromJson) Dns dns,@JsonKey(name: _dnsOverrideKeysJsonKey, fromJson: _dnsOverrideKeysFromJson) Set<DnsOverrideKey> dnsOverrideKeys,@JsonKey(fromJson: Ntp.safeNtpFromJson) Ntp ntp,@JsonKey(name: _ntpOverrideKeysJsonKey, fromJson: _ntpOverrideKeysFromJson) Set<NtpOverrideKey> ntpOverrideKeys,@JsonKey(name: 'geox-url', fromJson: _geoXUrlFromJson, toJson: _geoXUrlToJson) Map<GeoResource, String> geoXUrl,@JsonKey(name: 'geodata-loader') GeodataLoader geodataLoader,@JsonKey(name: 'global-ua') String? globalUa,@JsonKey(name: 'external-controller') ExternalControllerStatus externalController, String secret, Map<String, String> hosts,@JsonKey(name: 'geo-auto-update') bool geoAutoUpdate,@JsonKey(name: 'geo-update-interval') int geoUpdateInterval
});
@@ -6631,7 +6633,7 @@ class __$PatchClashConfigCopyWithImpl<$Res>
/// Create a copy of PatchClashConfig
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? mixedPort = null,Object? socksPort = null,Object? port = null,Object? redirPort = null,Object? tproxyPort = null,Object? mode = null,Object? allowLan = null,Object? logLevel = null,Object? ipv6 = null,Object? findProcessMode = null,Object? interfaceNameMode = null,Object? interfaceName = null,Object? keepAliveInterval = null,Object? unifiedDelay = null,Object? tcpConcurrent = null,Object? tun = null,Object? dns = null,Object? dnsOverrideKeys = null,Object? ntp = null,Object? ntpOverrideKeys = null,Object? geoXUrl = null,Object? geodataLoader = null,Object? globalUa = freezed,Object? externalController = null,Object? hosts = null,Object? geoAutoUpdate = null,Object? geoUpdateInterval = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? mixedPort = null,Object? socksPort = null,Object? port = null,Object? redirPort = null,Object? tproxyPort = null,Object? mode = null,Object? allowLan = null,Object? logLevel = null,Object? ipv6 = null,Object? findProcessMode = null,Object? interfaceNameMode = null,Object? interfaceName = null,Object? keepAliveInterval = null,Object? unifiedDelay = null,Object? tcpConcurrent = null,Object? tun = null,Object? dns = null,Object? dnsOverrideKeys = null,Object? ntp = null,Object? ntpOverrideKeys = null,Object? geoXUrl = null,Object? geodataLoader = null,Object? globalUa = freezed,Object? externalController = null,Object? secret = null,Object? hosts = null,Object? geoAutoUpdate = null,Object? geoUpdateInterval = null,}) {
return _then(_PatchClashConfig(
mixedPort: null == mixedPort ? _self.mixedPort : mixedPort // ignore: cast_nullable_to_non_nullable
as int,socksPort: null == socksPort ? _self.socksPort : socksPort // ignore: cast_nullable_to_non_nullable
@@ -6657,7 +6659,8 @@ as Set<NtpOverrideKey>,geoXUrl: null == geoXUrl ? _self._geoXUrl : geoXUrl // ig
as Map<GeoResource, String>,geodataLoader: null == geodataLoader ? _self.geodataLoader : geodataLoader // ignore: cast_nullable_to_non_nullable
as GeodataLoader,globalUa: freezed == globalUa ? _self.globalUa : globalUa // ignore: cast_nullable_to_non_nullable
as String?,externalController: null == externalController ? _self.externalController : externalController // ignore: cast_nullable_to_non_nullable
as ExternalControllerStatus,hosts: null == hosts ? _self._hosts : hosts // ignore: cast_nullable_to_non_nullable
as ExternalControllerStatus,secret: null == secret ? _self.secret : secret // ignore: cast_nullable_to_non_nullable
as String,hosts: null == hosts ? _self._hosts : hosts // ignore: cast_nullable_to_non_nullable
as Map<String, String>,geoAutoUpdate: null == geoAutoUpdate ? _self.geoAutoUpdate : geoAutoUpdate // ignore: cast_nullable_to_non_nullable
as bool,geoUpdateInterval: null == geoUpdateInterval ? _self.geoUpdateInterval : geoUpdateInterval // ignore: cast_nullable_to_non_nullable
as int,
+2
View File
@@ -552,6 +552,7 @@ _PatchClashConfig _$PatchClashConfigFromJson(Map<String, dynamic> json) =>
json['external-controller'],
) ??
ExternalControllerStatus.close,
secret: json['secret'] as String? ?? '',
hosts:
(json['hosts'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
@@ -593,6 +594,7 @@ Map<String, dynamic> _$PatchClashConfigToJson(_PatchClashConfig instance) =>
'global-ua': instance.globalUa,
'external-controller':
_$ExternalControllerStatusEnumMap[instance.externalController]!,
'secret': instance.secret,
'hosts': instance.hosts,
'geo-auto-update': instance.geoAutoUpdate,
'geo-update-interval': instance.geoUpdateInterval,
+23 -20
View File
@@ -295,7 +295,7 @@ as String,
/// @nodoc
mixin _$UpdateParams {
Tun get tun;@JsonKey(name: 'mixed-port') int get mixedPort;@JsonKey(name: 'allow-lan') bool get allowLan;@JsonKey(name: 'find-process-mode') FindProcessMode get findProcessMode; Mode get mode;@JsonKey(name: 'log-level') LogLevel get logLevel; bool get ipv6;@JsonKey(name: 'tcp-concurrent') bool get tcpConcurrent;@JsonKey(name: 'external-controller') ExternalControllerStatus get externalController;@JsonKey(name: 'unified-delay') bool get unifiedDelay; List<String> get authentication;@JsonKey(name: 'geo-auto-update') bool get geoAutoUpdate;@JsonKey(name: 'geo-update-interval') int get geoUpdateInterval;@JsonKey(name: 'geox-url') Map<String, String> get geoXUrl;
Tun get tun;@JsonKey(name: 'mixed-port') int get mixedPort;@JsonKey(name: 'allow-lan') bool get allowLan;@JsonKey(name: 'find-process-mode') FindProcessMode get findProcessMode; Mode get mode;@JsonKey(name: 'log-level') LogLevel get logLevel; bool get ipv6;@JsonKey(name: 'tcp-concurrent') bool get tcpConcurrent;@JsonKey(name: 'external-controller') ExternalControllerStatus get externalController; String get secret;@JsonKey(name: 'unified-delay') bool get unifiedDelay; List<String> get authentication;@JsonKey(name: 'geo-auto-update') bool get geoAutoUpdate;@JsonKey(name: 'geo-update-interval') int get geoUpdateInterval;@JsonKey(name: 'geox-url') Map<String, String> get geoXUrl;
/// Create a copy of UpdateParams
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -309,20 +309,20 @@ $UpdateParamsCopyWith<UpdateParams> get copyWith => _$UpdateParamsCopyWithImpl<U
@override
bool operator ==(Object other) {
final _this = this as UpdateParams;
return identical(this, other) || (other.runtimeType == runtimeType&&other is UpdateParams&&(identical(other.tun, _this.tun) || other.tun == _this.tun)&&(identical(other.mixedPort, _this.mixedPort) || other.mixedPort == _this.mixedPort)&&(identical(other.allowLan, _this.allowLan) || other.allowLan == _this.allowLan)&&(identical(other.findProcessMode, _this.findProcessMode) || other.findProcessMode == _this.findProcessMode)&&(identical(other.mode, _this.mode) || other.mode == _this.mode)&&(identical(other.logLevel, _this.logLevel) || other.logLevel == _this.logLevel)&&(identical(other.ipv6, _this.ipv6) || other.ipv6 == _this.ipv6)&&(identical(other.tcpConcurrent, _this.tcpConcurrent) || other.tcpConcurrent == _this.tcpConcurrent)&&(identical(other.externalController, _this.externalController) || other.externalController == _this.externalController)&&(identical(other.unifiedDelay, _this.unifiedDelay) || other.unifiedDelay == _this.unifiedDelay)&&const DeepCollectionEquality().equals(other.authentication, _this.authentication)&&(identical(other.geoAutoUpdate, _this.geoAutoUpdate) || other.geoAutoUpdate == _this.geoAutoUpdate)&&(identical(other.geoUpdateInterval, _this.geoUpdateInterval) || other.geoUpdateInterval == _this.geoUpdateInterval)&&const DeepCollectionEquality().equals(other.geoXUrl, _this.geoXUrl));
return identical(this, other) || (other.runtimeType == runtimeType&&other is UpdateParams&&(identical(other.tun, _this.tun) || other.tun == _this.tun)&&(identical(other.mixedPort, _this.mixedPort) || other.mixedPort == _this.mixedPort)&&(identical(other.allowLan, _this.allowLan) || other.allowLan == _this.allowLan)&&(identical(other.findProcessMode, _this.findProcessMode) || other.findProcessMode == _this.findProcessMode)&&(identical(other.mode, _this.mode) || other.mode == _this.mode)&&(identical(other.logLevel, _this.logLevel) || other.logLevel == _this.logLevel)&&(identical(other.ipv6, _this.ipv6) || other.ipv6 == _this.ipv6)&&(identical(other.tcpConcurrent, _this.tcpConcurrent) || other.tcpConcurrent == _this.tcpConcurrent)&&(identical(other.externalController, _this.externalController) || other.externalController == _this.externalController)&&(identical(other.secret, _this.secret) || other.secret == _this.secret)&&(identical(other.unifiedDelay, _this.unifiedDelay) || other.unifiedDelay == _this.unifiedDelay)&&const DeepCollectionEquality().equals(other.authentication, _this.authentication)&&(identical(other.geoAutoUpdate, _this.geoAutoUpdate) || other.geoAutoUpdate == _this.geoAutoUpdate)&&(identical(other.geoUpdateInterval, _this.geoUpdateInterval) || other.geoUpdateInterval == _this.geoUpdateInterval)&&const DeepCollectionEquality().equals(other.geoXUrl, _this.geoXUrl));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode {
final _this = this as UpdateParams;
return Object.hash(runtimeType,_this.tun,_this.mixedPort,_this.allowLan,_this.findProcessMode,_this.mode,_this.logLevel,_this.ipv6,_this.tcpConcurrent,_this.externalController,_this.unifiedDelay,const DeepCollectionEquality().hash(_this.authentication),_this.geoAutoUpdate,_this.geoUpdateInterval,const DeepCollectionEquality().hash(_this.geoXUrl));
return Object.hash(runtimeType,_this.tun,_this.mixedPort,_this.allowLan,_this.findProcessMode,_this.mode,_this.logLevel,_this.ipv6,_this.tcpConcurrent,_this.externalController,_this.secret,_this.unifiedDelay,const DeepCollectionEquality().hash(_this.authentication),_this.geoAutoUpdate,_this.geoUpdateInterval,const DeepCollectionEquality().hash(_this.geoXUrl));
}
@override
String toString() {
final _this = this as UpdateParams;
return 'UpdateParams(tun: ${_this.tun}, mixedPort: ${_this.mixedPort}, allowLan: ${_this.allowLan}, findProcessMode: ${_this.findProcessMode}, mode: ${_this.mode}, logLevel: ${_this.logLevel}, ipv6: ${_this.ipv6}, tcpConcurrent: ${_this.tcpConcurrent}, externalController: ${_this.externalController}, unifiedDelay: ${_this.unifiedDelay}, authentication: ${_this.authentication}, geoAutoUpdate: ${_this.geoAutoUpdate}, geoUpdateInterval: ${_this.geoUpdateInterval}, geoXUrl: ${_this.geoXUrl})';
return 'UpdateParams(tun: ${_this.tun}, mixedPort: ${_this.mixedPort}, allowLan: ${_this.allowLan}, findProcessMode: ${_this.findProcessMode}, mode: ${_this.mode}, logLevel: ${_this.logLevel}, ipv6: ${_this.ipv6}, tcpConcurrent: ${_this.tcpConcurrent}, externalController: ${_this.externalController}, secret: ${_this.secret}, unifiedDelay: ${_this.unifiedDelay}, authentication: ${_this.authentication}, geoAutoUpdate: ${_this.geoAutoUpdate}, geoUpdateInterval: ${_this.geoUpdateInterval}, geoXUrl: ${_this.geoXUrl})';
}
@@ -333,7 +333,7 @@ abstract mixin class $UpdateParamsCopyWith<$Res> {
factory $UpdateParamsCopyWith(UpdateParams value, $Res Function(UpdateParams) _then) = _$UpdateParamsCopyWithImpl;
@useResult
$Res call({
Tun tun,@JsonKey(name: 'mixed-port') int mixedPort,@JsonKey(name: 'allow-lan') bool allowLan,@JsonKey(name: 'find-process-mode') FindProcessMode findProcessMode, Mode mode,@JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6,@JsonKey(name: 'tcp-concurrent') bool tcpConcurrent,@JsonKey(name: 'external-controller') ExternalControllerStatus externalController,@JsonKey(name: 'unified-delay') bool unifiedDelay, List<String> authentication,@JsonKey(name: 'geo-auto-update') bool geoAutoUpdate,@JsonKey(name: 'geo-update-interval') int geoUpdateInterval,@JsonKey(name: 'geox-url') Map<String, String> geoXUrl
Tun tun,@JsonKey(name: 'mixed-port') int mixedPort,@JsonKey(name: 'allow-lan') bool allowLan,@JsonKey(name: 'find-process-mode') FindProcessMode findProcessMode, Mode mode,@JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6,@JsonKey(name: 'tcp-concurrent') bool tcpConcurrent,@JsonKey(name: 'external-controller') ExternalControllerStatus externalController, String secret,@JsonKey(name: 'unified-delay') bool unifiedDelay, List<String> authentication,@JsonKey(name: 'geo-auto-update') bool geoAutoUpdate,@JsonKey(name: 'geo-update-interval') int geoUpdateInterval,@JsonKey(name: 'geox-url') Map<String, String> geoXUrl
});
@@ -350,7 +350,7 @@ class _$UpdateParamsCopyWithImpl<$Res>
/// Create a copy of UpdateParams
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? tun = null,Object? mixedPort = null,Object? allowLan = null,Object? findProcessMode = null,Object? mode = null,Object? logLevel = null,Object? ipv6 = null,Object? tcpConcurrent = null,Object? externalController = null,Object? unifiedDelay = null,Object? authentication = null,Object? geoAutoUpdate = null,Object? geoUpdateInterval = null,Object? geoXUrl = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? tun = null,Object? mixedPort = null,Object? allowLan = null,Object? findProcessMode = null,Object? mode = null,Object? logLevel = null,Object? ipv6 = null,Object? tcpConcurrent = null,Object? externalController = null,Object? secret = null,Object? unifiedDelay = null,Object? authentication = null,Object? geoAutoUpdate = null,Object? geoUpdateInterval = null,Object? geoXUrl = null,}) {
return _then(UpdateParams(
tun: null == tun ? _self.tun : tun // ignore: cast_nullable_to_non_nullable
as Tun,mixedPort: null == mixedPort ? _self.mixedPort : mixedPort // ignore: cast_nullable_to_non_nullable
@@ -361,7 +361,8 @@ as Mode,logLevel: null == logLevel ? _self.logLevel : logLevel // ignore: cast_n
as LogLevel,ipv6: null == ipv6 ? _self.ipv6 : ipv6 // ignore: cast_nullable_to_non_nullable
as bool,tcpConcurrent: null == tcpConcurrent ? _self.tcpConcurrent : tcpConcurrent // ignore: cast_nullable_to_non_nullable
as bool,externalController: null == externalController ? _self.externalController : externalController // ignore: cast_nullable_to_non_nullable
as ExternalControllerStatus,unifiedDelay: null == unifiedDelay ? _self.unifiedDelay : unifiedDelay // ignore: cast_nullable_to_non_nullable
as ExternalControllerStatus,secret: null == secret ? _self.secret : secret // ignore: cast_nullable_to_non_nullable
as String,unifiedDelay: null == unifiedDelay ? _self.unifiedDelay : unifiedDelay // ignore: cast_nullable_to_non_nullable
as bool,authentication: null == authentication ? _self.authentication : authentication // ignore: cast_nullable_to_non_nullable
as List<String>,geoAutoUpdate: null == geoAutoUpdate ? _self.geoAutoUpdate : geoAutoUpdate // ignore: cast_nullable_to_non_nullable
as bool,geoUpdateInterval: null == geoUpdateInterval ? _self.geoUpdateInterval : geoUpdateInterval // ignore: cast_nullable_to_non_nullable
@@ -460,10 +461,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( Tun tun, @JsonKey(name: 'mixed-port') int mixedPort, @JsonKey(name: 'allow-lan') bool allowLan, @JsonKey(name: 'find-process-mode') FindProcessMode findProcessMode, Mode mode, @JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6, @JsonKey(name: 'tcp-concurrent') bool tcpConcurrent, @JsonKey(name: 'external-controller') ExternalControllerStatus externalController, @JsonKey(name: 'unified-delay') bool unifiedDelay, List<String> authentication, @JsonKey(name: 'geo-auto-update') bool geoAutoUpdate, @JsonKey(name: 'geo-update-interval') int geoUpdateInterval, @JsonKey(name: 'geox-url') Map<String, String> geoXUrl)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( Tun tun, @JsonKey(name: 'mixed-port') int mixedPort, @JsonKey(name: 'allow-lan') bool allowLan, @JsonKey(name: 'find-process-mode') FindProcessMode findProcessMode, Mode mode, @JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6, @JsonKey(name: 'tcp-concurrent') bool tcpConcurrent, @JsonKey(name: 'external-controller') ExternalControllerStatus externalController, String secret, @JsonKey(name: 'unified-delay') bool unifiedDelay, List<String> authentication, @JsonKey(name: 'geo-auto-update') bool geoAutoUpdate, @JsonKey(name: 'geo-update-interval') int geoUpdateInterval, @JsonKey(name: 'geox-url') Map<String, String> geoXUrl)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _UpdateParams() when $default != null:
return $default(_that.tun,_that.mixedPort,_that.allowLan,_that.findProcessMode,_that.mode,_that.logLevel,_that.ipv6,_that.tcpConcurrent,_that.externalController,_that.unifiedDelay,_that.authentication,_that.geoAutoUpdate,_that.geoUpdateInterval,_that.geoXUrl);case _:
return $default(_that.tun,_that.mixedPort,_that.allowLan,_that.findProcessMode,_that.mode,_that.logLevel,_that.ipv6,_that.tcpConcurrent,_that.externalController,_that.secret,_that.unifiedDelay,_that.authentication,_that.geoAutoUpdate,_that.geoUpdateInterval,_that.geoXUrl);case _:
return orElse();
}
@@ -481,10 +482,10 @@ return $default(_that.tun,_that.mixedPort,_that.allowLan,_that.findProcessMode,_
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( Tun tun, @JsonKey(name: 'mixed-port') int mixedPort, @JsonKey(name: 'allow-lan') bool allowLan, @JsonKey(name: 'find-process-mode') FindProcessMode findProcessMode, Mode mode, @JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6, @JsonKey(name: 'tcp-concurrent') bool tcpConcurrent, @JsonKey(name: 'external-controller') ExternalControllerStatus externalController, @JsonKey(name: 'unified-delay') bool unifiedDelay, List<String> authentication, @JsonKey(name: 'geo-auto-update') bool geoAutoUpdate, @JsonKey(name: 'geo-update-interval') int geoUpdateInterval, @JsonKey(name: 'geox-url') Map<String, String> geoXUrl) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( Tun tun, @JsonKey(name: 'mixed-port') int mixedPort, @JsonKey(name: 'allow-lan') bool allowLan, @JsonKey(name: 'find-process-mode') FindProcessMode findProcessMode, Mode mode, @JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6, @JsonKey(name: 'tcp-concurrent') bool tcpConcurrent, @JsonKey(name: 'external-controller') ExternalControllerStatus externalController, String secret, @JsonKey(name: 'unified-delay') bool unifiedDelay, List<String> authentication, @JsonKey(name: 'geo-auto-update') bool geoAutoUpdate, @JsonKey(name: 'geo-update-interval') int geoUpdateInterval, @JsonKey(name: 'geox-url') Map<String, String> geoXUrl) $default,) {final _that = this;
switch (_that) {
case _UpdateParams():
return $default(_that.tun,_that.mixedPort,_that.allowLan,_that.findProcessMode,_that.mode,_that.logLevel,_that.ipv6,_that.tcpConcurrent,_that.externalController,_that.unifiedDelay,_that.authentication,_that.geoAutoUpdate,_that.geoUpdateInterval,_that.geoXUrl);case _:
return $default(_that.tun,_that.mixedPort,_that.allowLan,_that.findProcessMode,_that.mode,_that.logLevel,_that.ipv6,_that.tcpConcurrent,_that.externalController,_that.secret,_that.unifiedDelay,_that.authentication,_that.geoAutoUpdate,_that.geoUpdateInterval,_that.geoXUrl);case _:
throw StateError('Unexpected subclass');
}
@@ -501,10 +502,10 @@ return $default(_that.tun,_that.mixedPort,_that.allowLan,_that.findProcessMode,_
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( Tun tun, @JsonKey(name: 'mixed-port') int mixedPort, @JsonKey(name: 'allow-lan') bool allowLan, @JsonKey(name: 'find-process-mode') FindProcessMode findProcessMode, Mode mode, @JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6, @JsonKey(name: 'tcp-concurrent') bool tcpConcurrent, @JsonKey(name: 'external-controller') ExternalControllerStatus externalController, @JsonKey(name: 'unified-delay') bool unifiedDelay, List<String> authentication, @JsonKey(name: 'geo-auto-update') bool geoAutoUpdate, @JsonKey(name: 'geo-update-interval') int geoUpdateInterval, @JsonKey(name: 'geox-url') Map<String, String> geoXUrl)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( Tun tun, @JsonKey(name: 'mixed-port') int mixedPort, @JsonKey(name: 'allow-lan') bool allowLan, @JsonKey(name: 'find-process-mode') FindProcessMode findProcessMode, Mode mode, @JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6, @JsonKey(name: 'tcp-concurrent') bool tcpConcurrent, @JsonKey(name: 'external-controller') ExternalControllerStatus externalController, String secret, @JsonKey(name: 'unified-delay') bool unifiedDelay, List<String> authentication, @JsonKey(name: 'geo-auto-update') bool geoAutoUpdate, @JsonKey(name: 'geo-update-interval') int geoUpdateInterval, @JsonKey(name: 'geox-url') Map<String, String> geoXUrl)? $default,) {final _that = this;
switch (_that) {
case _UpdateParams() when $default != null:
return $default(_that.tun,_that.mixedPort,_that.allowLan,_that.findProcessMode,_that.mode,_that.logLevel,_that.ipv6,_that.tcpConcurrent,_that.externalController,_that.unifiedDelay,_that.authentication,_that.geoAutoUpdate,_that.geoUpdateInterval,_that.geoXUrl);case _:
return $default(_that.tun,_that.mixedPort,_that.allowLan,_that.findProcessMode,_that.mode,_that.logLevel,_that.ipv6,_that.tcpConcurrent,_that.externalController,_that.secret,_that.unifiedDelay,_that.authentication,_that.geoAutoUpdate,_that.geoUpdateInterval,_that.geoXUrl);case _:
return null;
}
@@ -516,7 +517,7 @@ return $default(_that.tun,_that.mixedPort,_that.allowLan,_that.findProcessMode,_
@JsonSerializable()
class _UpdateParams implements UpdateParams {
const _UpdateParams({required this.tun, @JsonKey(name: 'mixed-port') required this.mixedPort, @JsonKey(name: 'allow-lan') required this.allowLan, @JsonKey(name: 'find-process-mode') required this.findProcessMode, required this.mode, @JsonKey(name: 'log-level') required this.logLevel, required this.ipv6, @JsonKey(name: 'tcp-concurrent') required this.tcpConcurrent, @JsonKey(name: 'external-controller') required this.externalController, @JsonKey(name: 'unified-delay') required this.unifiedDelay, List<String> authentication = const [], @JsonKey(name: 'geo-auto-update') this.geoAutoUpdate = false, @JsonKey(name: 'geo-update-interval') this.geoUpdateInterval = 24, @JsonKey(name: 'geox-url') Map<String, String> geoXUrl = const {}}): _authentication = authentication,_geoXUrl = geoXUrl;
const _UpdateParams({required this.tun, @JsonKey(name: 'mixed-port') required this.mixedPort, @JsonKey(name: 'allow-lan') required this.allowLan, @JsonKey(name: 'find-process-mode') required this.findProcessMode, required this.mode, @JsonKey(name: 'log-level') required this.logLevel, required this.ipv6, @JsonKey(name: 'tcp-concurrent') required this.tcpConcurrent, @JsonKey(name: 'external-controller') required this.externalController, this.secret = '', @JsonKey(name: 'unified-delay') required this.unifiedDelay, List<String> authentication = const [], @JsonKey(name: 'geo-auto-update') this.geoAutoUpdate = false, @JsonKey(name: 'geo-update-interval') this.geoUpdateInterval = 24, @JsonKey(name: 'geox-url') Map<String, String> geoXUrl = const {}}): _authentication = authentication,_geoXUrl = geoXUrl;
factory _UpdateParams.fromJson(Map<String, dynamic> json) => _$UpdateParamsFromJson(json);
@override final Tun tun;
@@ -528,6 +529,7 @@ class _UpdateParams implements UpdateParams {
@override final bool ipv6;
@override@JsonKey(name: 'tcp-concurrent') final bool tcpConcurrent;
@override@JsonKey(name: 'external-controller') final ExternalControllerStatus externalController;
@override@JsonKey() final String secret;
@override@JsonKey(name: 'unified-delay') final bool unifiedDelay;
final List<String> _authentication;
@override@JsonKey() List<String> get authentication {
@@ -559,18 +561,18 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _UpdateParams&&(identical(other.tun, tun) || other.tun == tun)&&(identical(other.mixedPort, mixedPort) || other.mixedPort == mixedPort)&&(identical(other.allowLan, allowLan) || other.allowLan == allowLan)&&(identical(other.findProcessMode, findProcessMode) || other.findProcessMode == findProcessMode)&&(identical(other.mode, mode) || other.mode == mode)&&(identical(other.logLevel, logLevel) || other.logLevel == logLevel)&&(identical(other.ipv6, ipv6) || other.ipv6 == ipv6)&&(identical(other.tcpConcurrent, tcpConcurrent) || other.tcpConcurrent == tcpConcurrent)&&(identical(other.externalController, externalController) || other.externalController == externalController)&&(identical(other.unifiedDelay, unifiedDelay) || other.unifiedDelay == unifiedDelay)&&const DeepCollectionEquality().equals(other.authentication, _authentication)&&(identical(other.geoAutoUpdate, geoAutoUpdate) || other.geoAutoUpdate == geoAutoUpdate)&&(identical(other.geoUpdateInterval, geoUpdateInterval) || other.geoUpdateInterval == geoUpdateInterval)&&const DeepCollectionEquality().equals(other.geoXUrl, _geoXUrl));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _UpdateParams&&(identical(other.tun, tun) || other.tun == tun)&&(identical(other.mixedPort, mixedPort) || other.mixedPort == mixedPort)&&(identical(other.allowLan, allowLan) || other.allowLan == allowLan)&&(identical(other.findProcessMode, findProcessMode) || other.findProcessMode == findProcessMode)&&(identical(other.mode, mode) || other.mode == mode)&&(identical(other.logLevel, logLevel) || other.logLevel == logLevel)&&(identical(other.ipv6, ipv6) || other.ipv6 == ipv6)&&(identical(other.tcpConcurrent, tcpConcurrent) || other.tcpConcurrent == tcpConcurrent)&&(identical(other.externalController, externalController) || other.externalController == externalController)&&(identical(other.secret, secret) || other.secret == secret)&&(identical(other.unifiedDelay, unifiedDelay) || other.unifiedDelay == unifiedDelay)&&const DeepCollectionEquality().equals(other.authentication, _authentication)&&(identical(other.geoAutoUpdate, geoAutoUpdate) || other.geoAutoUpdate == geoAutoUpdate)&&(identical(other.geoUpdateInterval, geoUpdateInterval) || other.geoUpdateInterval == geoUpdateInterval)&&const DeepCollectionEquality().equals(other.geoXUrl, _geoXUrl));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode {
return Object.hash(runtimeType,tun,mixedPort,allowLan,findProcessMode,mode,logLevel,ipv6,tcpConcurrent,externalController,unifiedDelay,const DeepCollectionEquality().hash(_authentication),geoAutoUpdate,geoUpdateInterval,const DeepCollectionEquality().hash(_geoXUrl));
return Object.hash(runtimeType,tun,mixedPort,allowLan,findProcessMode,mode,logLevel,ipv6,tcpConcurrent,externalController,secret,unifiedDelay,const DeepCollectionEquality().hash(_authentication),geoAutoUpdate,geoUpdateInterval,const DeepCollectionEquality().hash(_geoXUrl));
}
@override
String toString() {
return 'UpdateParams(tun: $tun, mixedPort: $mixedPort, allowLan: $allowLan, findProcessMode: $findProcessMode, mode: $mode, logLevel: $logLevel, ipv6: $ipv6, tcpConcurrent: $tcpConcurrent, externalController: $externalController, unifiedDelay: $unifiedDelay, authentication: $authentication, geoAutoUpdate: $geoAutoUpdate, geoUpdateInterval: $geoUpdateInterval, geoXUrl: $geoXUrl)';
return 'UpdateParams(tun: $tun, mixedPort: $mixedPort, allowLan: $allowLan, findProcessMode: $findProcessMode, mode: $mode, logLevel: $logLevel, ipv6: $ipv6, tcpConcurrent: $tcpConcurrent, externalController: $externalController, secret: $secret, unifiedDelay: $unifiedDelay, authentication: $authentication, geoAutoUpdate: $geoAutoUpdate, geoUpdateInterval: $geoUpdateInterval, geoXUrl: $geoXUrl)';
}
@@ -581,7 +583,7 @@ abstract mixin class _$UpdateParamsCopyWith<$Res> implements $UpdateParamsCopyWi
factory _$UpdateParamsCopyWith(_UpdateParams value, $Res Function(_UpdateParams) _then) = __$UpdateParamsCopyWithImpl;
@override @useResult
$Res call({
Tun tun,@JsonKey(name: 'mixed-port') int mixedPort,@JsonKey(name: 'allow-lan') bool allowLan,@JsonKey(name: 'find-process-mode') FindProcessMode findProcessMode, Mode mode,@JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6,@JsonKey(name: 'tcp-concurrent') bool tcpConcurrent,@JsonKey(name: 'external-controller') ExternalControllerStatus externalController,@JsonKey(name: 'unified-delay') bool unifiedDelay, List<String> authentication,@JsonKey(name: 'geo-auto-update') bool geoAutoUpdate,@JsonKey(name: 'geo-update-interval') int geoUpdateInterval,@JsonKey(name: 'geox-url') Map<String, String> geoXUrl
Tun tun,@JsonKey(name: 'mixed-port') int mixedPort,@JsonKey(name: 'allow-lan') bool allowLan,@JsonKey(name: 'find-process-mode') FindProcessMode findProcessMode, Mode mode,@JsonKey(name: 'log-level') LogLevel logLevel, bool ipv6,@JsonKey(name: 'tcp-concurrent') bool tcpConcurrent,@JsonKey(name: 'external-controller') ExternalControllerStatus externalController, String secret,@JsonKey(name: 'unified-delay') bool unifiedDelay, List<String> authentication,@JsonKey(name: 'geo-auto-update') bool geoAutoUpdate,@JsonKey(name: 'geo-update-interval') int geoUpdateInterval,@JsonKey(name: 'geox-url') Map<String, String> geoXUrl
});
@@ -598,7 +600,7 @@ class __$UpdateParamsCopyWithImpl<$Res>
/// Create a copy of UpdateParams
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? tun = null,Object? mixedPort = null,Object? allowLan = null,Object? findProcessMode = null,Object? mode = null,Object? logLevel = null,Object? ipv6 = null,Object? tcpConcurrent = null,Object? externalController = null,Object? unifiedDelay = null,Object? authentication = null,Object? geoAutoUpdate = null,Object? geoUpdateInterval = null,Object? geoXUrl = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? tun = null,Object? mixedPort = null,Object? allowLan = null,Object? findProcessMode = null,Object? mode = null,Object? logLevel = null,Object? ipv6 = null,Object? tcpConcurrent = null,Object? externalController = null,Object? secret = null,Object? unifiedDelay = null,Object? authentication = null,Object? geoAutoUpdate = null,Object? geoUpdateInterval = null,Object? geoXUrl = null,}) {
return _then(_UpdateParams(
tun: null == tun ? _self.tun : tun // ignore: cast_nullable_to_non_nullable
as Tun,mixedPort: null == mixedPort ? _self.mixedPort : mixedPort // ignore: cast_nullable_to_non_nullable
@@ -609,7 +611,8 @@ as Mode,logLevel: null == logLevel ? _self.logLevel : logLevel // ignore: cast_n
as LogLevel,ipv6: null == ipv6 ? _self.ipv6 : ipv6 // ignore: cast_nullable_to_non_nullable
as bool,tcpConcurrent: null == tcpConcurrent ? _self.tcpConcurrent : tcpConcurrent // ignore: cast_nullable_to_non_nullable
as bool,externalController: null == externalController ? _self.externalController : externalController // ignore: cast_nullable_to_non_nullable
as ExternalControllerStatus,unifiedDelay: null == unifiedDelay ? _self.unifiedDelay : unifiedDelay // ignore: cast_nullable_to_non_nullable
as ExternalControllerStatus,secret: null == secret ? _self.secret : secret // ignore: cast_nullable_to_non_nullable
as String,unifiedDelay: null == unifiedDelay ? _self.unifiedDelay : unifiedDelay // ignore: cast_nullable_to_non_nullable
as bool,authentication: null == authentication ? _self._authentication : authentication // ignore: cast_nullable_to_non_nullable
as List<String>,geoAutoUpdate: null == geoAutoUpdate ? _self.geoAutoUpdate : geoAutoUpdate // ignore: cast_nullable_to_non_nullable
as bool,geoUpdateInterval: null == geoUpdateInterval ? _self.geoUpdateInterval : geoUpdateInterval // ignore: cast_nullable_to_non_nullable
+2
View File
@@ -34,6 +34,7 @@ _UpdateParams _$UpdateParamsFromJson(Map<String, dynamic> json) =>
_$ExternalControllerStatusEnumMap,
json['external-controller'],
),
secret: json['secret'] as String? ?? '',
unifiedDelay: json['unified-delay'] as bool,
authentication:
(json['authentication'] as List<dynamic>?)
@@ -61,6 +62,7 @@ Map<String, dynamic> _$UpdateParamsToJson(_UpdateParams instance) =>
'tcp-concurrent': instance.tcpConcurrent,
'external-controller':
_$ExternalControllerStatusEnumMap[instance.externalController]!,
'secret': instance.secret,
'unified-delay': instance.unifiedDelay,
'authentication': instance.authentication,
'geo-auto-update': instance.geoAutoUpdate,
+3
View File
@@ -493,6 +493,9 @@ class SetupAction extends _$SetupAction {
tun: patchConfig.tun.copyWith(
enable: patchConfig.tun.enable && authorized,
),
externalController: patchConfig.secret.isEmpty
? ExternalControllerStatus.close
: patchConfig.externalController,
);
}
+2 -1
View File
@@ -156,7 +156,8 @@ void writeConfig(Ref ref, Config config) {
ref.read(hotKeyActionsProvider.notifier).value = config.hotKeyActions;
ref.read(proxiesStyleSettingProvider.notifier).value =
config.proxiesStyleProps;
ref.read(patchClashConfigProvider.notifier).value = config.patchClashConfig;
ref.read(patchClashConfigProvider.notifier).value = config.patchClashConfig
.ensureControllerSecret();
ref.read(excludeSSIDsProvider.notifier).value = config.excludeSSIDs;
}
+38 -7
View File
@@ -238,6 +238,23 @@ class AuthenticationPasswordItem extends ConsumerWidget {
}
}
class ExternalControllerSecretItem extends ConsumerWidget {
const ExternalControllerSecretItem({super.key});
@override
Widget build(BuildContext context, ref) {
return ConfigTextItem(
title: (l) => l.externalControllerSecret,
maxLength: TextInputLimits.password,
selector: patchClashConfigProvider.select((state) => state.secret),
normalize: (value) => value.trim(),
onChanged: (ref, value) => ref
.read(patchClashConfigProvider.notifier)
.update((state) => state.copyWith(secret: value)),
);
}
}
ConfigToggleItem _clashToggle({
required ConfigLabel title,
ConfigLabel? subtitle,
@@ -322,7 +339,10 @@ class GeneralView extends ConsumerWidget {
];
}
List<Widget> _inboundItems(bool authentication) {
List<Widget> _inboundItems({
required bool authentication,
required bool externalController,
}) {
return [
const PortItem(),
_clashToggle(
@@ -335,12 +355,15 @@ class GeneralView extends ConsumerWidget {
subtitle: (l) => l.externalControllerDesc,
select: (state) =>
state.externalController == ExternalControllerStatus.open,
update: (state, value) => state.copyWith(
externalController: value
? ExternalControllerStatus.open
: ExternalControllerStatus.close,
),
update: (state, value) => state
.copyWith(
externalController: value
? ExternalControllerStatus.open
: ExternalControllerStatus.close,
)
.ensureControllerSecret(),
),
if (externalController) const ExternalControllerSecretItem(),
const AuthenticationItem(),
if (authentication) ...const [
AuthenticationAccountItem(),
@@ -456,6 +479,11 @@ class GeneralView extends ConsumerWidget {
final authentication = ref.watch(
networkSettingProvider.select((state) => state.authentication.enable),
);
final externalController = ref.watch(
patchClashConfigProvider.select(
(state) => state.externalController == ExternalControllerStatus.open,
),
);
return BaseScaffold(
title: appLocalizations.general,
body: ListView(
@@ -473,7 +501,10 @@ class GeneralView extends ConsumerWidget {
),
generateSectionV3(
title: appLocalizations.inbound,
items: _inboundItems(authentication),
items: _inboundItems(
authentication: authentication,
externalController: externalController,
),
),
generateSectionV3(
title: appLocalizations.connection,
+3
View File
@@ -18,9 +18,12 @@ once_cell = "1.20.2"
sha2 = "0.10.8"
[target.'cfg(windows)'.dependencies]
tokio-stream = { version = "0.1", default-features = false }
windows-sys = { version = "0.52.0", features = [
"Win32_Foundation",
"Win32_NetworkManagement_IpHelper",
"Win32_Security",
"Win32_Security_Authorization",
"Win32_Storage_FileSystem",
"Win32_System_JobObjects",
"Win32_System_Threading",
+143 -14
View File
@@ -9,7 +9,10 @@ use std::fs::{File, OpenOptions};
not(target_os = "linux")
))]
use std::future::pending;
#[cfg(not(target_os = "linux"))]
#[cfg(not(any(
all(feature = "windows-service", target_os = "windows"),
target_os = "linux"
)))]
use std::future::Future;
use std::io::{BufRead, Error, Read};
#[cfg(windows)]
@@ -35,7 +38,7 @@ use windows_sys::Win32::System::JobObjects::{
};
#[cfg(not(target_os = "linux"))]
const LISTEN_PORT: u16 = 47890;
pub(super) const LISTEN_PORT: u16 = 47890;
#[cfg(not(target_os = "linux"))]
const CORE_PIPE_PREFIX: &str = r"\\.\pipe\FlClashCore_";
#[cfg(target_os = "linux")]
@@ -43,9 +46,13 @@ const CORE_SOCKET_PREFIX: &str = "/tmp/FlClashSocket_";
#[cfg(target_os = "linux")]
const CORE_SOCKET_SUFFIX: &str = ".sock";
const PROTOCOL_VERSION_HEADER: &str = "x-flclash-helper-protocol";
#[cfg(windows)]
const PROTOCOL_VERSION: &str = "8";
#[cfg(not(windows))]
const PROTOCOL_VERSION: &str = "6";
const EXPECTED_CORE_SHA256: &str = env!("CORE_SHA256");
const LOG_CAPACITY: usize = 100;
const MAX_REQUEST_BODY_BYTES: u64 = 4096;
const CORE_EXIT_TIMEOUT: Duration = Duration::from_millis(1500);
const CORE_GRACEFUL_EXIT_TIMEOUT: Duration = Duration::from_millis(3000);
const CORE_EXIT_POLL_INTERVAL: Duration = Duration::from_millis(20);
@@ -407,6 +414,10 @@ fn start(start_params: StartParams) -> warp::reply::Response {
StatusCode::BAD_REQUEST,
);
}
#[cfg(target_os = "linux")]
if let Err(error) = super::linux::ensure_owner_socket(&start_params.address) {
return error_response("invalidRequest", error.to_string(), StatusCode::BAD_REQUEST);
}
let mut managed = lock_surviving_poison(&MANAGED_CORE);
if let Err(error) = release_managed_core(&mut managed) {
@@ -429,10 +440,6 @@ fn start(start_params: StartParams) -> warp::reply::Response {
)
}
};
#[cfg(target_os = "linux")]
if let Err(error) = super::linux::ensure_owner_socket(&start_params.address) {
return error_response("invalidRequest", error.to_string(), StatusCode::BAD_REQUEST);
}
match core.spawn(&start_params.address) {
Ok(mut child) => {
@@ -631,6 +638,18 @@ async fn ping_request(ping_params: PingParams) -> Result<warp::reply::Response,
}
async fn handle_rejection(rejection: Rejection) -> Result<warp::reply::Response, Infallible> {
if rejection.find::<ForbiddenClient>().is_some() {
return Ok(warp::reply::with_header(
error_response(
"invalidRequest",
"browser origins and non-local hosts are not allowed",
StatusCode::FORBIDDEN,
),
"connection",
"close",
)
.into_response());
}
if rejection.find::<warp::reject::InvalidQuery>().is_some() {
return Ok(warp::reply::with_header(
error_response(
@@ -653,6 +672,25 @@ async fn handle_rejection(rejection: Rejection) -> Result<warp::reply::Response,
StatusCode::BAD_REQUEST,
));
}
if rejection.find::<warp::reject::PayloadTooLarge>().is_some() {
return Ok(warp::reply::with_header(
error_response(
"invalidRequest",
"request body exceeds 4096 bytes",
StatusCode::PAYLOAD_TOO_LARGE,
),
"connection",
"close",
)
.into_response());
}
if rejection.find::<warp::reject::LengthRequired>().is_some() {
return Ok(error_response(
"invalidRequest",
"request body length is required",
StatusCode::LENGTH_REQUIRED,
));
}
if rejection.is_not_found() {
return Ok(error_response(
"notFound",
@@ -660,6 +698,13 @@ async fn handle_rejection(rejection: Rejection) -> Result<warp::reply::Response,
StatusCode::NOT_FOUND,
));
}
if rejection.find::<warp::reject::InvalidHeader>().is_some() {
return Ok(error_response(
"invalidRequest",
"invalid request header",
StatusCode::BAD_REQUEST,
));
}
if rejection.find::<warp::reject::MethodNotAllowed>().is_some() {
return Ok(error_response(
"invalidRequest",
@@ -674,7 +719,29 @@ async fn handle_rejection(rejection: Rejection) -> Result<warp::reply::Response,
))
}
#[derive(Debug)]
struct ForbiddenClient;
impl warp::reject::Reject for ForbiddenClient {}
pub(super) fn routes() -> impl Filter<Extract = (impl Reply,), Error = Infallible> + Clone {
let native_client = warp::header::optional::<String>("origin")
.and(warp::header::optional::<String>("host"))
.and_then(|origin: Option<String>, host: Option<String>| async move {
let local_host = host.is_none_or(|host| {
host.parse::<warp::http::uri::Authority>()
.is_ok_and(|authority| {
["127.0.0.1", "localhost", "FlClashHelperService"]
.iter()
.any(|host| authority.host().eq_ignore_ascii_case(host))
})
});
if origin.is_some() || !local_host {
return Err(warp::reject::custom(ForbiddenClient));
}
Ok(())
})
.untuple_one();
// Matching the path before the method keeps an unknown path rejecting as
// "not found" instead of the method mismatch another endpoint reports.
let api_ping = warp::path("ping")
@@ -686,12 +753,14 @@ pub(super) fn routes() -> impl Filter<Extract = (impl Reply,), Error = Infallibl
let api_start = warp::path("start")
.and(warp::path::end())
.and(warp::post())
.and(warp::body::content_length_limit(MAX_REQUEST_BODY_BYTES))
.and(warp::body::json())
.and_then(start_request);
let api_stop = warp::path("stop")
.and(warp::path::end())
.and(warp::post())
.and(warp::body::content_length_limit(MAX_REQUEST_BODY_BYTES))
.and(warp::body::json())
.and_then(stop_request);
@@ -700,10 +769,8 @@ pub(super) fn routes() -> impl Filter<Extract = (impl Reply,), Error = Infallibl
.and(warp::get())
.map(get_logs);
api_ping
.or(api_start)
.or(api_stop)
.or(api_logs)
native_client
.and(api_ping.or(api_start).or(api_stop).or(api_logs))
.recover(handle_rejection)
}
@@ -731,8 +798,11 @@ pub async fn run_service() -> anyhow::Result<()> {
run_service_until(pending(), || Ok(())).await
}
#[cfg(not(target_os = "linux"))]
pub(super) async fn run_service_until<F, S>(shutdown: F, on_started: S) -> anyhow::Result<()>
#[cfg(not(any(
all(feature = "windows-service", target_os = "windows"),
target_os = "linux"
)))]
async fn run_service_until<F, S>(shutdown: F, on_started: S) -> anyhow::Result<()>
where
F: Future<Output = ()> + Send + 'static,
S: FnOnce() -> anyhow::Result<()>,
@@ -809,8 +879,8 @@ mod tests {
}
#[test]
fn protocol_6_uses_lowercase_session_ownership() {
assert_eq!(PROTOCOL_VERSION, "6");
fn protocol_uses_lowercase_session_ownership() {
assert_eq!(PROTOCOL_VERSION, if cfg!(windows) { "8" } else { "6" });
assert!(is_valid_session_id("0123456789abcdef0123456789abcdef"));
assert!(!is_valid_session_id("ABCDEF0123456789abcdef0123456789"));
assert!(!is_valid_session_id("0123456789abcdef"));
@@ -1236,6 +1306,7 @@ mod tests {
let response = warp::test::request()
.method("POST")
.path("/stop")
.body("")
.reply(&routes())
.await;
@@ -1267,6 +1338,64 @@ mod tests {
);
}
#[tokio::test]
async fn browser_origins_and_rebound_hosts_cannot_reach_any_endpoint() {
for (method, path) in [
("GET", "/ping"),
("GET", "/logs"),
("POST", "/start"),
("POST", "/stop"),
] {
for (header, value) in [
("origin", "https://attacker.test"),
("origin", "null"),
("host", "attacker.test:47890"),
] {
let response = warp::test::request()
.method(method)
.path(path)
.header(header, value)
.reply(&routes())
.await;
assert_eq!(response.status(), StatusCode::FORBIDDEN);
assert!(response
.headers()
.get("access-control-allow-origin")
.is_none());
}
}
for host in ["127.0.0.1:47890", "localhost:47890", "FlClashHelperService"] {
let response = warp::test::request()
.path("/logs")
.header("host", host)
.reply(&routes())
.await;
assert_eq!(response.status(), StatusCode::OK);
}
}
#[tokio::test]
async fn state_changes_are_size_limited() {
let oversized = " ".repeat(MAX_REQUEST_BODY_BYTES as usize + 1);
for path in ["/start", "/stop"] {
let response = warp::test::request()
.method("POST")
.path(path)
.body(&oversized)
.reply(&routes())
.await;
assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE);
assert_eq!(response.headers()["connection"], "close");
let response = warp::test::request()
.method("POST")
.path(path)
.reply(&routes())
.await;
assert_eq!(response.status(), StatusCode::LENGTH_REQUIRED);
}
}
#[cfg(not(target_os = "linux"))]
#[test]
fn only_accepts_random_core_pipe_namespace() {
+4
View File
@@ -1,5 +1,9 @@
pub mod hub;
#[cfg(target_os = "linux")]
pub mod linux;
#[cfg(any(all(feature = "windows-service", target_os = "windows"), test))]
pub mod owner;
#[cfg(all(feature = "windows-service", target_os = "windows"))]
pub mod peer;
#[cfg(all(feature = "windows-service", target_os = "windows"))]
pub mod windows;
+231
View File
@@ -0,0 +1,231 @@
use anyhow::{anyhow, bail, Result};
use std::ffi::{OsStr, OsString};
use std::net::SocketAddrV4;
const OWNER_SID_FLAG: &str = "--owner-sid";
const OWNER_PID_FLAG: &str = "--owner-pid";
const MAX_SID_SUBAUTHORITIES: usize = 15;
#[derive(Debug, PartialEq, Eq)]
pub enum ServiceCommand {
Run { owner_sid: String },
Install { owner_pid: u32 },
Uninstall,
}
/// The service is registered with its owner on the command line, which is how a
/// start by the service manager learns whom to answer. Starting without one
/// fails rather than serve anybody.
pub fn service_command(args: impl IntoIterator<Item = OsString>) -> Result<ServiceCommand> {
let args: Vec<OsString> = args.into_iter().collect();
let args: Vec<&OsStr> = args.iter().map(OsString::as_os_str).collect();
match args.as_slice() {
[] => bail!("the helper service was registered without an owner; reinstall it"),
[flag, sid] if *flag == OsStr::new(OWNER_SID_FLAG) => {
let sid = sid
.to_str()
.filter(|sid| is_sid_string(sid))
.ok_or_else(|| anyhow!("{OWNER_SID_FLAG} is not a SID"))?;
Ok(ServiceCommand::Run {
owner_sid: sid.to_string(),
})
}
[command] if *command == OsStr::new("uninstall") => Ok(ServiceCommand::Uninstall),
[command, flag, pid]
if *command == OsStr::new("install") && *flag == OsStr::new(OWNER_PID_FLAG) =>
{
let owner_pid = pid
.to_str()
.and_then(|pid| pid.parse::<u32>().ok())
.filter(|pid| *pid != 0)
.ok_or_else(|| anyhow!("{OWNER_PID_FLAG} is not a process ID"))?;
Ok(ServiceCommand::Install { owner_pid })
}
[command, ..] if *command == OsStr::new("install") => {
bail!("install takes exactly {OWNER_PID_FLAG} <pid>")
}
[value, ..] => bail!("unknown helper command: {}", value.to_string_lossy()),
}
}
pub fn owner_sid_arguments(owner_sid: &str) -> Vec<OsString> {
vec![OsString::from(OWNER_SID_FLAG), OsString::from(owner_sid)]
}
fn is_sid_string(value: &str) -> bool {
let Some(rest) = value.strip_prefix("S-1-") else {
return false;
};
let parts: Vec<&str> = rest.split('-').collect();
parts.len() <= MAX_SID_SUBAUTHORITIES + 1
&& parts
.iter()
.all(|part| !part.is_empty() && part.bytes().all(|byte| byte.is_ascii_digit()))
}
pub fn same_sid(left: &str, right: &str) -> bool {
left.eq_ignore_ascii_case(right)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TcpConnection {
pub local: SocketAddrV4,
pub remote: SocketAddrV4,
pub pid: u32,
}
/// Both ends of a loopback connection are in the table, each owned by its own
/// process; the client's end is the one whose local address is the peer address
/// the Helper accepted.
pub fn client_pid(
connections: &[TcpConnection],
client: SocketAddrV4,
server: SocketAddrV4,
) -> Option<u32> {
connections
.iter()
.find(|connection| connection.local == client && connection.remote == server)
.map(|connection| connection.pid)
.filter(|pid| *pid != 0)
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::Ipv4Addr;
const SID: &str = "S-1-5-21-1004336348-1177238915-682003330-1001";
fn os(values: &[&str]) -> Vec<OsString> {
values.iter().map(OsString::from).collect()
}
fn address(port: u16) -> SocketAddrV4 {
SocketAddrV4::new(Ipv4Addr::LOCALHOST, port)
}
#[test]
fn parses_the_service_commands() {
assert_eq!(
service_command(os(&["--owner-sid", SID])).unwrap(),
ServiceCommand::Run {
owner_sid: SID.to_string()
}
);
assert_eq!(
service_command(os(&["install", "--owner-pid", "4242"])).unwrap(),
ServiceCommand::Install { owner_pid: 4242 }
);
assert_eq!(
service_command(os(&["uninstall"])).unwrap(),
ServiceCommand::Uninstall
);
}
#[test]
fn refuses_to_run_without_an_owner() {
assert!(service_command(os(&[])).is_err());
assert!(service_command(os(&["--owner-sid"])).is_err());
assert!(service_command(os(&["--owner-sid", "S-1-5-18", "extra"])).is_err());
}
#[test]
fn rejects_an_owner_that_is_not_a_sid() {
for value in [
"",
"alice",
"S-1-",
"S-1-5-",
"S-1-5-x",
"S-2-5-18",
"S-1-5-18 ",
] {
assert!(
service_command(os(&["--owner-sid", value])).is_err(),
"{value:?}"
);
}
let long = format!("S-1-{}", vec!["5"; 17].join("-"));
assert!(service_command(os(&["--owner-sid", &long])).is_err());
}
#[test]
fn install_needs_a_real_owner_process() {
assert!(service_command(os(&["install"])).is_err());
assert!(service_command(os(&["install", "--owner-pid"])).is_err());
assert!(service_command(os(&["install", "--owner-pid", "0"])).is_err());
assert!(service_command(os(&["install", "--owner-pid", "-1"])).is_err());
assert!(service_command(os(&["install", "--owner-pid", "abc"])).is_err());
assert!(service_command(os(&["install", "--owner-pid", "7", "extra"])).is_err());
assert!(service_command(os(&["install", "7"])).is_err());
}
#[test]
fn rejects_unknown_or_extra_commands() {
assert!(service_command(os(&["unknown"])).is_err());
assert!(service_command(os(&["uninstall", "extra"])).is_err());
}
#[test]
fn registration_arguments_read_back_as_the_same_owner() {
let arguments = owner_sid_arguments(SID);
assert_eq!(
service_command(arguments).unwrap(),
ServiceCommand::Run {
owner_sid: SID.to_string()
}
);
}
#[test]
fn compares_sids_regardless_of_case() {
assert!(same_sid("S-1-5-21-1-2-3-1001", "s-1-5-21-1-2-3-1001"));
assert!(!same_sid("S-1-5-21-1-2-3-1001", "S-1-5-21-1-2-3-1002"));
}
#[test]
fn finds_the_process_on_the_client_end_of_the_connection() {
let server = address(47890);
let client = address(50123);
let connections = [
TcpConnection {
local: server,
remote: client,
pid: 700,
},
TcpConnection {
local: client,
remote: server,
pid: 4242,
},
TcpConnection {
local: address(50124),
remote: server,
pid: 9999,
},
];
assert_eq!(client_pid(&connections, client, server), Some(4242));
}
#[test]
fn finds_nobody_when_the_connection_is_gone_or_unowned() {
let server = address(47890);
let client = address(50123);
let helper_end_only = [TcpConnection {
local: server,
remote: client,
pid: 700,
}];
let unowned = [TcpConnection {
local: client,
remote: server,
pid: 0,
}];
assert_eq!(client_pid(&helper_end_only, client, server), None);
assert_eq!(client_pid(&unowned, client, server), None);
assert_eq!(client_pid(&[], client, server), None);
}
}
+275
View File
@@ -0,0 +1,275 @@
use crate::service::hub::{
ensure_core_sha256_configured, log_message, release_managed_core_on_shutdown, routes,
LISTEN_PORT,
};
use crate::service::owner::{client_pid, same_sid, TcpConnection};
use std::ffi::c_void;
use std::future::Future;
use std::io::{Error, Result as IoResult};
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
use std::pin::Pin;
use std::ptr::null_mut;
use std::sync::atomic::{AtomicBool, Ordering};
use std::task::{Context, Poll};
use std::time::Duration;
use tokio::net::{TcpListener, TcpStream};
use tokio::time::Sleep;
use tokio_stream::Stream;
use windows_sys::Win32::Foundation::{
CloseHandle, LocalFree, ERROR_INSUFFICIENT_BUFFER, HANDLE, NO_ERROR,
};
use windows_sys::Win32::NetworkManagement::IpHelper::{
GetExtendedTcpTable, MIB_TCPTABLE_OWNER_PID, TCP_TABLE_OWNER_PID_ALL,
};
use windows_sys::Win32::Security::Authorization::ConvertSidToStringSidW;
use windows_sys::Win32::Security::{GetTokenInformation, TokenUser, TOKEN_QUERY, TOKEN_USER};
use windows_sys::Win32::System::Threading::{
OpenProcess, OpenProcessToken, PROCESS_QUERY_LIMITED_INFORMATION,
};
const AF_INET: u32 = 2;
const TABLE_ATTEMPTS: usize = 5;
const ACCEPT_RETRY_DELAY: Duration = Duration::from_secs(1);
static REJECTION_LOGGED: AtomicBool = AtomicBool::new(false);
struct Handle(HANDLE);
impl Drop for Handle {
fn drop(&mut self) {
// SAFETY: the handle came from a successful Open* call.
unsafe {
CloseHandle(self.0);
}
}
}
pub fn process_sid(pid: u32) -> IoResult<String> {
// SAFETY: both handles are owned by a Handle; the u64 buffer gives the
// pointer-bearing TOKEN_USER the alignment it needs.
unsafe {
let process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
if process == 0 {
return Err(Error::last_os_error());
}
let process = Handle(process);
let mut token: HANDLE = 0;
if OpenProcessToken(process.0, TOKEN_QUERY, &mut token) == 0 {
return Err(Error::last_os_error());
}
let token = Handle(token);
let mut length = 0u32;
GetTokenInformation(token.0, TokenUser, null_mut(), 0, &mut length);
if length == 0 {
return Err(Error::last_os_error());
}
let mut buffer = vec![0u64; (length as usize).div_ceil(8)];
if GetTokenInformation(
token.0,
TokenUser,
buffer.as_mut_ptr() as *mut c_void,
length,
&mut length,
) == 0
{
return Err(Error::last_os_error());
}
let user = &*(buffer.as_ptr() as *const TOKEN_USER);
sid_to_string(user.User.Sid)
}
}
unsafe fn sid_to_string(sid: *mut c_void) -> IoResult<String> {
let mut wide: *mut u16 = null_mut();
if ConvertSidToStringSidW(sid, &mut wide) == 0 {
return Err(Error::last_os_error());
}
let mut length = 0;
while *wide.add(length) != 0 {
length += 1;
}
let value = String::from_utf16_lossy(std::slice::from_raw_parts(wide, length));
LocalFree(wide as *mut c_void);
Ok(value)
}
fn tcp_connections() -> IoResult<Vec<TcpConnection>> {
let mut size = 0u32;
for _ in 0..TABLE_ATTEMPTS {
let mut buffer = vec![0u32; (size as usize).div_ceil(4)];
let pointer = if buffer.is_empty() {
null_mut()
} else {
buffer.as_mut_ptr() as *mut c_void
};
let status = unsafe {
GetExtendedTcpTable(pointer, &mut size, 0, AF_INET, TCP_TABLE_OWNER_PID_ALL, 0)
};
match status {
NO_ERROR => return Ok(read_table(&buffer)),
ERROR_INSUFFICIENT_BUFFER => continue,
other => return Err(Error::from_raw_os_error(other as i32)),
}
}
Err(Error::other("the TCP table kept growing"))
}
fn read_table(buffer: &[u32]) -> Vec<TcpConnection> {
// SAFETY: a successful call left `dwNumEntries` rows in the buffer it was
// sized for.
unsafe {
let table = &*(buffer.as_ptr() as *const MIB_TCPTABLE_OWNER_PID);
std::slice::from_raw_parts(table.table.as_ptr(), table.dwNumEntries as usize)
.iter()
.map(|row| TcpConnection {
local: socket_address(row.dwLocalAddr, row.dwLocalPort),
remote: socket_address(row.dwRemoteAddr, row.dwRemotePort),
pid: row.dwOwningPid,
})
.collect()
}
}
/// Network byte order, with the port in the low 16 bits.
fn socket_address(address: u32, port: u32) -> SocketAddrV4 {
SocketAddrV4::new(
Ipv4Addr::from(u32::from_be(address)),
u16::from_be(port as u16),
)
}
fn is_owner_connection(stream: &TcpStream, owner_sid: &str) -> bool {
let (Ok(SocketAddr::V4(client)), Ok(SocketAddr::V4(server))) =
(stream.peer_addr(), stream.local_addr())
else {
return false;
};
let Ok(connections) = tcp_connections() else {
return false;
};
let Some(pid) = client_pid(&connections, client, server) else {
return false;
};
matches!(process_sid(pid), Ok(sid) if same_sid(&sid, owner_sid))
}
/// Loopback TCP has no peer credential, so the TCP table names the process
/// holding the client end, and that process must run as the account the Helper
/// was installed for.
struct AuthorizedIncoming {
listener: TcpListener,
owner_sid: String,
retry: Option<Pin<Box<Sleep>>>,
}
impl Stream for AuthorizedIncoming {
type Item = IoResult<TcpStream>;
/// hyper ends the whole server on an accept error, so retry after a pause.
fn poll_next(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Self::Item>> {
loop {
if let Some(retry) = self.retry.as_mut() {
match retry.as_mut().poll(context) {
Poll::Ready(()) => self.retry = None,
Poll::Pending => return Poll::Pending,
}
}
match self.listener.poll_accept(context) {
Poll::Ready(Ok((stream, _))) => {
if is_owner_connection(&stream, &self.owner_sid) {
return Poll::Ready(Some(Ok(stream)));
}
if !REJECTION_LOGGED.swap(true, Ordering::Relaxed) {
log_message(
"Helper refused a connection from an account other than its owner"
.to_string(),
);
}
}
Poll::Ready(Err(error)) => {
log_message(format!("Helper could not accept a connection: {error}"));
self.retry = Some(Box::pin(tokio::time::sleep(ACCEPT_RETRY_DELAY)));
}
Poll::Pending => return Poll::Pending,
}
}
}
}
pub(super) async fn serve_until<F, S>(
owner_sid: String,
shutdown: F,
on_started: S,
) -> anyhow::Result<()>
where
F: Future<Output = ()> + Send + 'static,
S: FnOnce() -> anyhow::Result<()>,
{
ensure_core_sha256_configured()?;
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, LISTEN_PORT))
.await
.map_err(|error| anyhow::anyhow!("bind helper server: {error}"))?;
on_started()?;
let incoming = AuthorizedIncoming {
listener,
owner_sid,
retry: None,
};
warp::serve(routes())
.serve_incoming_with_graceful_shutdown(incoming, shutdown)
.await;
release_managed_core_on_shutdown();
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn v4(address: SocketAddr) -> SocketAddrV4 {
match address {
SocketAddr::V4(address) => address,
SocketAddr::V6(_) => panic!("expected an IPv4 address"),
}
}
#[test]
fn the_tcp_table_names_this_process_on_its_own_loopback_connection() {
let listener = std::net::TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap();
let server = v4(listener.local_addr().unwrap());
let client = std::net::TcpStream::connect(server).unwrap();
let _accepted = listener.accept().unwrap();
let client = v4(client.local_addr().unwrap());
let connections = tcp_connections().unwrap();
assert_eq!(
client_pid(&connections, client, server),
Some(std::process::id())
);
}
#[test]
fn reads_the_account_of_a_process() {
let sid = process_sid(std::process::id()).unwrap();
assert!(sid.starts_with("S-1-5-"), "{sid}");
assert!(process_sid(u32::MAX - 3).is_err());
}
#[tokio::test]
async fn admits_the_owner_and_refuses_any_other_account() {
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await.unwrap();
let _client = std::net::TcpStream::connect(listener.local_addr().unwrap()).unwrap();
let (accepted, _) = listener.accept().await.unwrap();
let owner = process_sid(std::process::id()).unwrap();
assert!(is_owner_connection(&accepted, &owner));
assert!(!is_owner_connection(&accepted, "S-1-5-21-1-2-3-1001"));
}
}
+18 -53
View File
@@ -1,6 +1,8 @@
use crate::service::hub::run_service_until;
use crate::service::owner::{owner_sid_arguments, service_command, ServiceCommand};
use crate::service::peer::{process_sid, serve_until};
use anyhow::{bail, Context, Result};
use once_cell::sync::OnceCell;
use std::ffi::{OsStr, OsString};
use std::thread::sleep;
use std::time::{Duration, Instant};
@@ -34,36 +36,18 @@ const ERROR_SERVICE_CANNOT_ACCEPT_CTRL: i32 = 1061;
const ERROR_SERVICE_NOT_ACTIVE: i32 = 1062;
const ERROR_SERVICE_MARKED_FOR_DELETE: i32 = 1072;
#[derive(Debug, PartialEq, Eq)]
enum ServiceCommand {
Run,
Install,
Uninstall,
}
static OWNER_SID: OnceCell<String> = OnceCell::new();
pub fn main() -> Result<()> {
match service_command(std::env::args_os().skip(1))? {
ServiceCommand::Run => start_service().map_err(Into::into),
ServiceCommand::Install => install_service(),
ServiceCommand::Run { owner_sid } => start_service(owner_sid).map_err(Into::into),
ServiceCommand::Install { owner_pid } => install_service(owner_pid),
ServiceCommand::Uninstall => uninstall_service(),
}
}
fn service_command(args: impl IntoIterator<Item = OsString>) -> Result<ServiceCommand> {
let mut args = args.into_iter();
let command = match args.next().as_deref() {
None => ServiceCommand::Run,
Some(value) if value == OsStr::new("install") => ServiceCommand::Install,
Some(value) if value == OsStr::new("uninstall") => ServiceCommand::Uninstall,
Some(value) => bail!("unknown helper command: {}", value.to_string_lossy()),
};
if args.next().is_some() {
bail!("helper accepts at most one command");
}
Ok(command)
}
fn start_service() -> windows_service::Result<()> {
fn start_service(owner_sid: String) -> windows_service::Result<()> {
let _ = OWNER_SID.set(owner_sid);
service_dispatcher::start(SERVICE_NAME, ffi_service_main)
}
@@ -74,6 +58,10 @@ fn service_main(_arguments: Vec<OsString>) {
}
fn run_windows_service() -> Result<()> {
let owner_sid = OWNER_SID
.get()
.cloned()
.context("the helper service has no owner")?;
let (shutdown_tx, mut shutdown_rx) = watch::channel(false);
let status_handle = service_control_handler::register(
SERVICE_NAME,
@@ -116,7 +104,8 @@ fn run_windows_service() -> Result<()> {
let shutdown_status_handle = status_handle;
let running_status_handle = status_handle;
let service_result = runtime.block_on(run_service_until(
let service_result = runtime.block_on(serve_until(
owner_sid,
async move {
if !*shutdown_rx.borrow() {
let _ = shutdown_rx.changed().await;
@@ -155,7 +144,9 @@ fn run_windows_service() -> Result<()> {
Ok(())
}
fn install_service() -> Result<()> {
fn install_service(owner_pid: u32) -> Result<()> {
let owner_sid = process_sid(owner_pid)
.with_context(|| format!("read the account of process {owner_pid}"))?;
let manager = ServiceManager::local_computer(
None::<&str>,
ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE,
@@ -171,7 +162,7 @@ fn install_service() -> Result<()> {
start_type: ServiceStartType::AutoStart,
error_control: ServiceErrorControl::Normal,
executable_path,
launch_arguments: Vec::new(),
launch_arguments: owner_sid_arguments(&owner_sid),
dependencies: Vec::new(),
account_name: None,
account_password: None,
@@ -321,29 +312,3 @@ fn service_status(
process_id: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_service_commands() {
assert_eq!(service_command([]).unwrap(), ServiceCommand::Run);
assert_eq!(
service_command([OsString::from("install")]).unwrap(),
ServiceCommand::Install
);
assert_eq!(
service_command([OsString::from("uninstall")]).unwrap(),
ServiceCommand::Uninstall
);
}
#[test]
fn rejects_unknown_or_extra_service_commands() {
assert!(service_command([OsString::from("unknown")]).is_err());
assert!(
service_command([OsString::from("install"), OsString::from("unexpected")]).is_err()
);
}
}
+82 -4
View File
@@ -1,6 +1,7 @@
import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
@@ -213,23 +214,41 @@ void main() {
});
group('isPrivilegedStatOutput', () {
test('accepts a root-owned setuid binary', () {
test('accepts a root-owned setuid binary closed to others', () {
expect(
System.isPrivilegedStatOutput(
'root:admin -rwsr-sr-x\n',
'root:admin -rwsr-x---\n',
ownerPrefix: 'root:admin',
),
isTrue,
);
expect(
System.isPrivilegedStatOutput(
'root:root -rwsr-sr-x',
'root:alice -rws------+',
ownerPrefix: 'root:',
),
isTrue,
);
});
test('rejects a setuid binary that others can execute', () {
for (final mode in [
'-rwsr-sr-x',
'-rwsr-xr-x',
'-rwsr-x--x',
'-rwsr-x--t',
]) {
expect(
System.isPrivilegedStatOutput(
'root:admin $mode',
ownerPrefix: 'root:admin',
),
isFalse,
reason: mode,
);
}
});
test('rejects a root-owned binary without the setuid bit', () {
expect(
System.isPrivilegedStatOutput(
@@ -262,12 +281,21 @@ void main() {
'checkIsAdmin',
() {
test('stats the core path verbatim', () async {
processes.stub('stat', 'root:admin -rwsr-sr-x');
processes.stub('stat', 'root:admin -rwsr-x---');
expect(await system.checkIsAdmin(), isTrue);
expect(processes.argumentsFor('stat').last, appPath.corePath);
});
test(
'does not trust a core earlier versions left executable by all',
() async {
processes.stub('stat', 'root:admin -rwsr-sr-x');
expect(await system.checkIsAdmin(), isFalse);
},
);
test('reports a core that is not setuid root', () async {
processes.stub('stat', 'alice:staff -rwxr-xr-x');
@@ -285,6 +313,56 @@ void main() {
: false,
);
group('elevation shells', () {
test('macOS hands the Core to the admin group and nobody else', () {
expect(
System.macOSElevationShell('/Apps/Fl Clash/core'),
"chown root:admin '/Apps/Fl Clash/core' && "
"chmod 4750 '/Apps/Fl Clash/core'",
);
});
test('Linux hands the Core to the requesting user\'s group', () {
final shell = System.linuxElevationShell('/opt/FlClash/FlClashCore');
expect(shell, startsWith(r'group=$(id -g "$PKEXEC_UID") && '));
expect(shell, contains(r'chown "root:$group" '));
expect(shell, endsWith("chmod 4750 '/opt/FlClash/FlClashCore'"));
});
test('neither leaves execute to others', () {
for (final shell in [
System.macOSElevationShell('/core'),
System.linuxElevationShell('/core'),
]) {
expect(shell, isNot(contains('+sx')));
expect(shell, contains('chmod 4750'));
}
});
});
group('authorizeCore on macOS', () {
test('refuses an account outside the admin group', () async {
processes.stub('id', 'alice staff everyone');
expect(await system.authorizeCore(), AuthorizeCode.error);
expect(processes.ran('osascript'), isFalse);
});
test('elevates the Core for an admin account', () async {
processes.stub('id', 'alice staff admin');
expect(await system.authorizeCore(), AuthorizeCode.success);
expect(processes.argumentsFor('osascript').last, contains('chmod 4750'));
});
}, skip: system.isMacOS ? false : 'macOS only');
group('Windows installArguments', () {
test('names this process as the account the Helper serves', () {
expect(Windows.installArguments(4242), 'install --owner-pid 4242');
});
});
group('Linux installService', () {
test('asks pkexec to install the bundled Helper', () async {
expect(await Linux().installService(), isTrue);
+190 -7
View File
@@ -458,9 +458,6 @@ void main() {
rawConfig: {
'dns': {'enable': true, 'listen': '0.0.0.0:53'},
'ntp': {'enable': true, 'write-to-system': true},
'external-controller-tls': '127.0.0.1:9443',
'external-controller-unix': 'mihomo.sock',
'external-controller-pipe': r'\\.\pipe\mihomo',
},
realPatchConfig: const PatchClashConfig(),
overrideDns: false,
@@ -478,18 +475,204 @@ void main() {
final normal = await build(safeMode: false);
expect(normal['dns']['listen'], '0.0.0.0:53');
expect(normal['external-controller-tls'], '127.0.0.1:9443');
expect(normal['ntp']['write-to-system'], true);
final safe = await build(safeMode: true);
expect(safe['dns']['listen'], '');
expect(safe['external-controller-tls'], '');
expect(safe['external-controller-unix'], '');
expect(safe['external-controller-pipe'], '');
expect(safe['ntp']['write-to-system'], false);
expect(safe['ntp']['enable'], true);
});
test(
'makeRealProfileTask does not let a profile open a controller',
() async {
for (final safeMode in [false, true]) {
final result = await makeRealProfileTask(
MakeRealProfileState(
profilesPath: '/profiles',
profileId: 14,
rawConfig: {
'external-controller': '0.0.0.0:9090',
'secret': 'from-the-profile',
'external-controller-tls': '0.0.0.0:9443',
'external-controller-unix': '/tmp/victim',
'external-controller-pipe': r'\\.\pipe\mihomo',
},
realPatchConfig: const PatchClashConfig(),
overrideDns: false,
overrideNtp: false,
appendSystemDns: false,
proxyGroups: const [],
rules: const [],
addedRules: const [],
defaultUA: 'FlClash-Test',
safeMode: safeMode,
),
);
final config = loadYaml(result.yaml) as YamlMap;
expect(config['external-controller-tls'], '');
expect(config['external-controller-unix'], '');
expect(config['external-controller-pipe'], '');
expect(config['external-controller'], '');
expect(config['secret'], '');
}
},
);
test(
'makeRealProfileTask writes the app secret over the profile one',
() async {
Future<YamlMap> build(PatchClashConfig realPatchConfig) async {
final result = await makeRealProfileTask(
MakeRealProfileState(
profilesPath: '/profiles',
profileId: 14,
rawConfig: {'secret': 'from-the-profile'},
realPatchConfig: realPatchConfig,
overrideDns: false,
overrideNtp: false,
appendSystemDns: false,
proxyGroups: const [],
rules: const [],
addedRules: const [],
defaultUA: 'FlClash-Test',
),
);
return loadYaml(result.yaml) as YamlMap;
}
final open = await build(
const PatchClashConfig(
externalController: ExternalControllerStatus.open,
secret: 'from-the-app',
),
);
expect(open['external-controller'], '127.0.0.1:9090');
expect(open['secret'], 'from-the-app');
final closed = await build(const PatchClashConfig());
expect(closed['secret'], '');
final missingSecret = await build(
const PatchClashConfig(
externalController: ExternalControllerStatus.open,
),
);
expect(missingSecret['external-controller'], '');
},
);
test(
'profile listeners and host changes are disabled only in safe mode',
() async {
for (final safeMode in [false, true]) {
final result = await makeRealProfileTask(
MakeRealProfileState(
profilesPath: '/profiles',
profileId: 14,
rawConfig: {
'listeners': [
{'type': 'mixed', 'name': 'extra', 'port': 1234},
{'type': 'tun', 'name': 'extra-tun', 'auto-route': true},
],
'tunnels': [
{
'network': ['tcp'],
'address': '0.0.0.0:1234',
'target': 'example.test:443',
},
],
'ss-config': 'ss://cipher:password@:1234',
'vmess-config': 'vmess://uuid@:1234',
'tuic-server': {'enable': true, 'listen': '0.0.0.0:1234'},
'iptables': {'enable': true, 'dns-redirect': true},
'dns': {'enable': true, 'listen': '0.0.0.0:53'},
'ntp': {'enable': true, 'write-to-system': true},
},
realPatchConfig: const PatchClashConfig(
externalController: ExternalControllerStatus.open,
secret: 'from-the-app',
),
appendSystemDns: false,
proxyGroups: const [],
rules: const [],
addedRules: const [],
defaultUA: 'FlClash-Test',
safeMode: safeMode,
),
);
final config = loadYaml(result.yaml) as YamlMap;
if (safeMode) {
expect(config['listeners'], isEmpty);
expect(config['tunnels'], isEmpty);
expect(config['ss-config'], '');
expect(config['vmess-config'], '');
expect(config['tuic-server']['enable'], false);
expect(config['iptables']['enable'], false);
expect(config['dns']['listen'], '');
expect(config['ntp']['write-to-system'], false);
} else {
expect(config['listeners'], [
{'type': 'mixed', 'name': 'extra', 'port': 1234},
{'type': 'tun', 'name': 'extra-tun', 'auto-route': true},
]);
expect(config['tunnels'], [
{
'network': ['tcp'],
'address': '0.0.0.0:1234',
'target': 'example.test:443',
},
]);
expect(config['ss-config'], 'ss://cipher:password@:1234');
expect(config['vmess-config'], 'vmess://uuid@:1234');
expect(config['tuic-server'], {
'enable': true,
'listen': '0.0.0.0:1234',
});
expect(config['iptables'], {'enable': true, 'dns-redirect': true});
expect(config['dns']['listen'], '0.0.0.0:53');
expect(config['ntp']['write-to-system'], true);
}
expect(config['external-controller'], safeMode ? '' : '127.0.0.1:9090');
}
},
);
test('iptables interface names cannot add command arguments', () async {
for (final interfaceName in ['br-lan.10', 'lo -M /tmp/extra']) {
final result = await makeRealProfileTask(
MakeRealProfileState(
profilesPath: '/profiles',
profileId: 14,
rawConfig: {
'iptables': {
'enable': true,
'inbound-interface': interfaceName,
'bypass': ['192.168.0.0/16'],
},
},
realPatchConfig: const PatchClashConfig(),
appendSystemDns: false,
proxyGroups: const [],
rules: const [],
addedRules: const [],
defaultUA: 'FlClash-Test',
),
);
final config = loadYaml(result.yaml) as YamlMap;
expect(
config['iptables'],
interfaceName == 'br-lan.10'
? {
'enable': true,
'inbound-interface': interfaceName,
'bypass': ['192.168.0.0/16'],
}
: {'enable': false},
);
}
});
group('makeRealProfileTask interface-name mode', () {
Future<YamlMap> runWith(PatchClashConfig realPatchConfig) async {
final rawConfig = await decodeJSONTask<Map<String, dynamic>>(
@@ -59,6 +59,14 @@ void main() {
expect(_inserted(completion, 'mixed-port'), 'mixed-port: ');
});
test('omits disabled controllers and keeps listener completion', () {
final controllers = _labels(_complete('external-controller-|'));
expect(controllers, isNot(contains('external-controller-tls')));
expect(controllers, isNot(contains('external-controller-unix')));
expect(controllers, isNot(contains('external-controller-pipe')));
expect(_labels(_complete('listen|')), contains('listeners'));
});
test('keeps dashes in the prefix and opens a sequence', () {
final completion = _complete('proxy-g|');
expect(completion!.prefix, 'proxy-g');
+68
View File
@@ -371,6 +371,17 @@ void main() {
expect(restored.interfaceName, 'eth0');
});
test('the controller secret survives round-trip and defaults to none', () {
expect(const PatchClashConfig().secret, isEmpty);
final restored = roundTrip(
() => const PatchClashConfig(secret: 'abc').toJson(),
PatchClashConfig.fromJson,
);
expect(restored.secret, 'abc');
});
test('unknown interface-name-mode falls back to clear', () {
final restored = PatchClashConfig.fromJson({
'interface-name-mode': 'unknown',
@@ -380,6 +391,63 @@ void main() {
});
});
group('PatchClashConfig.ensureControllerSecret', () {
const open = PatchClashConfig(
externalController: ExternalControllerStatus.open,
);
test('leaves a closed controller without a secret as it is', () {
const closed = PatchClashConfig();
expect(identical(closed.ensureControllerSecret(), closed), isTrue);
});
test('gives an open controller a secret of its own', () {
final ensured = open.ensureControllerSecret();
expect(ensured.secret, matches(RegExp(r'^[A-Za-z0-9]{32}$')));
expect(ensured.externalController, ExternalControllerStatus.open);
});
test('is idempotent, so a secret the user chose or was shown stays', () {
final ensured = open.ensureControllerSecret();
expect(ensured.ensureControllerSecret().secret, ensured.secret);
expect(
open.copyWith(secret: 'mine').ensureControllerSecret().secret,
'mine',
);
});
test(
'drops the secret with the controller, and opening again rotates it',
() {
final first = open.ensureControllerSecret();
final closed = first
.copyWith(externalController: ExternalControllerStatus.close)
.ensureControllerSecret();
final second = closed
.copyWith(externalController: ExternalControllerStatus.open)
.ensureControllerSecret();
expect(closed.secret, isEmpty);
expect(second.secret, isNot(first.secret));
},
);
test('reaches the Core through the update params', () {
final params = open
.copyWith(secret: 'abc')
.toUpdateParams(
routeMode: RouteMode.config,
authentication: const [],
);
expect(params.secret, 'abc');
expect(params.toJson()['secret'], 'abc');
});
});
group('ProxiesStyleProps JSON round-trip', () {
test('default values', () {
const props = ProxiesStyleProps();
+18
View File
@@ -226,6 +226,24 @@ void main() {
});
});
group('writeConfig', () {
test('never leaves a restored open controller without a secret', () {
final ref = container.read(Provider<Ref>((ref) => ref));
writeConfig(
ref,
const Config(
themeProps: ThemeProps(),
patchClashConfig: PatchClashConfig(
externalController: ExternalControllerStatus.open,
),
),
);
expect(container.read(patchClashConfigProvider).secret, hasLength(32));
});
});
group('buildConfigOverrides', () {
test('produces correct overrides', () {
const config = Config(
+43
View File
@@ -627,6 +627,49 @@ void main() {
ExternalControllerStatus.open,
);
});
Future<UpdateParams> updateWith(PatchClashConfig patchConfig) async {
final core = _MockCoreHandlerInterface();
when(() => core.updateConfig(any())).thenAnswer((_) async => '');
final scopedAction = TestSetupAction();
final scoped = ProviderContainer(
overrides: [
profilesProvider.overrideWith(TestProfiles.new),
setupActionProvider.overrideWith(() => scopedAction),
coreHandlerProvider.overrideWithValue(CoreController.scoped(core)),
],
);
addTearDown(scoped.dispose);
scoped.read(setupActionProvider.notifier);
scoped.read(patchClashConfigProvider.notifier).value = patchConfig;
await scopedAction.updateConfig();
return verify(() => core.updateConfig(captureAny())).captured.single
as UpdateParams;
}
test('hands the Core the controller secret with the address', () async {
final params = await updateWith(
const PatchClashConfig(
externalController: ExternalControllerStatus.open,
secret: 'abc',
),
);
expect(params.externalController, ExternalControllerStatus.open);
expect(params.secret, 'abc');
});
test('never opens the controller without a secret', () async {
final params = await updateWith(
const PatchClashConfig(
externalController: ExternalControllerStatus.open,
),
);
expect(params.externalController, ExternalControllerStatus.close);
});
});
group('recoverMissingProfile', () {
@@ -0,0 +1,116 @@
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/providers/app.dart';
import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/providers/database.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/views/config/general.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:material_ui/material_ui.dart';
import '../helpers/test_app.dart';
import '../helpers/test_profiles.dart';
const _secretLabel = 'External controller secret';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late ProviderContainer container;
Future<void> pumpGeneralView(WidgetTester tester) async {
tester.view.physicalSize = const Size(1000, 3000);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
container = ProviderContainer(
overrides: [profilesProvider.overrideWith(TestProfiles.new)],
);
addTearDown(container.dispose);
globalState.container = container;
container
.read(viewSizeProvider.notifier)
.update((_) => const Size(1000, 3000));
await tester.pumpWidget(
UncontrolledProviderScope(
container: container,
child: const TestApp(child: GeneralView()),
),
);
await tester.pumpAndSettle();
}
Future<void> tapControllerSwitch(WidgetTester tester) async {
await tester.tap(
find.descendant(
of: find.widgetWithText(ListTile, 'External controller'),
matching: find.byType(Switch),
),
);
await tester.pumpAndSettle();
}
testWidgets('opening the controller shows the secret it was given', (
tester,
) async {
await pumpGeneralView(tester);
expect(find.text(_secretLabel), findsNothing);
await tapControllerSwitch(tester);
final config = container.read(patchClashConfigProvider);
expect(config.externalController, ExternalControllerStatus.open);
expect(config.secret, hasLength(32));
expect(find.text(_secretLabel), findsOneWidget);
expect(find.text(config.secret), findsOneWidget);
});
testWidgets('a secret typed into the row replaces the generated one', (
tester,
) async {
await pumpGeneralView(tester);
await tapControllerSwitch(tester);
await tester.tap(find.text(_secretLabel));
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextFormField), ' my-own-secret ');
await tester.pumpAndSettle();
await tester.tap(find.text('Submit'));
await tester.pumpAndSettle();
expect(container.read(patchClashConfigProvider).secret, 'my-own-secret');
});
testWidgets('the row refuses an empty secret', (tester) async {
await pumpGeneralView(tester);
await tapControllerSwitch(tester);
final generated = container.read(patchClashConfigProvider).secret;
await tester.tap(find.text(_secretLabel));
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextFormField), ' ');
await tester.pumpAndSettle();
await tester.tap(find.text('Submit'));
await tester.pumpAndSettle();
expect(container.read(patchClashConfigProvider).secret, generated);
});
testWidgets('closing the controller drops the secret and its row', (
tester,
) async {
await pumpGeneralView(tester);
await tapControllerSwitch(tester);
await tapControllerSwitch(tester);
expect(
container.read(patchClashConfigProvider).externalController,
ExternalControllerStatus.close,
);
expect(container.read(patchClashConfigProvider).secret, isEmpty);
expect(find.text(_secretLabel), findsNothing);
});
}