fix(core,helper): only let the owner drive the elevated Core
Changelog: The elevated Core only answers its owner
This commit is contained in:
+76
-8
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -150,6 +150,7 @@
|
||||
"allowBypass": "アプリによるVPNバイパスを許可",
|
||||
"externalController": "外部コントローラー",
|
||||
"externalControllerDesc": "有効にすると、ポート9090でClashコアを制御できます",
|
||||
"externalControllerSecret": "外部コントローラーのシークレット",
|
||||
"ipv6Desc": "有効にすると、IPv6トラフィックを受信できます",
|
||||
"app": "アプリ",
|
||||
"general": "一般",
|
||||
|
||||
@@ -150,6 +150,7 @@
|
||||
"allowBypass": "Разрешить приложениям обходить VPN",
|
||||
"externalController": "Внешний контроллер",
|
||||
"externalControllerDesc": "При включении ядром Clash можно управлять через порт 9090",
|
||||
"externalControllerSecret": "Секрет внешнего контроллера",
|
||||
"ipv6Desc": "При включении можно принимать трафик IPv6",
|
||||
"app": "Приложение",
|
||||
"general": "Общие",
|
||||
|
||||
@@ -150,6 +150,7 @@
|
||||
"allowBypass": "允许应用绕过VPN",
|
||||
"externalController": "外部控制器",
|
||||
"externalControllerDesc": "开启后将可以通过9090端口控制Clash内核",
|
||||
"externalControllerSecret": "外部控制器密钥",
|
||||
"ipv6Desc": "开启后将可以接收IPv6流量",
|
||||
"app": "应用",
|
||||
"general": "常规",
|
||||
|
||||
+17
-5
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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'] = {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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("フェード"),
|
||||
|
||||
@@ -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("Растворение"),
|
||||
|
||||
@@ -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("淡入"),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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');
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user