Compare commits

..

70 Commits

Author SHA1 Message Date
dependabot[bot] 11c8125286 Bump actions/checkout from 6 to 7
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-11-02 18:44:17 +00:00
DHR60 0f1f82af4e Fix (#10501) 2026-11-02 15:42:27 +08:00
吴小白 3d0f45f6a8 Update loongarch64 build (#10487)
* Update loongarch64 build

Signed-off-by: 吴小白 <296015668@qq.com>

* Update GlobalHotKeys

---------

Signed-off-by: 吴小白 <296015668@qq.com>
Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-11-02 15:27:24 +08:00
DHR60 19850f92be Profile pre-resolver (#10220)
* Profile pre-resolver

* Try cname

* UI
2026-11-02 15:02:44 +08:00
Shubham Padkonde 4c4449e74d fix: match proxy group types without case sensitivity (#10472) 2026-11-02 09:53:18 +08:00
DHR60 59487dca75 Add MASQUE warp support (#10498) 2026-11-02 09:30:09 +08:00
DHR60 d4880dbe02 Fix policy group (#10495) 2026-11-02 09:29:45 +08:00
DHR60 0856ffd7d8 Try fix dns (#10485) 2026-11-02 09:27:59 +08:00
Shubham Padkonde c436b998b4 fix: persist repeated profile test results (#10466) 2026-11-02 09:26:34 +08:00
Miheichev Aleksandr Sergeevich 7a52be4150 chore(deps): bump IPNetwork2 from 4.3.0 to 5.0.1 (#10453)
* chore(deps): bump IPNetwork2 from 4.3.0 to 5.0.1

* test: use AOT-safe IPNetwork route assertions
2026-11-02 09:24:35 +08:00
DHR60 8c02b5f2f6 Update cert workflow (#10497)
* Update cert workflow

* Update certificate
2026-11-02 09:24:18 +08:00
Miheichev Aleksandr Sergeevich 743eb323c2 fix(webapi): preserve Unix environment names and isolate test fixtures (#10455)
* fix(webapi): preserve Unix environment names and isolate local test fixtures

* test(webapi): isolate launcher fixtures from host process IDs

* test(webapi): allow auth fixture scheduling headroom
2026-11-02 09:20:34 +08:00
Miheichev Aleksandr Sergeevich ebabf79017 fix(webapi): allow reading the instance-lock owner on macOS (#10454)
* test(webapi): cover instance-lock owner reads across processes

* fix(webapi): keep macOS instance-lock owners readable

* test(webapi): wait for lock release after owner exit
2026-11-02 09:20:08 +08:00
Miheichev Aleksandr Sergeevich 756916978d fix(xray): stop emitting obsolete echForceQuery (#10480) 2026-11-01 21:11:17 +08:00
Miheichev Aleksandr Sergeevich a2a8a77ff6 chore(deps): update MaterialDesignThemes to 5.4.0 (#10423) 2026-11-01 21:08:08 +08:00
dependabot[bot] 2bc79bc5a1 Bump Repobot.SQLite.Unofficial from 3.53.4.1 to 3.54.1 (#10416)
---
updated-dependencies:
- dependency-name: Repobot.SQLite.Unofficial
  dependency-version: 3.54.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-01 21:07:41 +08:00
dependabot[bot] 4c8b865d03 Bump docker/build-push-action from 6 to 7 (#10427)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6 to 7.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-01 21:07:20 +08:00
Miheichev Aleksandr Sergeevich 3d44ba8ec8 chore(deps): update NLog to 6.2.2 (#10409) 2026-11-01 21:06:39 +08:00
dependabot[bot] aac4520f59 Bump actions/setup-python from 6 to 7 (#10426)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6 to 7.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-01 21:05:17 +08:00
dependabot[bot] b13ae2592a Bump actions/download-artifact from 7 to 8 (#10425)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7 to 8.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v7...v8)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-01 21:05:07 +08:00
dependabot[bot] bf2584c9a9 Bump docker/setup-buildx-action from 3 to 4 (#10424)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-01 21:04:55 +08:00
Miheichev Aleksandr Sergeevich e044b90c83 chore(deps): update Downloader to 5.10.2 (#10491) 2026-11-01 21:04:52 +08:00
DHR60 3dce503a08 Fix (#10494) 2026-11-01 21:01:34 +08:00
DHR60 70ab27e436 Fix udp test (#10484) 2026-11-01 20:58:58 +08:00
DHR60 9593b289b9 Fix (#10460) 2026-11-01 20:54:43 +08:00
DHR60 5664270be7 Update cert (#10450) 2026-11-01 20:54:27 +08:00
Lukas 926790b071 fix(speedtest): clear the last speed when a speed test starts (#10449)
A speed test only stored a speed above zero, so a node that was skipped
because its delay test failed, or whose download failed, kept the speed
of the previous run. The list showed the skip or error text until it was
sorted or refreshed; then the old speed came back and the node sorted as
if it had been measured. Clear the stored speed when a speed or mixed
test starts.
2026-11-01 20:53:45 +08:00
Ekko82 e1c9633535 fix: keep the PAC listener running after a client reset (#10447)
* fix: keep the PAC listener running after a client reset

* Update PacManager.cs

* Update PacManager.cs

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-11-01 20:50:32 +08:00
UnrealTale 36184045bb Fix (#10431) (#10432) 2026-11-01 20:44:44 +08:00
DHR60 fcd313007a Update GlobalHotKeys (#10493) 2026-11-01 20:44:11 +08:00
Wu Shuwen 7e14cf9773 Preserve subscription profiles on failed import (#10429) 2026-11-01 20:43:48 +08:00
Wu Shuwen ad3e24b6b5 Fix fragment packet selection for REALITY (#10428) 2026-11-01 20:39:08 +08:00
Miheichev Aleksandr Sergeevich 4169c12a1f test: strengthen ProfileExManager concurrency regression coverage (#10410) 2026-11-01 20:35:38 +08:00
DHR60 8b93d35766 Add ProDataGrid (#10347)
* Bump Avalonia.Desktop from 12.1.2 to 12.1.4

---
updated-dependencies:
- dependency-name: Avalonia.Desktop
  dependency-version: 12.1.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Remove Avalonia.Controls.DataGrid

* Add ProDataGrid Feature

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-11-01 20:34:24 +08:00
DHR60 78b0b2224b Native Aot (#10295)
* Protect models

* Json Context

* YAML AOT-friendly optimization

* Run the Test in AOT mode

* Remove TaskScheduler

* Clean build output

* Workflows
2026-11-01 20:29:06 +08:00
DHR60 e2f4292321 ReactiveUI 26 (#10249)
* ReactiveUI 25

* Fix

* Up

* ReactiveUI 26
2026-11-01 20:00:02 +08:00
Nozilla-X 57f17d5939 fix(profile-ex): make ProfileExItem creation atomic per IndexId (#10420)
ConcurrentBag only makes individual operations safe. GetProfileExItem performed FirstOrDefault and AddProfileEx as separate operations, so concurrent speed tests could both miss and create duplicate entries for the same IndexId. Recheck under a dedicated lock before creating, and replace the bag under the same lock in InitData/ClearAll so creation cannot target a bag that is being swapped. Existing reads and item updates stay lock-free.
2026-10-20 16:58:12 +08:00
Nozilla-X 66dd7da6e6 feat(web): 添加无头 WebAPI 支持 (#10300)
* feat(web): add headless ServiceLib API and replaceable UI host

* build(web): package API without bundled WebUI

* ci(web): build and test headless API packages

* docs(web): document the headless API and optional WebUI

* test(web): verify restore and build before API tests

* docs(web): align API release examples with current upstream

* test(web): cover creating and editing an empty-URL subscription group

Verify the API accepts and persists a subscription whose Url and MoreUrl stay empty, that an edit keeps the empty URL and zero interval, and that ServiceLib is untouched.

* fix(web): support per-target prerelease update preferences

* fix(web): require management key for non-loopback listeners

* ci: reduce development Web API build matrix

* fix(web): make native self-update rollback atomic and preserve progress

* docs(web): remove self-update E2E report

* ci(web): decouple Web releases from Linux Desktop

* feat(web): support standalone WebUI clients and rename the API to v2rayN.WebAPI

- Resolve every REST, login, setup, download, SSE-ticket and EventSource
  request through one configurable Backend base per WebUI origin, with a
  strict exact-match origin allowlist and no wildcard or credential CORS.
- Run the origin guard before authentication, and keep first-run setup,
  instance diagnostics and the hosted-UI CSP same-origin as before.
- Rename the project directory, csproj, namespace, executable, lock,
  build identity and self-update target to v2rayN.WebAPI while retaining
  V2RAYN_WEB_* settings, WebVersion metadata, webVersion fields and the
  existing release asset contracts.
- Migrate API-owned update preference identifiers and reject legacy
  v2rayN.Web update packages with a manual reinstall instruction.
- Require staged native executables to be Linux ELF images for their RID.
- Add CORS, update-identity and setup-security tests, and record the
  standalone-WebUI audit under v2rayN.WebAPI/STANDALONE-WEBUI-AUDIT.md.

* Remove WebUI hosting and configuration coupling from WebAPI

* Finish WebAPI cleanup and isolate native self-update worker

* Keep WebAPI client documentation implementation-neutral

* ci(webapi): verify Windows win-x64 build and startup

* fix(webapi): validate keys and unify loopback limits

* test(webapi): isolate update smoke from systemd runner

* feat(webapi): add Windows x64 release and self-update

* fix(webapi): verify release manifest version

* ci(webapi): publish web packages through the official release flow

* ci(webapi): run the web release inside the release-all workflow

* webapi: finish the rename and fix shutdown, terminal launch and help

- finish the Web -> WebAPI product rename: workflow files build-WebAPI.yml /
  release-WebAPI.yml (path filters, reusable call sites, container tag),
  solution entry, health headers X-v2rayn-WebAPI-*, WebAPI.self-update
  capability, WebAPI-update-progress.json / WebAPI-update-runtime-state.json,
  WebAPIData/WebAPI-auth.json with a migration from the legacy locations,
  and the README compatibility notes
- end /api/events when the host starts stopping so Kestrel's graceful stop
  cannot pin the whole shutdown budget with a client attached; a runtime
  cleanup that starts after the host budget was exhausted now runs a reduced
  fresh pass instead of skipping Core stop and shared-state saves entirely
- record the interactive launch context for detached children and helpers so
  systemd markers leaked into a terminal (INVOCATION_ID / JOURNAL_STREAM) no
  longer force foreground mode or misfire the supervised-deployment key check,
  self-update gating and lifecycle decisions
- add --help / -h with the options and common environment variables in all
  three launcher locales, and keep help flags out of the host arguments

* fix(webapi): retry transient file replacements during native updates

Windows antivirus and the search indexer can hold a freshly written
executable open for a moment, so replacing the installed executable or its
build identity could fail with a bare access-denied error and abort the
update or its rollback. Retry these replacements for a bounded window and
report the operation, target path and cause when they still fail.

* ci(webapi): display the release-all Web job as release-WebAPI

The reusable-workflow job key still used the pre-rename name, which is the
label the Actions UI shows for the nested Web release jobs.

* feat(webapi): add win-arm64, linux-riscv64 and linux-loongarch64 coverage

- build, package, sign and self-update support for the three architectures:
  win-arm64 (windows-11-arm native startup smoke), linux-riscv64 (RISE RISC-V
  runners with the community xujiegb/dotnet-riscv SDK; single-file publishing
  verified on real hardware) and linux-loongarch64 (the upstream QEMU loong64
  VM with the official Loongson SDK, smoke-tested inside the VM)
- per-RID native executable validation: ELF machines x86-64/AArch64/RISC-V
  (0xF3)/LoongArch (0x102) and Windows PE AMD64/ARM64 (0xAA64), behind a
  single IsSupportedRid gate
- release asset map and packager/manifest tables extended; the riscv64 and
  loongarch64 Core bundles ship without the cronet payload
- release manifests now ignore unknown RID entries so a newer manifest cannot
  be rejected wholesale by an older build

* fix(webapi): keep the loongarch64 QEMU smoke out of the packaged publish tree

The in-VM smoke ran the app inside the virtiofs-shared publish directory and
left runtime files owned by the VM root (v2rayN.WebAPI.instance.lock) that the
host runner could not read, so packaging failed with EACCES. The smoke now runs
in a VM-local copy, and a host-side guard verifies the shared tree is complete
and readable before packaging.

* docs(webapi): document the six-platform release matrix

Releases now cover linux-x64/arm64/riscv64/loongarch64 and win-x64/arm64;
note the per-RID machine validation and the forward-compatible manifest
behavior in the API release documentation.

* ci(webapi): keep the release-all desktop dispatch exactly upstream

build-all.yml dispatches the desktop workflows with "ref": "master" exactly
as upstream does. The fork-only github.ref_name change is reverted so the PR
leaves the existing release behavior untouched; the WebAPI release stays an
additive reusable job in the same workflow.

* fix(webapi): probe the effective listener and refuse unverifiable updates

GetCurrentHealthUri and GetLauncherUris forced every probe to 127.0.0.1 and
fell back to port 5080, so IPv6-only, wildcard and port-only listeners could
be checked at the wrong endpoint. The detached update helper then either
rolled back a healthy replacement or restarted without being able to verify
the old instance, and --stop could probe the wrong process.

Both callers now resolve the probe through one WebProbeUriResolver that uses
the same effective Kestrel configuration as the host (command line, urls,
http/https ports, Kestrel endpoints, preferHostingUrls) and maps each
listener to its matching loopback address: "*", "+" and host names probe
localhost, 0.0.0.0 probes 127.0.0.1, [::] probes [::1], and loopback
literals keep their family. When no loopback HTTP probe exists (HTTPS-only
or an explicit non-loopback address), GetWebUpdateTarget reports
canInstall=false with maintenance.webUpdateProbeUnavailable and the update
is refused before anything is stopped; the helper still accepts only
loopback HTTP plan URIs, now including [::1]. --stop reports the new
stopUnprobeable result instead of signalling an unverified owner, and the
background launcher fails closed with startUnprobeable.

Regression: new resolver/listener tests (default, custom port, IPv6-only,
HTTP_PORTS, Kestrel override, HTTPS-only) and the real update fixture now
covers an IPv6-only replacement, asserts that a failed update leaves the old
build running (replacement PID), and proves that an unprobeable instance
refuses both the API and --stop while staying up.

* ci(webapi): capture the real smoke PID and bound the SIGTERM waits

The riscv64 smoke backgrounded "cd && app" and recorded the subshell PID,
so the later kill could target the wrapper while the app kept running. Both
the riscv64 and loong64 smokes now start the app directly, record the
application PID, fail fast when the process exits before health, wait a
bounded time for SIGTERM before printing the log and force-killing, and
assert a zero exit status. The loong64 VM smoke replaces its fixed
two-second sleep with the same exit assertion.

* chore(webapi): drop unreferenced legacy scripts

bootstrap-xray.sh installed the removed bundled core-bin layout, and
test-backup-fixtures.py compared desktop/Web backup ZIPs from before the
WebUI decoupling; nothing references either script.

* fix(webapi): unify the loopback probe, diagnostics and helper rules

The resolver rewrote a configured localhost to 127.0.0.1 while the health
diagnostics and the update helper each carried their own copy of the
accepted-host rule. Keep localhost as localhost (resolving it covers both
families instead of forcing one IPv4 literal) and make
WebProbeUriResolver.IsProbeableLoopbackUri the single rule: plain HTTP to a
host the diagnostics accept as local (localhost, 127.0.0.1 or ::1). The
helper validates plans with that rule, the health endpoint uses the shared
WebSetupAccessPolicy.IsLocalDiagnosticSource predicate, and every probe URI
the resolver produces satisfies the rule. Unprobeable listeners
(HTTPS-only or an explicit non-loopback address) still refuse the
self-update up front: canInstall=false with
maintenance.webUpdateProbeUnavailable while the current instance keeps
running.

Regression: localhost stays localhost across the resolver and launcher
tests, the probe-rule invariant, the shared diagnostics/probe address
rule, a direct unit test of the refusal reason, and a real
localhost-listener replacement scenario in the update fixture.

* fix(webapi): gate generated probes through the shared loopback rule

MapToLoopbackHost accepted every address the OS reports as loopback, which in
.NET is the whole 127.0.0.0/8 range, and TryCreateHealthUri returned true
without checking the generated URI against the same rule the diagnostics and
the update helper use. A listener on 127.0.0.2 therefore passed the up-front
canInstall check, but the helper rejected its plan (and refused to restart
the previous build for the same reason), so an update could stop the service
without bringing anything back.

TryCreateHealthUri now gates the constructed probe through
IsProbeableLoopbackUri, so loopback aliases are refused before the instance
is stopped and a mixed listener list falls through to the next verifiable
endpoint.

Regression: 127.0.0.2/127.255.255.254 mapping rows, a dedicated
loopback-alias rejection test, the alias canInstall=false reason case, the
mixed-list fall-through, and a real 127.0.0.2-listener fixture scenario that
proves the update is refused while the current instance keeps running.

* refactor(webapi): centralize integration tests and simplify release scripts

Move the Linux integration tests (startup security, release packaging
dry-run, full-ZIP runtime, native update regression and its fixture) from
Scripts/ into Tests/Integration/; Scripts/verify.sh stays the unified entry
and all path resolution, workflow calls, README text and cross-script
invocations were updated. No test coverage changes.

Merge Test-Windows.ps1 and Test-WindowsUpdate.ps1 into one
Tests/Integration/Test-Windows.ps1 with -Mode startup|update: both modes
share the native process control (CREATE_NEW_PROCESS_GROUP + CTRL_BREAK),
environment isolation, free-port selection, no-proxy health waiting, JSON
fixtures and retained-on-failure cleanup. CI keeps the Windows x64/ARM64
startup smokes and the x64 replacement/rollback smoke.

Delete the package-native.sh thin wrapper; the workflow and the packaging
dry-run now call web-release-assets.py package directly with the same
--rid/--publish/--dist arguments, so asset names and ZIP boundaries stay
unchanged.

* test(webapi): keep the Windows health-body assertion in the merged smoke

The merged Wait-TestHealth only accepted HTTP 200 in startup mode, while
the original startup smoke parsed the health JSON and required
status == "ok". Restore the body assertion in the shared helper so both
modes verify the health payload instead of trusting the status code alone.

* fix(webapi): preflight and verify native backup restores

* test(webapi): isolate restore preflight smoke from systemd

* test(webapi): set Windows smoke autostart default

* test(webapi): retry transient Windows smoke cleanup

* test(webapi): retry Windows smoke temp cleanup longer

* ci(webapi): check whitespace on workflow dispatch
2026-10-20 10:52:23 +08:00
DHR60 5e0ea3275d Fix (#10404) 2026-10-18 19:23:03 +08:00
Dmitry 673283da4b Fix data race in ProfileExManager (#10388)
* Fix data race in ProfileExManager

Parallel speed tests over many profiles corrupt the save queue of test results: IndexIdEnqueue touches a plain Queue<string> from every test thread, so Contains plus Enqueue race - the same profile gets queued twice, and Queue.Contains can even throw ArgumentOutOfRangeException out of SetTestDelay, losing the result. SaveQueueIndexIds has the same class of bug: it snapshots Count and then dequeues in a loop, so concurrent enqueues throw or lose entries. GetProfileExItem could create duplicate rows for the same index the same way.

Guard the queue and the profile lookup with a lock and drain a snapshot instead of a stale count. No behavior change besides correctness.

* Fix data race in ProfileExManager

Parallel speed tests over many profiles corrupt the save queue of test results: IndexIdEnqueue touches a plain Queue<string> from every test thread, so Contains plus Enqueue race - the same profile gets queued twice, and Queue.Contains can even throw ArgumentOutOfRangeException out of SetTestDelay, losing the result. SaveQueueIndexIds has the same class of bug: it snapshots Count and then dequeues in a loop, so concurrent enqueues throw or lose entries.

Switch the queue to ConcurrentQueue and drain it with TryDequeue. As suggested in review, duplicates are not filtered anymore: this is a helper, and the save loop just writes the same row more than once.
2026-10-17 19:37:43 +08:00
Miheichev Aleksandr Sergeevich 1f083adbe4 chore(deps): update Downloader to 5.10.1 (#10382)
Downloader 5.9.8 -> 5.10.1

5.10.0 no longer reads the headers of an error response as the file's:
with 5.9.8, an error status on the first request followed by a good
response made the download complete with the file cut to the length of
the error body. 5.10.0 retries that first request on 408, 429, 503 and
504, but a transport error or timeout there no longer gets the second
attempt that 5.9.8 made. 5.10.1 only fixes the mirror choice for
downloads with several URLs, which DownloaderHelper does not use.

Dependabot does not propose this update: its NuGet compatibility check
rejects Downloader 5.10.x for the WPF project's
net10.0-windows10.0.19041 target.
2026-10-17 16:59:38 +08:00
dependabot[bot] 80ab14d057 Bump TUnit from 1.73.19 to 1.75.0 (#10357)
---
updated-dependencies:
- dependency-name: TUnit
  dependency-version: 1.75.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-17 16:55:34 +08:00
2dust 95f438abc9 Update winget-publish.yml
Update winget-publish.yml
2026-10-14 15:37:05 +08:00
DHR60 f1023e08e1 Fix (#10344)
* chore. update core urls

* Fix clash delay
2026-10-13 20:36:41 +08:00
2dust 1883178b5f up 7.25.6 2026-10-11 13:55:36 +08:00
2dust 1cde521d75 Prevent zip path traversal in app upgrade
Hardened upgrade extraction by validating each zip entry resolves under the app startup directory before writing files, and aborting with a clear failure message on suspicious paths. Added shared path utilities for directory containment checks and OS-aware path string comparison, then reused them for executable matching and `bin` prefix checks during extraction.

https://github.com/2dust/v2rayN/issues/10313
2026-10-11 13:52:28 +08:00
Ali Ahmadi 2407d44f4f Fix empty latency tests and Linux startup errors (#10335)
* Fix overflow when speed tests have no eligible profiles

* Preserve core executable permissions in Debian packages

* Avoid requesting an icon refresh before view initialization
2026-10-11 11:06:46 +08:00
dependabot[bot] fbf822d74c Bump TUnit from 1.72.16 to 1.73.19 (#10334)
---
updated-dependencies:
- dependency-name: TUnit
  dependency-version: 1.73.19
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-11 10:56:36 +08:00
liu-xindi 4869a6cbba Route IPv6 into the Xray TUN when EnableIPv6Address is on (#10329)
* Route IPv6 into the Xray TUN on NAT66 hosts and when IPv6 is enabled

#10080 dropped ::/0 from autoSystemRoutingTable whenever the host held no
address inside 2000::/3. A host behind NAT66 holds only unique local
addresses, yet it reaches the IPv6 internet through its IPv6 default
gateway. With ::/0 gone, its IPv6 follows the system default route,
bypasses the tunnel and exposes the real IPv6 address (#10327).

The check also overrode EnableIPv6Address. Before #9930, enabling it
always added ::/0; since #10080 the option only assigned the interface
address, so users who had enabled it were affected as well.

Route ::/0 when EnableIPv6Address is on, and otherwise when the host has
IPv6 connectivity: a global IPv6 address or an IPv6 default gateway.
Routes whose next hop is ::, such as the one the tunnel installs, do not
count. A host with neither, as in #10051, still skips ::/0. The
RouteExcludeAddress branch uses the same decision.

Reproduced in network namespaces with Xray 26.7.28: on a NAT66 client the
7.25.4 TUN inbound let curl -6 reach the echo server from the NAT66
address, while the inbound generated by this change sends it through the
proxy, with EnableIPv6Address on or off.

Fixes #10327

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Simplify: route ::/0 when EnableIPv6Address is on

Drop the rename and the gateway detection; keep HasGlobalIPv6Address
and let EnableIPv6Address override it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0142Bna7M2GhesX7P7d4CSrt

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 10:55:39 +08:00
DHR60 b501efbc45 Fix (#10326) 2026-10-10 09:14:30 +08:00
Miheichev Aleksandr Sergeevich 5ea8ae6420 fix(ui): wrap the pre-release label in the update dialog (#10303)
The per-core pre-release switch and its label sit in a horizontal
StackPanel inside a fixed-width column of the update dialog.
Translations longer than the column do not fit: in Avalonia the label
runs over the core name and the update status (Russian, French,
Hungarian, Persian), and in WPF it is cut off. Use a DockPanel so that
the label wraps inside its column, as the interval label of the
subscription window does since #10292. The dialog keeps its width.

In Avalonia the panel also had a vertical margin on top of the
margin of the switch, so the two rows with the switch were 8 px taller
than the other rows. Use a horizontal-only margin, as the WPF view
already does.
2026-10-07 11:11:27 +08:00
Miheichev Aleksandr Sergeevich fa9d4a4769 fix(ui): line up the backup dialog columns again (#10302)
#10292 made the label and button columns of the backup view Auto with
a minimum width, so that longer labels and buttons fit. Each of the two
cards sizes its own grid, so when the remote (WebDAV) buttons are wider
than the local ones, the local buttons no longer sit under the remote
ones and the separators of the two cards differ in length. This shows
in English and in every other language except Chinese.

Share the column widths of the two cards with SharedSizeGroup: both
cards line up again, and the long labels and buttons still fit. In
Avalonia the buttons stay centred in the shared column; in WPF they
fill it as before, so the local buttons get as wide as the remote ones.
2026-10-07 11:09:32 +08:00
DHR60 771ef3192a Update cert (#10308) 2026-10-07 11:04:58 +08:00
2dust f5747bb321 up 7.25.5 2026-10-05 16:43:51 +08:00
ariansharifi d099e44ff6 Fix subscription custom configs ignoring custom config socks port (#10296)
* Fix subscription custom configs ignoring custom config socks port

Full Xray/sing-box configs imported from a subscription no longer got the
subscription's "Custom config socks port" (SubItem.PreSocksPort) after
eff5845 and 9638b4c. 7.24.4 and earlier copied it onto every imported
config, and the Clash, Hysteria2 and raw-file paths still do.

Without it, GetPreSocksItem builds no pre-socks core for these profiles,
so they cannot be used in TUN mode. Setting the port on the profile is
not a workaround, because every subscription update re-creates the
profiles.

* Add regression test for subscription custom config socks port
2026-10-05 16:36:52 +08:00
DHR60 c63d271d16 Up TUnit.Assertions.Should (#10294) 2026-10-05 16:27:38 +08:00
Miheichev Aleksandr Sergeevich 6152cab494 fix(ui): make cut-off button labels and input hints readable (#10292)
* fix(ui): let update and backup buttons fit their labels

The update dialog gives its two buttons a fixed 100 px width, and the
backup view puts its buttons in a fixed 200 px grid column, so longer
labels are cut off, for example "Only Chec", "Check and" and "Backup
to remote (WebD" in English. Use MinWidth="100" for the update buttons
and an Auto column with MinWidth="200" for the backup buttons: short
labels keep their current size and long ones fit.

The "Automatic update interval (minutes)" label of the subscription
window is cut off the same way between the switch and the interval
field, so let it wrap.

* fix(ui): show cut-off input hints as tooltips

The hints of the previous and next proxy remarks fields in the
subscription window and of the server and log filters are wider than
their fields, so their end is cut off and there is no way to read it.
Show the same text as a tooltip, as the custom config socks port field
of the subscription window already does.

* fix(ui): let dialog buttons and backup labels fit longer translations

The Confirm and Cancel buttons of the editing and settings windows,
and the Reset button of the hotkey window, have a fixed 100 px width,
and the backup view puts its labels in a fixed 300 px column.
Translations longer than the English text are cut off there: the
Russian "Подтвердить" reads "Подтверди", and "Резервное копирование
на удалённый сервер (WebDAV)" loses its end. Use MinWidth="100" for
these buttons, as the message box already does with MinWidth="90",
and an Auto label column with MinWidth="300".
2026-10-05 16:27:19 +08:00
2dust 3187eeef79 Code clean 2026-10-04 11:41:34 +08:00
Miheichev Aleksandr Sergeevich 3364ec0e57 i18n(ru): translate the update dialog buttons in full (#10291)
Add the Russian string for menuCheckAndUpdate, which #10282 added to
ResUI.resx, as "Проверить и обновить". Restore the full Russian text
of menuCheckOnly ("Только проверить") and menuCheckUpdate ("Проверить
обновления"), which were shortened only to fit the fixed 100 px
buttons of the update dialog; they fit once the buttons are sized by
their labels.
2026-10-04 11:08:10 +08:00
dependabot[bot] 23033fa002 Bump TUnit from 1.68.17 to 1.72.16 (#10288)
---
updated-dependencies:
- dependency-name: TUnit
  dependency-version: 1.72.16
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-04 11:07:42 +08:00
Eliot the Cougar 125cbb18a1 Keep Xray startup logs visible in horizontal and vertical layouts (#10272)
* Keep Xray log content visible in separate log layouts

* Revert wpf

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-10-04 11:06:06 +08:00
2dust f85326cf0e Add per-core prerelease update selection
Replaces the global prerelease toggle with per-core prerelease options in both WPF and Avalonia update views, and persists selections via `CheckPreReleaseCoreTypes` in config. Update checks now resolve prerelease mode per selected core (including auto-check flow), and the sing-box max-version lock is removed to allow newer versions.
2026-10-03 14:58:45 +08:00
Tosd 3666fc549d Resx: clarify update button wording (#10282) 2026-10-02 15:08:27 +08:00
Miheichev Aleksandr Sergeevich a147249f95 i18n(ru): translate the Add [MASQUE] menu item (#10276)
Add the Russian string for menuAddMasqueServer, which #10233 added to
ResUI.resx. Without it the Russian UI shows this one item of the
Configuration menu in English. The text follows the other
Add [protocol] items: "Добавить сервер [MASQUE]".
2026-10-02 15:06:49 +08:00
Eliot the Cougar a28f68c3a7 Avoid reentrant log scrolling during Avalonia window dragging (#10271) 2026-10-01 19:55:30 +08:00
Ezra-Y 1a689f869c Fix macOS TUN interface name collisions (#10270) 2026-10-01 19:51:08 +08:00
DHR60 50023a5976 Xray tun features (#10264)
* Xray tun features

* Try sync
2026-10-01 14:55:09 +08:00
DHR60 c659ca8af8 Add MASQUE support (#10233)
* Add MASQUE support

* Add SettingsCoreType
2026-10-01 14:49:45 +08:00
2dust 7d6a967c18 up 7.25.4 2026-09-30 14:38:34 +08:00
2dust 6c2edcb382 Revert "Bump Avalonia.Desktop from 12.1.2 to 12.1.3 (#10216)"
This reverts commit e1cb99cd6e.
2026-09-30 14:34:07 +08:00
322 changed files with 32894 additions and 1388 deletions
+941
View File
@@ -0,0 +1,941 @@
name: build v2rayN.WebAPI
on:
workflow_call:
inputs:
release_tag:
required: false
type: string
default: ''
workflow_dispatch:
inputs:
release_tag:
description: 'Release tag (for example 7.x.y). Empty builds a development API package.'
required: false
default: ''
type: string
pull_request:
branches: [master]
paths:
- 'v2rayN/v2rayN.WebAPI/**'
- 'v2rayN/ServiceLib/**'
- 'v2rayN/ServiceLib.Tests/**'
- 'v2rayN/Directory.Build.*'
- 'v2rayN/Directory.Packages.props'
- 'v2rayN/v2rayN.slnx'
- 'global.json'
- '.github/workflows/build-WebAPI.yml'
- '.github/workflows/release-WebAPI.yml'
- '.github/workflows/build-all.yml'
- '.github/workflows/upload-sign.yml'
push:
branches: [master]
paths:
- 'v2rayN/v2rayN.WebAPI/**'
- 'v2rayN/ServiceLib/**'
- 'v2rayN/ServiceLib.Tests/**'
- 'v2rayN/Directory.Build.*'
- 'v2rayN/Directory.Packages.props'
- 'v2rayN/v2rayN.slnx'
- 'global.json'
- '.github/workflows/build-WebAPI.yml'
- '.github/workflows/release-WebAPI.yml'
- '.github/workflows/build-all.yml'
- '.github/workflows/upload-sign.yml'
permissions:
contents: read
jobs:
macos:
name: macOS WebAPI and ServiceLib tests
runs-on: macos-latest
timeout-minutes: 15
env:
TUNIT_MAX_PARALLEL_TESTS: 1
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-dotnet@v6
with:
dotnet-version: '10.x'
- name: Run ServiceLib.Tests
run: dotnet test --project v2rayN/ServiceLib.Tests/ServiceLib.Tests.csproj -c Release
- name: Run WebAPI.Tests
run: dotnet test --project v2rayN/v2rayN.WebAPI/Tests/v2rayN.WebAPI.Tests.csproj -c Release
identity:
name: Resolve API build identity
runs-on: ubuntu-24.04
outputs:
version: ${{ steps.resolve.outputs.version }}
tag: ${{ steps.resolve.outputs.tag }}
release: ${{ steps.resolve.outputs.release }}
build_date: ${{ steps.resolve.outputs.build_date }}
steps:
- name: Resolve release identity
id: resolve
shell: bash
env:
RELEASE_TAG: ${{ inputs.release_tag }}
run: |
set -euo pipefail
build_date="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
if [[ -z "$RELEASE_TAG" ]]; then
{
echo "version=0.0.0-dev"
echo "tag="
echo "release=false"
echo "build_date=$build_date"
} >> "$GITHUB_OUTPUT"
else
[[ "$RELEASE_TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
echo "Invalid release tag: $RELEASE_TAG" >&2
exit 1
}
{
echo "version=$RELEASE_TAG"
echo "tag=$RELEASE_TAG"
echo "release=true"
echo "build_date=$build_date"
} >> "$GITHUB_OUTPUT"
fi
verify:
name: Backend, ServiceLib, package smoke
runs-on: ubuntu-24.04
env:
TUNIT_MAX_PARALLEL_TESTS: 1
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-dotnet@v6
with:
dotnet-version: '10.x'
- name: Verify API build, backend and ServiceLib tests, native startup and package boundaries
run: bash v2rayN/v2rayN.WebAPI/Scripts/verify.sh "$RUNNER_TEMP/v2rayN.WebAPI-verify"
- name: Check patch whitespace
if: github.event_name == 'pull_request' || github.event_name == 'push' || github.event_name == 'workflow_dispatch'
shell: bash
env:
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before || '' }}
run: |
set -euo pipefail
base_sha="$BASE_SHA"
if [[ -z "$base_sha" ]]; then
base_sha="$(git rev-parse HEAD^)"
fi
if [[ "$base_sha" =~ ^0+$ ]]; then
empty_tree="$(git hash-object -t tree -w /dev/null)"
git diff --check "$empty_tree" HEAD
else
git diff --check "$base_sha...HEAD"
fi
windows:
name: Windows WebAPI build and runtime smoke
needs: identity
runs-on: windows-latest
env:
TUNIT_MAX_PARALLEL_TESTS: 1
WEB_VERSION: ${{ needs.identity.outputs.version }}
WEB_COMMIT: ${{ github.sha }}
WEB_BUILD_DATE: ${{ needs.identity.outputs.build_date }}
WEB_REPOSITORY: ${{ github.repository }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-dotnet@v6
with:
dotnet-version: '10.x'
- uses: actions/setup-python@v7
with:
python-version: '3.x'
- name: Restore WebAPI
shell: pwsh
run: dotnet restore v2rayN/v2rayN.WebAPI/v2rayN.WebAPI.csproj
- name: Restore WebAPI.Tests
shell: pwsh
run: dotnet restore v2rayN/v2rayN.WebAPI/Tests/v2rayN.WebAPI.Tests.csproj
- name: Restore ServiceLib.Tests
shell: pwsh
run: dotnet restore v2rayN/ServiceLib.Tests/ServiceLib.Tests.csproj
- name: Build WebAPI
shell: pwsh
run: dotnet build v2rayN/v2rayN.WebAPI/v2rayN.WebAPI.csproj --configuration Release --no-restore
- name: Run WebAPI.Tests
shell: pwsh
run: dotnet test --project v2rayN/v2rayN.WebAPI/Tests/v2rayN.WebAPI.Tests.csproj --configuration Release --no-restore
- name: Run ServiceLib.Tests
shell: pwsh
run: dotnet test --project v2rayN/ServiceLib.Tests/ServiceLib.Tests.csproj --configuration Release --no-restore
- name: Publish win-x64 self-contained single-file WebAPI
shell: pwsh
run: |
$publishDirectory = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-x64'
dotnet publish v2rayN/v2rayN.WebAPI/v2rayN.WebAPI.csproj `
--configuration Release `
--runtime win-x64 `
--self-contained true `
-p:PublishSingleFile=true `
-p:IncludeNativeLibrariesForSelfExtract=true `
-p:WebVersion=$env:WEB_VERSION `
-p:WebCommit=$env:WEB_COMMIT `
-p:WebBuildDate=$env:WEB_BUILD_DATE `
-p:WebRepository=$env:WEB_REPOSITORY `
--output $publishDirectory
$identity = [ordered]@{
product = 'v2rayN.WebAPI'
version = $env:WEB_VERSION
commit = $env:WEB_COMMIT
buildDate = $env:WEB_BUILD_DATE
rid = 'win-x64'
}
$json = $identity | ConvertTo-Json -Compress
[IO.File]::WriteAllText((Join-Path $publishDirectory 'v2rayN.WebAPI.build.json'), $json, [Text.UTF8Encoding]::new($false))
- name: Verify single-file publish output
shell: pwsh
run: |
$publishDirectory = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-x64'
$executable = Join-Path $publishDirectory 'v2rayN.WebAPI.exe'
if (-not (Test-Path -LiteralPath $executable -PathType Leaf)) {
throw "dotnet publish did not produce $executable"
}
if ((Get-Item -LiteralPath $executable).Length -eq 0) {
throw "Published executable is empty: $executable"
}
$looseDlls = @(Get-ChildItem -LiteralPath $publishDirectory -Filter '*.dll' -File -Recurse)
if ($looseDlls.Count -gt 0) {
throw "Single-file publish left loose DLLs: $($looseDlls.FullName -join ', ')"
}
- name: Smoke-test first startup, health endpoint and graceful shutdown
shell: pwsh
run: |
$publishDirectory = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-x64'
& ./v2rayN/v2rayN.WebAPI/Tests/Integration/Test-Windows.ps1 -Mode startup -PublishDirectory $publishDirectory
- name: Publish previous Windows fixture for update regression
if: inputs.release_tag != ''
shell: pwsh
run: |
$publishDirectory = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-x64-previous'
dotnet publish v2rayN/v2rayN.WebAPI/v2rayN.WebAPI.csproj `
--configuration Release `
--runtime win-x64 `
--self-contained true `
-p:PublishSingleFile=true `
-p:IncludeNativeLibrariesForSelfExtract=true `
-p:WebVersion=0.0.1-regression `
-p:WebCommit=$env:WEB_COMMIT `
-p:WebBuildDate=$env:WEB_BUILD_DATE `
-p:WebRepository=$env:WEB_REPOSITORY `
--output $publishDirectory
$identity = [ordered]@{
product = 'v2rayN.WebAPI'
version = '0.0.1-regression'
commit = $env:WEB_COMMIT
buildDate = $env:WEB_BUILD_DATE
rid = 'win-x64'
}
$json = $identity | ConvertTo-Json -Compress
[IO.File]::WriteAllText((Join-Path $publishDirectory 'v2rayN.WebAPI.build.json'), $json, [Text.UTF8Encoding]::new($false))
- name: Add the matching Windows Core bundle
if: inputs.release_tag != ''
shell: pwsh
run: |
$bundle = Join-Path $env:RUNNER_TEMP 'v2rayN-windows-64.zip'
$expanded = Join-Path $env:RUNNER_TEMP 'v2rayN-windows-64-core'
$publishDirectory = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-x64'
$previousDirectory = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-x64-previous'
Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/2dust/v2rayN-core-bin/refs/heads/master/v2rayN-windows-64.zip' -OutFile $bundle
Expand-Archive -LiteralPath $bundle -DestinationPath $expanded -Force
$sourceBin = Join-Path $expanded 'v2rayN-windows-64\bin'
if (-not (Test-Path -LiteralPath $sourceBin -PathType Container)) {
throw "Windows Core bundle is missing $sourceBin"
}
foreach ($directory in @($publishDirectory, $previousDirectory)) {
$destinationBin = Join-Path $directory 'bin'
New-Item -ItemType Directory -Path $destinationBin -Force | Out-Null
Copy-Item -Path (Join-Path $sourceBin '*') -Destination $destinationBin -Recurse -Force
}
foreach ($directory in @($publishDirectory, $previousDirectory)) {
$checkBin = Join-Path $directory 'bin'
foreach ($relative in @('xray\xray.exe','sing_box\sing-box.exe','mihomo\mihomo.exe','geoip.dat','geosite.dat','geoip.metadb','Country.mmdb')) {
$required = Join-Path $checkBin $relative
if (-not (Test-Path -LiteralPath $required -PathType Leaf)) { throw "Missing Windows Core asset: $required" }
}
}
$candidateBin = Join-Path $publishDirectory 'bin'
& (Join-Path $candidateBin 'xray\xray.exe') -version
if ($LASTEXITCODE -ne 0) { throw "xray.exe smoke failed: $LASTEXITCODE" }
& (Join-Path $candidateBin 'sing_box\sing-box.exe') version
if ($LASTEXITCODE -ne 0) { throw "sing-box.exe smoke failed: $LASTEXITCODE" }
& (Join-Path $candidateBin 'mihomo\mihomo.exe') -v
if ($LASTEXITCODE -ne 0) { throw "mihomo.exe smoke failed: $LASTEXITCODE" }
- name: Smoke-test Windows backup restore handoff and Core intent recovery
if: inputs.release_tag != ''
shell: pwsh
run: |
& ./v2rayN/v2rayN.WebAPI/Tests/Integration/Test-Windows.ps1 `
-Mode restore `
-PublishDirectory (Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-x64')
- name: Smoke-test Windows self-update replacement and rollback
if: inputs.release_tag != ''
shell: pwsh
run: |
& ./v2rayN/v2rayN.WebAPI/Tests/Integration/Test-Windows.ps1 `
-Mode update `
-PreviousPublishDirectory (Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-x64-previous') `
-CandidatePublishDirectory (Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-x64')
- name: Package and smoke-test Windows full-install and app-only ZIPs
if: inputs.release_tag != ''
shell: pwsh
run: |
$publishDirectory = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-x64'
$dist = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-x64-dist'
python v2rayN/v2rayN.WebAPI/Scripts/web-release-assets.py package --rid win-x64 --publish $publishDirectory --dist $dist
$fullZip = Join-Path $dist 'v2rayN-windows-64-WebAPI.zip'
$appOnlyZip = Join-Path $dist 'v2rayN-windows-64-WebAPI-update.zip'
if (-not (Test-Path -LiteralPath $fullZip -PathType Leaf) -or -not (Test-Path -LiteralPath $appOnlyZip -PathType Leaf)) {
throw 'Windows release ZIPs were not created.'
}
$extracted = Join-Path $env:RUNNER_TEMP 'v2rayN-windows-64-WebAPI-smoke'
Expand-Archive -LiteralPath $fullZip -DestinationPath $extracted -Force
& ./v2rayN/v2rayN.WebAPI/Tests/Integration/Test-Windows.ps1 -Mode startup -PublishDirectory $extracted
"Windows full-install ZIP: $fullZip"
"Windows app-only update ZIP: $appOnlyZip"
- uses: actions/upload-artifact@v7
if: inputs.release_tag != ''
with:
name: v2rayN.WebAPI-win-x64
path: ${{ runner.temp }}/v2rayN.WebAPI-win-x64-dist/*.zip
if-no-files-found: error
# The self-update replacement smoke stays on win-x64: the updater logic is RID-generic
# and the per-RID PE/ELF checks are covered by unit tests. This job proves the win-arm64
# build, first startup, health endpoint and graceful shutdown on native ARM64 Windows.
windows-arm64:
name: Windows ARM64 WebAPI build and runtime smoke
needs: identity
runs-on: windows-11-arm
env:
TUNIT_MAX_PARALLEL_TESTS: 1
WEB_VERSION: ${{ needs.identity.outputs.version }}
WEB_COMMIT: ${{ github.sha }}
WEB_BUILD_DATE: ${{ needs.identity.outputs.build_date }}
WEB_REPOSITORY: ${{ github.repository }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-dotnet@v6
with:
dotnet-version: '10.x'
- uses: actions/setup-python@v7
with:
python-version: '3.x'
- name: Restore WebAPI
shell: pwsh
run: dotnet restore v2rayN/v2rayN.WebAPI/v2rayN.WebAPI.csproj
- name: Restore WebAPI.Tests
shell: pwsh
run: dotnet restore v2rayN/v2rayN.WebAPI/Tests/v2rayN.WebAPI.Tests.csproj
- name: Restore ServiceLib.Tests
shell: pwsh
run: dotnet restore v2rayN/ServiceLib.Tests/ServiceLib.Tests.csproj
- name: Build WebAPI
shell: pwsh
run: dotnet build v2rayN/v2rayN.WebAPI/v2rayN.WebAPI.csproj --configuration Release --no-restore
- name: Run WebAPI.Tests
shell: pwsh
run: dotnet test --project v2rayN/v2rayN.WebAPI/Tests/v2rayN.WebAPI.Tests.csproj --configuration Release --no-restore
- name: Run ServiceLib.Tests
shell: pwsh
run: dotnet test --project v2rayN/ServiceLib.Tests/ServiceLib.Tests.csproj --configuration Release --no-restore
- name: Publish win-arm64 self-contained single-file WebAPI
shell: pwsh
run: |
$publishDirectory = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-arm64'
dotnet publish v2rayN/v2rayN.WebAPI/v2rayN.WebAPI.csproj `
--configuration Release `
--runtime win-arm64 `
--self-contained true `
-p:PublishSingleFile=true `
-p:IncludeNativeLibrariesForSelfExtract=true `
-p:WebVersion=$env:WEB_VERSION `
-p:WebCommit=$env:WEB_COMMIT `
-p:WebBuildDate=$env:WEB_BUILD_DATE `
-p:WebRepository=$env:WEB_REPOSITORY `
--output $publishDirectory
$identity = [ordered]@{
product = 'v2rayN.WebAPI'
version = $env:WEB_VERSION
commit = $env:WEB_COMMIT
buildDate = $env:WEB_BUILD_DATE
rid = 'win-arm64'
}
$json = $identity | ConvertTo-Json -Compress
[IO.File]::WriteAllText((Join-Path $publishDirectory 'v2rayN.WebAPI.build.json'), $json, [Text.UTF8Encoding]::new($false))
- name: Smoke-test first startup, health endpoint and graceful shutdown
shell: pwsh
run: |
$publishDirectory = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-arm64'
& ./v2rayN/v2rayN.WebAPI/Tests/Integration/Test-Windows.ps1 -Mode startup -PublishDirectory $publishDirectory
- name: Add the matching Windows ARM64 Core bundle
if: inputs.release_tag != ''
shell: pwsh
run: |
$bundle = Join-Path $env:RUNNER_TEMP 'v2rayN-windows-arm64.zip'
$expanded = Join-Path $env:RUNNER_TEMP 'v2rayN-windows-arm64-core'
$publishDirectory = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-arm64'
Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/2dust/v2rayN-core-bin/refs/heads/master/v2rayN-windows-arm64.zip' -OutFile $bundle
Expand-Archive -LiteralPath $bundle -DestinationPath $expanded -Force
$sourceBin = Join-Path $expanded 'v2rayN-windows-arm64\bin'
if (-not (Test-Path -LiteralPath $sourceBin -PathType Container)) {
throw "Windows ARM64 Core bundle is missing $sourceBin"
}
$destinationBin = Join-Path $publishDirectory 'bin'
New-Item -ItemType Directory -Path $destinationBin -Force | Out-Null
Copy-Item -Path (Join-Path $sourceBin '*') -Destination $destinationBin -Recurse -Force
foreach ($relative in @('xray\xray.exe','sing_box\sing-box.exe','mihomo\mihomo.exe','geoip.dat','geosite.dat','geoip.metadb','Country.mmdb')) {
$required = Join-Path $destinationBin $relative
if (-not (Test-Path -LiteralPath $required -PathType Leaf)) { throw "Missing Windows ARM64 Core asset: $required" }
}
& (Join-Path $destinationBin 'xray\xray.exe') -version
if ($LASTEXITCODE -ne 0) { throw "xray.exe smoke failed: $LASTEXITCODE" }
& (Join-Path $destinationBin 'sing_box\sing-box.exe') version
if ($LASTEXITCODE -ne 0) { throw "sing-box.exe smoke failed: $LASTEXITCODE" }
& (Join-Path $destinationBin 'mihomo\mihomo.exe') -v
if ($LASTEXITCODE -ne 0) { throw "mihomo.exe smoke failed: $LASTEXITCODE" }
- name: Package Windows ARM64 full-install and app-only ZIPs
if: inputs.release_tag != ''
shell: pwsh
run: |
$publishDirectory = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-arm64'
$dist = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-arm64-dist'
python v2rayN/v2rayN.WebAPI/Scripts/web-release-assets.py package --rid win-arm64 --publish $publishDirectory --dist $dist
$fullZip = Join-Path $dist 'v2rayN-windows-arm64-WebAPI.zip'
$appOnlyZip = Join-Path $dist 'v2rayN-windows-arm64-WebAPI-update.zip'
if (-not (Test-Path -LiteralPath $fullZip -PathType Leaf) -or -not (Test-Path -LiteralPath $appOnlyZip -PathType Leaf)) {
throw 'Windows ARM64 release ZIPs were not created.'
}
$extracted = Join-Path $env:RUNNER_TEMP 'v2rayN-windows-arm64-WebAPI-smoke'
Expand-Archive -LiteralPath $fullZip -DestinationPath $extracted -Force
& ./v2rayN/v2rayN.WebAPI/Tests/Integration/Test-Windows.ps1 -Mode startup -PublishDirectory $extracted
"Windows ARM64 full-install ZIP: $fullZip"
"Windows ARM64 app-only update ZIP: $appOnlyZip"
- uses: actions/upload-artifact@v7
if: inputs.release_tag != ''
with:
name: v2rayN.WebAPI-win-arm64
path: ${{ runner.temp }}/v2rayN.WebAPI-win-arm64-dist/*.zip
if-no-files-found: error
native:
name: Native API build (${{ matrix.rid }})
needs: identity
strategy:
fail-fast: false
matrix:
# Development builds keep x64 coverage; releases also validate ARM64.
# Only tested Linux RIDs are enabled; native RID/runner pairs are independent
# of Desktop workflows and the container platform matrix.
include: >-
${{ fromJSON(needs.identity.outputs.release == 'true'
&& '[{"rid":"linux-x64","runner":"ubuntu-24.04"},{"rid":"linux-arm64","runner":"ubuntu-24.04-arm"}]'
|| '[{"rid":"linux-x64","runner":"ubuntu-24.04"}]') }}
runs-on: ${{ matrix.runner }}
env:
RID: ${{ matrix.rid }}
V2RAYN_WEB_VERSION: ${{ needs.identity.outputs.version }}
V2RAYN_WEB_REPOSITORY: ${{ github.repository }}
V2RAYN_WEB_COMMIT: ${{ github.sha }}
V2RAYN_WEB_BUILD_DATE: ${{ needs.identity.outputs.build_date }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-dotnet@v6
with:
dotnet-version: '10.x'
- name: Publish native API
run: bash v2rayN/v2rayN.WebAPI/Scripts/publish-native.sh "$RID"
- name: Verify native executable
run: test -x "v2rayN/v2rayN.WebAPI/publish/$RID/v2rayN.WebAPI"
- name: Add the matching Linux Core bundle
if: needs.identity.outputs.release == 'true'
run: sh v2rayN/v2rayN.WebAPI/Scripts/fetch-core-bundle.sh "$RID" "v2rayN/v2rayN.WebAPI/publish/$RID/bin"
- name: Package API install and self-update ZIPs
if: needs.identity.outputs.release == 'true'
run: python3 v2rayN/v2rayN.WebAPI/Scripts/web-release-assets.py package --rid "$RID" --publish "v2rayN/v2rayN.WebAPI/publish/$RID" --dist dist
- name: Smoke-test bundled Cores
if: needs.identity.outputs.release == 'true'
run: |
set -euo pipefail
publish="v2rayN/v2rayN.WebAPI/publish/$RID"
"$publish/bin/xray/xray" -version
"$publish/bin/sing_box/sing-box" version
"$publish/bin/mihomo/mihomo" -v
- uses: actions/upload-artifact@v7
if: needs.identity.outputs.release == 'true'
with:
name: v2rayN.WebAPI-${{ matrix.rid }}
path: dist/*.zip
if-no-files-found: error
# linux-riscv64 runs natively on the RISE RISC-V runners with the community
# xujiegb/dotnet-riscv SDK; single-file publish is verified to work there.
native-riscv64:
name: Native API build (linux-riscv64)
needs: identity
if: needs.identity.outputs.release == 'true'
runs-on: ubuntu-24.04-riscv
container: debian:13
env:
RID: linux-riscv64
V2RAYN_WEB_VERSION: ${{ needs.identity.outputs.version }}
V2RAYN_WEB_REPOSITORY: ${{ github.repository }}
V2RAYN_WEB_COMMIT: ${{ github.sha }}
V2RAYN_WEB_BUILD_DATE: ${{ needs.identity.outputs.build_date }}
steps:
- name: Prepare tools (Debian)
shell: bash
run: |
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file ca-certificates python3 libc6-dev libgcc-s1 libstdc++6 zlib1g libatomic1 libicu76 || \
apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file ca-certificates python3 libc6-dev libgcc-s1 libstdc++6 zlib1g libatomic1 libicu72
- name: Checkout repo
shell: bash
env: { GITHUB_TOKEN: "${{ github.token }}" }
run: |
set -euo pipefail
rm -rf ./*
git init .
git config --global --add safe.directory "$PWD"
git remote add origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
git fetch --depth=1 origin "${GITHUB_SHA}"
git checkout FETCH_HEAD
- name: Install community .NET SDK (linux-riscv64)
shell: bash
run: |
set -euo pipefail
SDK=dotnet-sdk-10.0.111-linux-riscv64.tar.gz
curl -fL "https://github.com/xujiegb/dotnet-riscv/releases/download/10.0.111/$SDK" -o /tmp/$SDK
mkdir -p "$HOME/.dotnet"
tar -C "$HOME/.dotnet" -xzf /tmp/$SDK
echo "DOTNET_ROOT=$HOME/.dotnet" >> "$GITHUB_ENV"
echo "$HOME/.dotnet" >> "$GITHUB_PATH"
export DOTNET_ROOT="$HOME/.dotnet"
export PATH="$DOTNET_ROOT:$PATH"
dotnet --info
- name: Publish native API
shell: bash
run: |
set -euo pipefail
export DOTNET_ROOT="$HOME/.dotnet"
export PATH="$DOTNET_ROOT:$PATH"
bash v2rayN/v2rayN.WebAPI/Scripts/publish-native.sh "$RID"
test -x "v2rayN/v2rayN.WebAPI/publish/$RID/v2rayN.WebAPI"
- name: Add the matching Linux Core bundle
shell: bash
run: sh v2rayN/v2rayN.WebAPI/Scripts/fetch-core-bundle.sh "$RID" "v2rayN/v2rayN.WebAPI/publish/$RID/bin"
- name: Package API install and self-update ZIPs
shell: bash
run: python3 v2rayN/v2rayN.WebAPI/Scripts/web-release-assets.py package --rid "$RID" --publish "v2rayN/v2rayN.WebAPI/publish/$RID" --dist dist
- name: Smoke-test bundled Cores
shell: bash
run: |
set -euo pipefail
publish="v2rayN/v2rayN.WebAPI/publish/$RID"
"$publish/bin/xray/xray" -version
"$publish/bin/sing_box/sing-box" version
"$publish/bin/mihomo/mihomo" -v
- name: Smoke-test the published API (native riscv64)
shell: bash
run: |
set -euo pipefail
publish="v2rayN/v2rayN.WebAPI/publish/$RID"
"$publish/v2rayN.WebAPI" --help | head -n 5
export V2RAYN_WEB_API_KEY=ci-smoke-key-0123456789abcdef
export ASPNETCORE_URLS=http://127.0.0.1:5080
cd "$publish"
./v2rayN.WebAPI --foreground --no-open > /tmp/smoke.log 2>&1 &
app_pid=$!
echo "$app_pid" > /tmp/smoke.pid
echo "riscv64 API smoke application PID: $app_pid"
ok=0
for i in $(seq 1 90); do
if curl --noproxy '*' -fsS http://127.0.0.1:5080/api/health >/dev/null 2>&1; then ok=1; break; fi
if ! kill -0 "$app_pid" 2>/dev/null; then break; fi
sleep 1
done
if [ "$ok" != 1 ]; then
echo 'riscv64 API smoke did not become healthy'
tail -n 60 /tmp/smoke.log || true
kill -TERM "$app_pid" 2>/dev/null || true
sleep 1
kill -KILL "$app_pid" 2>/dev/null || true
exit 1
fi
kill -TERM "$app_pid"
stopped=0
for i in $(seq 1 30); do
if ! kill -0 "$app_pid" 2>/dev/null; then stopped=1; break; fi
sleep 1
done
if [ "$stopped" != 1 ]; then
echo 'riscv64 API smoke did not exit within 30s after SIGTERM'
tail -n 60 /tmp/smoke.log || true
kill -KILL "$app_pid" 2>/dev/null || true
exit 1
fi
set +e
wait "$app_pid"
exit_code=$?
set -e
if [ "$exit_code" -ne 0 ]; then
echo "riscv64 API smoke exited with status $exit_code after SIGTERM"
tail -n 60 /tmp/smoke.log || true
exit 1
fi
echo 'linux-riscv64 native API smoke OK'
- uses: actions/upload-artifact@v7
with:
name: v2rayN.WebAPI-linux-riscv64
path: dist/*.zip
if-no-files-found: error
# linux-loongarch64 publishes inside the upstream QEMU loong64 VM with the official
# Loongson .NET SDK; the VM runs the native smoke before the host packages the ZIPs.
native-loong64:
name: Native API build (linux-loongarch64)
needs: identity
if: needs.identity.outputs.release == 'true'
runs-on: ubuntu-26.04-arm
env:
RID: linux-loongarch64
V2RAYN_WEB_VERSION: ${{ needs.identity.outputs.version }}
V2RAYN_WEB_REPOSITORY: ${{ github.repository }}
V2RAYN_WEB_COMMIT: ${{ github.sha }}
V2RAYN_WEB_BUILD_DATE: ${{ needs.identity.outputs.build_date }}
QCOW2_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/debian13-loong64.qcow2
EFI_CODE_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/edk2-loongarch64-code.fd
EFI_VARS_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/edk2-loongarch64-vars.fd
QCOW2_IMAGE: debian13-loong64.qcow2
EFI_CODE: edk2-loongarch64-code.fd
EFI_VARS: edk2-loongarch64-vars.fd
QEMU_VERSION: 10.2.4
steps:
- name: Prepare host tools
shell: bash
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y git rsync qemu-utils qemu-system-common expect wget curl ca-certificates libfdt1 libglib2.0-0 libpixman-1-0 libslirp0 virtiofsd
sudo ln -sf /usr/libexec/virtiofsd /usr/local/bin/virtiofsd
virtiofsd --version || virtiofsd --help | head
- name: Host clone repo
shell: bash
env: { GITHUB_TOKEN: "${{ github.token }}" }
run: |
set -euo pipefail
rm -rf repo
git init repo
git -C repo config --global --add safe.directory "$PWD/repo" || true
git -C repo remote add origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
git -C repo fetch --depth=1 origin "${GITHUB_SHA}"
git -C repo checkout FETCH_HEAD
- name: Download QEMU prebuild
run: |
set -euo pipefail
wget -O qemu-linux-arm64-v8.2.tar.gz "https://github.com/xujiegb/qemu-linux-prebuild/releases/download/${QEMU_VERSION}/qemu-linux-arm64-v8.2.tar.gz"
tar -xzf qemu-linux-arm64-v8.2.tar.gz
mkdir -p "$HOME/qemu-install"
rsync -a qemu-linux-arm64-v8.2/ "$HOME/qemu-install/"
"$HOME/qemu-install/bin/qemu-system-loongarch64" --version
- name: Download loong64 qcow2 and EFI firmware
shell: bash
run: |
set -euo pipefail
wget -O "$QCOW2_IMAGE" "$QCOW2_URL"
wget -O "$EFI_CODE" "$EFI_CODE_URL"
wget -O "$EFI_VARS" "$EFI_VARS_URL"
qemu-img info "$QCOW2_IMAGE"
- name: Stage the VM build script and the matching Core bundle
shell: bash
run: |
set -euo pipefail
cat > webapi-loong.env <<EOF
WEB_VERSION=${V2RAYN_WEB_VERSION}
WEB_REPOSITORY=${V2RAYN_WEB_REPOSITORY}
WEB_COMMIT=${V2RAYN_WEB_COMMIT}
WEB_BUILD_DATE=${V2RAYN_WEB_BUILD_DATE}
EOF
cat > webapi-loong-run.sh <<'SCRIPT'
#!/usr/bin/env bash
set +e
set -o pipefail
source /workspace/webapi-loong.env
export DOTNET_ROOT=/root/.dotnet
export PATH="$DOTNET_ROOT:$PATH"
mkdir -p /root/.dotnet
SDK=dotnet-sdk-10.0.111-linux-loongarch64.tar.gz
curl -fL "https://github.com/loongson/dotnet/releases/download/v10.0.111-loongarch64/$SDK" -o /tmp/$SDK || exit 9
tar -C /root/.dotnet -xzf /tmp/$SDK || exit 9
dotnet --info >/dev/null 2>&1 || exit 9
cd /root/v2rayN-src || exit 9
mkdir -p /workspace/publish/linux-loongarch64
V2RAYN_WEB_VERSION="$WEB_VERSION" V2RAYN_WEB_REPOSITORY="$WEB_REPOSITORY" V2RAYN_WEB_COMMIT="$WEB_COMMIT" V2RAYN_WEB_BUILD_DATE="$WEB_BUILD_DATE" \
bash v2rayN/v2rayN.WebAPI/Scripts/publish-native.sh linux-loongarch64 /workspace/publish/linux-loongarch64 2>&1 | tee /tmp/publish.log
rc=$?
if [ $rc -eq 0 ]; then
mkdir -p /workspace/publish/linux-loongarch64/bin
cp -a /workspace/core-bundle/. /workspace/publish/linux-loongarch64/bin/
# Smoke a VM-local copy: the shared publish tree must stay exactly the packaging
# input, without runtime lock/data files owned by the VM user.
rm -rf /root/websmoke
cp -a /workspace/publish/linux-loongarch64 /root/websmoke
cd /root/websmoke || exit 9
./bin/xray/xray -version >/dev/null || rc=9
./bin/sing_box/sing-box version >/dev/null || rc=9
./bin/mihomo/mihomo -v >/dev/null || rc=9
./v2rayN.WebAPI --help >/dev/null || rc=9
V2RAYN_WEB_API_KEY=ci-smoke-key-0123456789abcdef ASPNETCORE_URLS=http://127.0.0.1:5080 ./v2rayN.WebAPI --foreground --no-open > /tmp/smoke.log 2>&1 &
APP=$!
echo "$APP" > /tmp/smoke.pid
ok=0
for i in $(seq 1 90); do
if curl --noproxy '*' -fsS http://127.0.0.1:5080/api/health >/dev/null 2>&1; then ok=1; break; fi
if ! kill -0 "$APP" 2>/dev/null; then break; fi
sleep 1
done
if [ "$ok" != 1 ]; then
echo 'loongarch64 API smoke did not become healthy'
tail -n 60 /tmp/smoke.log
kill -KILL "$APP" 2>/dev/null || true
rc=9
fi
if [ "$rc" = 0 ]; then
kill -TERM "$APP"
exited=0
for i in $(seq 1 60); do
if ! kill -0 "$APP" 2>/dev/null; then exited=1; break; fi
sleep 1
done
if [ "$exited" != 1 ]; then
echo 'loongarch64 API smoke did not exit within 60s after SIGTERM'
tail -n 60 /tmp/smoke.log
kill -KILL "$APP" 2>/dev/null || true
rc=9
else
wait "$APP"
app_rc=$?
if [ "$app_rc" != 0 ]; then
echo "loongarch64 API smoke exited with status $app_rc after SIGTERM"
tail -n 60 /tmp/smoke.log
rc=9
fi
fi
fi
fi
if [ "$rc" != 0 ]; then echo '=== publish.log ==='; tail -n 120 /tmp/publish.log 2>/dev/null; fi
echo "__WEBAPI_DONE__$rc"
SCRIPT
bash repo/v2rayN/v2rayN.WebAPI/Scripts/fetch-core-bundle.sh "$RID" "$GITHUB_WORKSPACE/core-bundle"
ls -la "$GITHUB_WORKSPACE/core-bundle" | head -n 15
- name: Build WebAPI loongarch64 in the VM (publish + native smoke)
shell: bash
timeout-minutes: 180
run: |
set -euo pipefail
mkdir -p "$GITHUB_WORKSPACE/dist"
export VIRTIOFSD_SOCKET="$RUNNER_TEMP/virtiofs-webapi-loongarch64.sock"
rm -f "$VIRTIOFSD_SOCKET"
sudo virtiofsd --socket-path="$VIRTIOFSD_SOCKET" --shared-dir="$GITHUB_WORKSPACE" --cache=auto &
VIRTIOFSD_PID=$!
trap 'sudo kill "$VIRTIOFSD_PID" 2>/dev/null || true; rm -f "$VIRTIOFSD_SOCKET"' EXIT
for i in {1..100}; do
[[ -S "$VIRTIOFSD_SOCKET" ]] && break
sleep 0.1
done
[[ -S "$VIRTIOFSD_SOCKET" ]] || { echo "virtiofsd socket not created"; exit 1; }
sudo chown "$USER:$(id -gn)" "$VIRTIOFSD_SOCKET" || true
sudo chmod 660 "$VIRTIOFSD_SOCKET" || true
expect <<'EOF'
log_user 1
set timeout 10800
set qemu_bin "$env(HOME)/qemu-install/bin/qemu-system-loongarch64"
set qemu_rom_dir "$env(HOME)/qemu-install/share/qemu"
set virtiofs_socket $env(VIRTIOFSD_SOCKET)
set qcow2 $env(QCOW2_IMAGE)
set efi_code $env(EFI_CODE)
set efi_vars $env(EFI_VARS)
proc wait_prompt {} {
expect {
-re "__CI_PROMPT__# " {}
timeout { exit 1 }
eof { exit 1 }
}
}
proc run_cmd {cmd} {
send -- "$cmd\r"
wait_prompt
}
spawn $qemu_bin \
-L $qemu_rom_dir \
-machine virt \
-accel tcg,thread=multi,tb-size=2048 \
-cpu la464 \
-m 9216 \
-object memory-backend-memfd,id=mem0,size=9216M,share=on \
-numa node,memdev=mem0 \
-smp 4 \
-drive if=pflash,format=raw,unit=0,file=$efi_code,readonly=on \
-drive if=pflash,format=raw,unit=1,file=$efi_vars \
-device virtio-blk-pci,drive=hd0,bootindex=1 \
-drive if=none,media=disk,id=hd0,file=$qcow2,format=qcow2 \
-netdev user,id=net0 \
-device virtio-net-pci-non-transitional,netdev=net0 \
-chardev socket,id=charfs0,path=$virtiofs_socket \
-device vhost-user-fs-pci,chardev=charfs0,tag=workspace \
-display none \
-serial stdio \
-monitor none
expect -re "login:|debian login:"
send -- "root\r"
expect -re "Password:|密码:|密码:"
send -- "password\r"
expect {
-re "# " {}
timeout { exit 1 }
eof { exit 1 }
}
send -- "export TERM=dumb; export PS1='__CI_PROMPT__# '\r"
wait_prompt
run_cmd "mkdir -p /workspace"
run_cmd "mount -t virtiofs workspace /workspace"
run_cmd "IFACE=\$(ip -o link show | awk -F': ' '\$2 != \"lo\" {print \$2; exit}') ; ip link set \$IFACE up || true"
run_cmd "dhclient \$IFACE || true"
run_cmd "printf 'nameserver 10.0.2.3\nnameserver 1.1.1.1\n' >/etc/resolv.conf"
run_cmd "apt-get update || apt-get update || apt-get update"
run_cmd "DEBIAN_FRONTEND=noninteractive apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file ca-certificates python3 libc6-dev libgcc-s1 libstdc++6 zlib1g libatomic1 libicu76 || DEBIAN_FRONTEND=noninteractive apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file ca-certificates python3 libc6-dev libgcc-s1 libstdc++6 zlib1g libatomic1"
run_cmd "rm -rf /root/v2rayN-src"
run_cmd "rsync -a /workspace/repo/ /root/v2rayN-src/"
run_cmd "cd /root/v2rayN-src && git config --global --add safe.directory /root/v2rayN-src"
send -- "bash /workspace/webapi-loong-run.sh\r"
expect {
-re "__WEBAPI_DONE__0" { send -- "poweroff\r" }
-re "__WEBAPI_DONE__" { exit 1 }
timeout { exit 1 }
eof { exit 1 }
}
EOF
- name: Verify the loongarch64 publish tree
shell: bash
run: |
set -euo pipefail
publish="$GITHUB_WORKSPACE/publish/linux-loongarch64"
test -x "$publish/v2rayN.WebAPI"
test -f "$publish/v2rayN.WebAPI.build.json"
unreadable="$(find "$publish" ! -readable -print -quit)"
if [ -n "$unreadable" ]; then
echo "The loongarch64 publish tree contains unreadable entries:"
find "$publish" ! -readable -print | head -n 20
exit 1
fi
echo "loongarch64 publish tree is complete and readable"
- name: Package API install and self-update ZIPs
shell: bash
run: |
set -euo pipefail
python3 repo/v2rayN/v2rayN.WebAPI/Scripts/web-release-assets.py package --rid "$RID" --publish "$GITHUB_WORKSPACE/publish/linux-loongarch64" --dist "$GITHUB_WORKSPACE/dist"
ls -la "$GITHUB_WORKSPACE/dist"
- uses: actions/upload-artifact@v7
with:
name: v2rayN.WebAPI-linux-loongarch64
path: dist/*.zip
if-no-files-found: error
container:
name: API Containerfile (${{ matrix.rid }})
needs: identity
strategy:
fail-fast: false
matrix:
# Development builds keep amd64 coverage; releases also validate ARM64.
include: >-
${{ fromJSON(needs.identity.outputs.release == 'true'
&& '[{"rid":"linux-x64","platform":"linux/amd64"},{"rid":"linux-arm64","platform":"linux/arm64"}]'
|| '[{"rid":"linux-x64","platform":"linux/amd64"}]') }}
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: docker/setup-buildx-action@v4
- name: Build API container (no push)
uses: docker/build-push-action@v7
env:
DOCKER_BUILD_RECORD_UPLOAD: false
with:
context: .
file: ./v2rayN/v2rayN.WebAPI/Containerfile
platforms: ${{ matrix.platform }}
tags: v2rayn-webapi:ci
build-args: |
RID=${{ matrix.rid }}
WEB_VERSION=${{ needs.identity.outputs.version }}
WEB_REPOSITORY=${{ github.repository }}
WEB_COMMIT=${{ github.sha }}
WEB_BUILD_DATE=${{ needs.identity.outputs.build_date }}
push: false
release-assets:
name: Assemble API release assets
if: needs.identity.outputs.release == 'true'
needs: [identity, verify, windows, windows-arm64, native, native-riscv64, native-loong64, container]
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/download-artifact@v8
with:
pattern: v2rayN.WebAPI-*
path: release-assets
merge-multiple: true
- name: Build and verify WebAPI-update.json
env:
RELEASE_TAG: ${{ needs.identity.outputs.tag }}
run: |
set -euo pipefail
python3 v2rayN/v2rayN.WebAPI/Scripts/web-update-manifest.py write \
--version "$RELEASE_TAG" \
--repository "${{ github.repository }}" \
--commit "${{ github.sha }}" \
--build-date "${{ needs.identity.outputs.build_date }}" \
--dist release-assets \
--output release-assets/WebAPI-update.json
python3 v2rayN/v2rayN.WebAPI/Scripts/web-update-manifest.py verify \
--manifest release-assets/WebAPI-update.json \
--dist release-assets \
--repository "${{ github.repository }}"
- name: Smoke-test full API ZIP
run: bash v2rayN/v2rayN.WebAPI/Tests/Integration/test-full-zip-runtime.sh release-assets/v2rayN-linux-64-WebAPI.zip
- uses: actions/upload-artifact@v7
with:
name: WebAPI-release-packages
path: release-assets/*.zip
if-no-files-found: error
- uses: actions/upload-artifact@v7
with:
name: WebAPI-release-manifest
path: release-assets/WebAPI-update.json
if-no-files-found: error
+7
View File
@@ -20,6 +20,13 @@ jobs:
secrets:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
release-WebAPI:
if: inputs.release_tag != ''
uses: ./.github/workflows/release-WebAPI.yml
with:
release_tag: ${{ inputs.release_tag }}
secrets: inherit
update:
runs-on: ubuntu-latest
steps:
+68 -262
View File
@@ -29,6 +29,22 @@ jobs:
release_tag: ${{ inputs.release_tag }}
secrets: inherit
build-aot:
uses: ./.github/workflows/build.yml
with:
target: linux
isAot: true
release-aot-zip:
if: inputs.release_tag != ''
needs: build-aot
uses: ./.github/workflows/package-zip.yml
with:
target: linux
release_tag: ${{ inputs.release_tag }}
isAot: true
secrets: inherit
deb:
name: build and release deb x64 & arm64
if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
@@ -205,149 +221,45 @@ jobs:
deb-loong64:
name: build and release deb loong64
if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
runs-on: ubuntu-26.04-arm
runs-on: ubuntu-26.04
env:
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
QCOW2_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/debian13-loong64.qcow2
EFI_CODE_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/edk2-loongarch64-code.fd
EFI_VARS_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/edk2-loongarch64-vars.fd
QCOW2_IMAGE: debian13-loong64.qcow2
EFI_CODE: edk2-loongarch64-code.fd
EFI_VARS: edk2-loongarch64-vars.fd
QEMU_VERSION: 10.2.4
DOCKER_IMAGE: ghcr.io/loong64/debian:trixie
steps:
- name: Prepare host tools
shell: bash
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y git rsync qemu-utils qemu-system-common expect wget curl ca-certificates libfdt1 libglib2.0-0 libpixman-1-0 libslirp0 virtiofsd
sudo ln -sf /usr/libexec/virtiofsd /usr/local/bin/virtiofsd
virtiofsd --version || virtiofsd --help | head
- name: Host clone repo
shell: bash
env: { GITHUB_TOKEN: "${{ github.token }}" }
run: |
set -euo pipefail
rm -rf repo
git clone --no-single-branch --tags "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" repo
- name: Download QEMU prebuild
run: |
set -euo pipefail
wget -O qemu-linux-arm64-v8.2.tar.gz "https://github.com/xujiegb/qemu-linux-prebuild/releases/download/${QEMU_VERSION}/qemu-linux-arm64-v8.2.tar.gz"
tar -xzf qemu-linux-arm64-v8.2.tar.gz
mkdir -p "$HOME/qemu-install"
rsync -a qemu-linux-arm64-v8.2/ "$HOME/qemu-install/"
"$HOME/qemu-install/bin/qemu-system-loongarch64" --version
- name: Download loong64 qcow2 and EFI firmware
shell: bash
run: |
set -euo pipefail
wget -O "$QCOW2_IMAGE" "$QCOW2_URL"
wget -O "$EFI_CODE" "$EFI_CODE_URL"
wget -O "$EFI_VARS" "$EFI_VARS_URL"
qemu-img info "$QCOW2_IMAGE"
- name: Build loong64 DEB in VM through serial console
- name: Checkout repo
uses: actions/checkout@v7
with:
submodules: recursive
fetch-depth: 0
- name: Setup QEMU
uses: docker/setup-qemu-action@v4
- name: Package DEB (Debian-family)
shell: bash
timeout-minutes: 180
env: { RELEASE_TAG: "${{ env.RELEASE_TAG }}" }
run: |
set -euo pipefail
mkdir -p "$GITHUB_WORKSPACE/dist/deb-loong64"
export VIRTIOFSD_SOCKET="$RUNNER_TEMP/virtiofs-deb-loong64.sock"
rm -f "$VIRTIOFSD_SOCKET"
sudo virtiofsd --socket-path="$VIRTIOFSD_SOCKET" --shared-dir="$GITHUB_WORKSPACE" --cache=auto &
VIRTIOFSD_PID=$!
trap 'sudo kill "$VIRTIOFSD_PID" 2>/dev/null || true; rm -f "$VIRTIOFSD_SOCKET"' EXIT
for i in {1..100}; do
[[ -S "$VIRTIOFSD_SOCKET" ]] && break
sleep 0.1
done
[[ -S "$VIRTIOFSD_SOCKET" ]] || { echo "virtiofsd socket not created"; exit 1; }
sudo chown "$USER:$(id -gn)" "$VIRTIOFSD_SOCKET" || true
sudo chmod 660 "$VIRTIOFSD_SOCKET" || true
expect <<'EOF'
log_user 1
set timeout -1
set release_tag $env(RELEASE_TAG)
set qemu_bin "$env(HOME)/qemu-install/bin/qemu-system-loongarch64"
set qemu_rom_dir "$env(HOME)/qemu-install/share/qemu"
set virtiofs_socket $env(VIRTIOFSD_SOCKET)
set qcow2 $env(QCOW2_IMAGE)
set efi_code $env(EFI_CODE)
set efi_vars $env(EFI_VARS)
proc wait_prompt {} {
expect {
-re "__CI_PROMPT__# " {}
timeout { exit 1 }
eof { exit 1 }
}
}
proc run_cmd {cmd} {
send -- "$cmd\r"
wait_prompt
}
spawn $qemu_bin \
-L $qemu_rom_dir \
-machine virt \
-accel tcg,thread=multi,tb-size=2048 \
-cpu la464 \
-m 9216 \
-object memory-backend-memfd,id=mem0,size=9216M,share=on \
-numa node,memdev=mem0 \
-smp 4 \
-drive if=pflash,format=raw,unit=0,file=$efi_code,readonly=on \
-drive if=pflash,format=raw,unit=1,file=$efi_vars \
-device virtio-blk-pci,drive=hd0,bootindex=1 \
-drive if=none,media=disk,id=hd0,file=$qcow2,format=qcow2 \
-netdev user,id=net0 \
-device virtio-net-pci-non-transitional,netdev=net0 \
-chardev socket,id=charfs0,path=$virtiofs_socket \
-device vhost-user-fs-pci,chardev=charfs0,tag=workspace \
-display none \
-serial stdio \
-monitor none
expect -re "login:|debian login:"
send -- "root\r"
expect -re "Password:|密码:|密码:"
send -- "password\r"
expect {
-re "# " {}
timeout { exit 1 }
eof { exit 1 }
}
send -- "export TERM=dumb; export PS1='__CI_PROMPT__# '\r"
wait_prompt
run_cmd "mkdir -p /workspace"
run_cmd "mount -t virtiofs workspace /workspace"
run_cmd "IFACE=\$(ip -o link show | awk -F': ' '\$2 != \"lo\" {print \$2; exit}') ; ip link set \$IFACE up || true"
run_cmd "dhclient \$IFACE || true"
run_cmd "printf 'nameserver 10.0.2.3\nnameserver 1.1.1.1\n' >/etc/resolv.conf"
run_cmd "apt-get update || apt-get update || apt-get update"
run_cmd "DEBIAN_FRONTEND=noninteractive apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file ca-certificates desktop-file-utils xdg-utils fakeroot dpkg-dev gcc make libc6-dev libgcc-s1 libstdc++6 zlib1g libatomic1 fuse3"
run_cmd "rm -rf /root/v2rayN-src"
run_cmd "rsync -a --info=progress2 /workspace/repo/ /root/v2rayN-src/"
run_cmd "cd /root/v2rayN-src && git config --global --add safe.directory /root/v2rayN-src"
run_cmd "cd /root/v2rayN-src && chmod 755 package-debian-loong.sh"
send -- "cd /root/v2rayN-src; cat >/tmp/run-loong-build.sh <<'SCRIPT'\nset +e\nset -o pipefail\nbash -x ./package-debian-loong.sh \"\$RELEASE_TAG\" 2>&1 | tee /tmp/build.log\nrc=\$?\nmkdir -p /workspace/dist/deb-loong64\ncp -av /root/debbuild/*.deb /workspace/dist/deb-loong64/ 2>/dev/null || true\necho __BUILD_DONE__\$rc\nSCRIPT\nRELEASE_TAG=\"$release_tag\" bash /tmp/run-loong-build.sh\r"
expect {
-re "__BUILD_DONE__0" { send -- "poweroff\r" }
default { exit 1 }
}
EOF
- name: Collect DEBs
shell: bash
docker run --rm \
--privileged \
--platform linux/loong64 \
--env DEBIAN_FRONTEND=noninteractive \
--env DOTNET_EnableWriteXorExecute=0 \
--volume "${{ github.workspace }}:/root/v2rayN-src" \
--volume "$HOME/debbuild:/root/debbuild" \
--workdir /root/v2rayN-src \
"$DOCKER_IMAGE" bash -c "
apt-get update && \
apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file ca-certificates desktop-file-utils xdg-utils fakeroot dpkg-dev gcc make libc6-dev libgcc-s1 libstdc++6 zlib1g libatomic1 && \
git config --global --add safe.directory /root/v2rayN-src && \
./package-debian-loong.sh ${RELEASE_TAG}"
- name: Collect DEBs into workspace
run: |
set -euo pipefail
mkdir -p "$GITHUB_WORKSPACE/dist/deb-loong64"
rsync -av "$HOME/debbuild/" "$GITHUB_WORKSPACE/dist/deb-loong64/" || true
find "$GITHUB_WORKSPACE/dist/deb-loong64" -name "*.deb" -exec mv {} "$GITHUB_WORKSPACE/dist/deb-loong64/v2rayN-linux-loong64.deb" \; || true
echo "==== Dist tree ===="
ls -R "$GITHUB_WORKSPACE/dist/deb-loong64"
ls -R "$GITHUB_WORKSPACE/dist/deb-loong64" || true
- name: Upload DEB artifact
uses: actions/upload-artifact@v7
with: { name: release-deb-loong64, path: "dist/deb-loong64/**/*.deb", if-no-files-found: error }
@@ -355,149 +267,43 @@ jobs:
rpm-loong64:
name: build and release rpm loong64
if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
runs-on: ubuntu-26.04-arm
runs-on: ubuntu-26.04
env:
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
QCOW2_URL: https://github.com/xujiegb/fedora-loong64-qcow2/releases/download/43/fedora43-loong64.qcow2
EFI_CODE_URL: https://github.com/xujiegb/fedora-loong64-qcow2/releases/download/43/edk2-loongarch64-code.fd
EFI_VARS_URL: https://github.com/xujiegb/fedora-loong64-qcow2/releases/download/43/edk2-loongarch64-vars.fd
QCOW2_IMAGE: fedora43-loong64.qcow2
EFI_CODE: edk2-loongarch64-code.fd
EFI_VARS: edk2-loongarch64-vars.fd
QEMU_VERSION: 10.2.4
DOCKER_IMAGE: ghcr.io/fedora-remix-loongarch/mini-image:f43
steps:
- name: Prepare host tools
shell: bash
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y git rsync qemu-utils qemu-system-common expect wget curl ca-certificates libfdt1 libglib2.0-0 libpixman-1-0 libslirp0 virtiofsd
sudo ln -sf /usr/libexec/virtiofsd /usr/local/bin/virtiofsd
virtiofsd --version || virtiofsd --help | head
- name: Host clone repo
shell: bash
env: { GITHUB_TOKEN: "${{ github.token }}" }
run: |
set -euo pipefail
rm -rf repo
git clone --no-single-branch --tags "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" repo
- name: Download QEMU prebuild
run: |
set -euo pipefail
wget -O qemu-linux-arm64-v8.2.tar.gz "https://github.com/xujiegb/qemu-linux-prebuild/releases/download/${QEMU_VERSION}/qemu-linux-arm64-v8.2.tar.gz"
tar -xzf qemu-linux-arm64-v8.2.tar.gz
mkdir -p "$HOME/qemu-install"
rsync -a qemu-linux-arm64-v8.2/ "$HOME/qemu-install/"
"$HOME/qemu-install/bin/qemu-system-loongarch64" --version
- name: Download loong64 qcow2 and EFI firmware
shell: bash
run: |
set -euo pipefail
wget -O "$QCOW2_IMAGE" "$QCOW2_URL"
wget -O "$EFI_CODE" "$EFI_CODE_URL"
wget -O "$EFI_VARS" "$EFI_VARS_URL"
qemu-img info "$QCOW2_IMAGE"
- name: Build loong64 RPM in VM through serial console
- name: Checkout repo
uses: actions/checkout@v7
with:
submodules: recursive
fetch-depth: 0
- name: Setup QEMU
uses: docker/setup-qemu-action@v4
- name: Build loong64 RPM
shell: bash
timeout-minutes: 180
env: { RELEASE_TAG: "${{ env.RELEASE_TAG }}" }
run: |
set -euo pipefail
mkdir -p "$GITHUB_WORKSPACE/dist/rpm-loong64"
export VIRTIOFSD_SOCKET="$RUNNER_TEMP/virtiofs-rpm-loong64.sock"
rm -f "$VIRTIOFSD_SOCKET"
sudo virtiofsd --socket-path="$VIRTIOFSD_SOCKET" --shared-dir="$GITHUB_WORKSPACE" --cache=auto &
VIRTIOFSD_PID=$!
trap 'sudo kill "$VIRTIOFSD_PID" 2>/dev/null || true; rm -f "$VIRTIOFSD_SOCKET"' EXIT
for i in {1..100}; do
[[ -S "$VIRTIOFSD_SOCKET" ]] && break
sleep 0.1
done
[[ -S "$VIRTIOFSD_SOCKET" ]] || { echo "virtiofsd socket not created"; exit 1; }
sudo chown "$USER:$(id -gn)" "$VIRTIOFSD_SOCKET" || true
sudo chmod 660 "$VIRTIOFSD_SOCKET" || true
expect <<'EOF'
log_user 1
set timeout -1
set release_tag $env(RELEASE_TAG)
set qemu_bin "$env(HOME)/qemu-install/bin/qemu-system-loongarch64"
set qemu_rom_dir "$env(HOME)/qemu-install/share/qemu"
set virtiofs_socket $env(VIRTIOFSD_SOCKET)
set qcow2 $env(QCOW2_IMAGE)
set efi_code $env(EFI_CODE)
set efi_vars $env(EFI_VARS)
proc wait_prompt {} {
expect {
-re "__CI_PROMPT__# " {}
timeout { exit 1 }
eof { exit 1 }
}
}
proc run_cmd {cmd} {
send -- "$cmd\r"
wait_prompt
}
spawn $qemu_bin \
-L $qemu_rom_dir \
-machine virt \
-accel tcg,thread=multi,tb-size=2048 \
-cpu la464 \
-m 9216 \
-object memory-backend-memfd,id=mem0,size=9216M,share=on \
-numa node,memdev=mem0 \
-smp 4 \
-drive if=pflash,format=raw,unit=0,file=$efi_code,readonly=on \
-drive if=pflash,format=raw,unit=1,file=$efi_vars \
-device virtio-blk-pci,drive=hd0,bootindex=1 \
-drive if=none,media=disk,id=hd0,file=$qcow2,format=qcow2 \
-netdev user,id=net0 \
-device virtio-net-pci-non-transitional,netdev=net0 \
-chardev socket,id=charfs0,path=$virtiofs_socket \
-device vhost-user-fs-pci,chardev=charfs0,tag=workspace \
-display none \
-serial stdio \
-monitor none
expect -re "login:|fedora login:"
send -- "root\r"
expect -re "Password:|密码:|密码:"
send -- "password\r"
expect {
-re "# " {}
timeout { exit 1 }
eof { exit 1 }
}
send -- "export TERM=dumb; export PS1='__CI_PROMPT__# '\r"
wait_prompt
run_cmd "mkdir -p /workspace"
run_cmd "mount -t virtiofs workspace /workspace"
run_cmd "IFACE=\$(ip -o link show | awk -F': ' '\$2 != \"lo\" {print \$2; exit}') ; ip link set \$IFACE up || true"
run_cmd "dhclient \$IFACE || true"
run_cmd "printf 'nameserver 10.0.2.3\nnameserver 1.1.1.1\n' >/etc/resolv.conf"
run_cmd "dnf -y makecache || dnf -y makecache || dnf -y makecache"
run_cmd "dnf -y install sudo git rpm-build rpmdevtools dnf-plugins-core rsync findutils tar gzip unzip which curl jq file ca-certificates fuse3 --nogpgcheck"
run_cmd "rm -rf /root/v2rayN-src"
run_cmd "rsync -a --info=progress2 /workspace/repo/ /root/v2rayN-src/"
run_cmd "cd /root/v2rayN-src && git config --global --add safe.directory /root/v2rayN-src"
run_cmd "cd /root/v2rayN-src && chmod 755 package-rhel-loong.sh"
send -- "cd /root/v2rayN-src; cat >/tmp/run-loong-rpm-build.sh <<'SCRIPT'\nset +e\nset -o pipefail\nbash -x ./package-rhel-loong.sh \"\$RELEASE_TAG\" 2>&1 | tee /tmp/build.log\nrc=\$?\nmkdir -p /workspace/dist/rpm-loong64\nfind /root/rpmbuild/RPMS -name '*.rpm' -exec cp -av {} /workspace/dist/rpm-loong64/ \\; 2>/dev/null || true\necho __BUILD_DONE__\$rc\nSCRIPT\nRELEASE_TAG=\"$release_tag\" bash /tmp/run-loong-rpm-build.sh\r"
expect {
-re "__BUILD_DONE__0" { send -- "poweroff\r" }
default { exit 1 }
}
EOF
- name: Collect RPMs
shell: bash
docker run --rm \
--privileged \
--platform linux/loong64 \
--env DOTNET_EnableWriteXorExecute=0 \
--volume "${{ github.workspace }}:/root/v2rayN-src" \
--volume "$HOME/rpmbuild/RPMS:/root/rpmbuild/RPMS" \
--workdir /root/v2rayN-src \
"$DOCKER_IMAGE" bash -c "
dnf -y makecache && \
dnf -y install sudo git rpm-build rpmdevtools dnf-plugins-core rsync findutils tar gzip unzip which curl jq file ca-certificates fuse3 && \
git config --global --add safe.directory /root/v2rayN-src && \
./package-rhel-loong.sh ${RELEASE_TAG}"
- name: Collect RPMs into workspace
run: |
set -euo pipefail
mkdir -p "$GITHUB_WORKSPACE/dist/rpm-loong64"
rsync -av "$HOME/rpmbuild/RPMS/" "$GITHUB_WORKSPACE/dist/rpm-loong64/" || true
find "$GITHUB_WORKSPACE/dist/rpm-loong64" -name "*.loongarch64.rpm" -exec mv {} "$GITHUB_WORKSPACE/dist/rpm-loong64/v2rayN-linux-rhel-loong64.rpm" \; || true
echo "==== Dist tree ===="
ls -R "$GITHUB_WORKSPACE/dist/rpm-loong64"
ls -R "$GITHUB_WORKSPACE/dist/rpm-loong64" || true
- name: Upload RPM artifact
uses: actions/upload-artifact@v7
with: { name: release-rpm-loong64, path: "dist/rpm-loong64/**/*.rpm", if-no-files-found: error }
+17 -1
View File
@@ -28,7 +28,23 @@ jobs:
target: macos
release_tag: ${{ inputs.release_tag }}
secrets: inherit
build-aot:
uses: ./.github/workflows/build.yml
with:
target: macos
isAot: true
release-aot-zip:
if: inputs.release_tag != ''
needs: build-aot
uses: ./.github/workflows/package-zip.yml
with:
target: macos
release_tag: ${{ inputs.release_tag }}
isAot: true
secrets: inherit
dmg:
name: package and release macOS dmg
if: inputs.release_tag != ''
@@ -28,3 +28,19 @@ jobs:
target: windows-desktop
release_tag: ${{ inputs.release_tag }}
secrets: inherit
build-aot:
uses: ./.github/workflows/build.yml
with:
target: windows
isAot: true
release-aot-zip:
if: inputs.release_tag != ''
needs: build-aot
uses: ./.github/workflows/package-zip.yml
with:
target: windows-desktop
release_tag: ${{ inputs.release_tag }}
isAot: true
secrets: inherit
+54 -10
View File
@@ -10,6 +10,10 @@ on:
required: false
type: string
default: './v2rayN.Desktop/v2rayN.Desktop.csproj'
isAot:
required: false
type: boolean
default: false
jobs:
build:
@@ -27,7 +31,6 @@ jobs:
)
}}
env:
Output: "${{ github.workspace }}/${{ matrix.arch }}"
RID: |-
${{
case(
@@ -41,6 +44,7 @@ jobs:
${{
case(
inputs.target == 'windows', '-p:EnableWindowsTargeting=true',
inputs.target == 'linux', (matrix.arch == 'arm64' && '-p:LinkerFlavor=lld -p:ObjCopyName=llvm-objcopy' || ''),
''
)
}}
@@ -71,25 +75,65 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v6
with:
dotnet-version: 10.0.1xx
- name: Build v2rayN
dotnet-version: 10.0.x
- name: Install dependencies
shell: bash
working-directory: ./v2rayN
run: dotnet publish $Project -c Release -r $RID -p:SelfContained=true $ExtOpt -o $Output
if: inputs.target == 'linux' && inputs.isAot
run: |
sudo apt-get update
sudo apt-get install -y clang zlib1g-dev build-essential
- name: Install dependencies cross-arch
shell: bash
if: inputs.target == 'linux' && inputs.isAot && matrix.arch == 'arm64'
run: |
sudo apt-get update
sudo apt-get install -y lld llvm binutils-aarch64-linux-gnu gcc-aarch64-linux-gnu g++-aarch64-linux-gnu libc6-dev-arm64-cross
- name: Set AOT Environment Variable
shell: bash
run: |
if [ "${{ inputs.isAot }}" = "true" ]; then
echo "DOTNET_AOT_ARG=-p:IsAot=true" >> $GITHUB_ENV
else
echo "DOTNET_AOT_ARG=-p:IsAot=false" >> $GITHUB_ENV
fi
- name: Set OUTPUT Environment Variable
shell: bash
run: |
RAW_PATH="${{ github.workspace }}/${{ matrix.arch }}${{ inputs.isAot && '-aot' || '' }}"
echo "OUTPUT=${RAW_PATH//\\//}" >> "$GITHUB_ENV"
- name: Build AmazTool
shell: bash
working-directory: ./v2rayN
run: dotnet publish ./AmazTool/AmazTool.csproj -c Release -r $RID -p:SelfContained=true -p:PublishTrimmed=true $ExtOpt -o $Output
run: dotnet publish ./AmazTool/AmazTool.csproj -c Release -r $RID -p:SelfContained=true -p:PublishTrimmed=true $ExtOpt ${{ env.DOTNET_AOT_ARG }} -o ${{ env.OUTPUT }}
- name: Build v2rayN WPF
shell: bash
if: inputs.project == './v2rayN/v2rayN.csproj'
working-directory: ./v2rayN
run: dotnet publish $Project -c Release -r $RID -p:SelfContained=true $ExtOpt -o ${{ env.OUTPUT }}
- name: Build v2rayN Avalonia
shell: bash
if: inputs.project == './v2rayN.Desktop/v2rayN.Desktop.csproj'
working-directory: ./v2rayN
run: dotnet publish $Project -c Release -r $RID -p:SelfContained=true $ExtOpt ${{ env.DOTNET_AOT_ARG }} -o ${{ env.OUTPUT }}
- name: Remove Debug Symbols
shell: bash
run: |
find ${{ matrix.arch }} -type f -name '*.pdb' -delete
find ${{ env.OUTPUT }} -type f -name '*.pdb' -delete
find ${{ env.OUTPUT }} -type f -name '*.debug' -delete
find ${{ env.OUTPUT }} -type f -name '*.dbg' -delete
find ${{ env.OUTPUT }} -type f -name '*.sym' -delete
find ${{ env.OUTPUT }} -name '*.dSYM' -exec rm -rf {} +
- name: Upload build artifacts
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.arch }}
path: ${{ matrix.arch }}
name: ${{ matrix.arch }}${{ inputs.isAot && '-aot' || '' }}
path: ${{ env.OUTPUT }}
+52 -13
View File
@@ -9,6 +9,10 @@ on:
target: # windows linux macos windows-desktop
required: true
type: string
isAot:
required: false
type: boolean
default: false
secrets:
GPG_PRIVATE_KEY:
required: true
@@ -43,27 +47,62 @@ jobs:
- name: Checkout
uses: actions/checkout@v7
- name: Set package variables
id: package-vars
shell: bash
env:
INPUT_TARGET: ${{ inputs.target }}
INPUT_IS_AOT: ${{ inputs.isAot }}
run: |
set -euo pipefail
package_dir="v2rayN-${Target}-${Arch}"
package_file="${package_dir}.zip"
build_artifact_name="${{ matrix.arch }}"
rename_suffix=""
if [[ "$INPUT_IS_AOT" == "true" ]]; then
build_artifact_name="${build_artifact_name}-aot"
rename_suffix="-aot"
fi
if [[ "$INPUT_TARGET" == "windows-desktop" ]]; then
rename_suffix="-desktop${rename_suffix}"
fi
final_package_file="${package_dir}${rename_suffix}.zip"
{
echo "build_artifact_name=${build_artifact_name}"
echo "package_dir=${package_dir}"
echo "package_file=${package_file}"
echo "final_package_file=${final_package_file}"
echo "rename_suffix=${rename_suffix}"
} >> "$GITHUB_OUTPUT"
- name: Restore build artifacts
uses: actions/download-artifact@v8
with:
name: ${{ matrix.arch }}
path: v2rayN-${{ env.Target }}-${{ env.Arch }}
name: ${{ steps.package-vars.outputs.build_artifact_name }}
path: ${{ steps.package-vars.outputs.package_dir }}
- name: Get v2rayN-core-bin
run: wget -nv -O v2rayN-$Target-$Arch.zip "https://github.com/2dust/v2rayN-core-bin/raw/refs/heads/master/v2rayN-$Target-$Arch.zip"
run: |
wget -nv -O "${{ steps.package-vars.outputs.package_file }}" "https://github.com/2dust/v2rayN-core-bin/raw/refs/heads/master/v2rayN-$Target-$Arch.zip"
- name: Package zip archive
run: 7z a -tZip v2rayN-$Target-$Arch.zip v2rayN-$Target-$Arch -mx1
shell: bash
run: |
7z a -tZip "${{ steps.package-vars.outputs.package_file }}" "${{ steps.package-vars.outputs.package_dir }}" -mx1
- name: Rename package archive
if: steps.package-vars.outputs.rename_suffix != ''
shell: bash
run: |
mv "${{ steps.package-vars.outputs.package_file }}" \
"${{ steps.package-vars.outputs.package_dir }}${{ steps.package-vars.outputs.rename_suffix }}.zip"
- name: Rename windows-desktop
if: inputs.target == 'windows-desktop'
run: mv "v2rayN-$Target-$Arch.zip" "v2rayN-$Target-$Arch-desktop.zip"
- name: Upload zip artifact
uses: actions/upload-artifact@v7
with:
name: release-${{ inputs.target }}-${{ matrix.arch }}
path: v2rayN*.zip
name: release-${{ inputs.target }}-${{ matrix.arch }}${{ inputs.isAot && '-aot' || '' }}
path: ${{ steps.package-vars.outputs.final_package_file }}
if-no-files-found: error
release:
@@ -75,7 +114,7 @@ jobs:
uses: ./.github/workflows/upload-sign.yml
with:
release_tag: ${{ inputs.release_tag }}
artifact_name: release-${{ inputs.target }}-${{ matrix.arch }}
artifact_name: release-${{ inputs.target }}-${{ matrix.arch }}${{ inputs.isAot && '-aot' || '' }}
file_pattern: '*.zip'
secrets:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
+53
View File
@@ -0,0 +1,53 @@
name: release v2rayN.WebAPI
on:
workflow_call:
inputs:
release_tag:
required: true
type: string
secrets:
GPG_PRIVATE_KEY:
required: false
workflow_dispatch:
inputs:
release_tag:
description: 'Release tag (for example 7.x.y).'
required: true
type: string
permissions:
contents: write
actions: read
jobs:
build:
uses: ./.github/workflows/build-WebAPI.yml
permissions:
contents: read
with:
release_tag: ${{ inputs.release_tag }}
release-web-packages:
name: sign and upload Web API packages
# Upload through the shared signing flow, exactly like the desktop packages.
if: inputs.release_tag != ''
needs: build
uses: ./.github/workflows/upload-sign.yml
with:
release_tag: ${{ inputs.release_tag }}
artifact_name: WebAPI-release-packages
file_pattern: 'v2rayN-*-WebAPI*.zip'
secrets: { GPG_PRIVATE_KEY: "${{ secrets.GPG_PRIVATE_KEY }}" }
release-web-manifest:
name: sign and upload Web update manifest
if: inputs.release_tag != ''
# Advertise the update only after its packages and manifest have been uploaded.
needs: [build, release-web-packages]
uses: ./.github/workflows/upload-sign.yml
with:
release_tag: ${{ inputs.release_tag }}
artifact_name: WebAPI-release-manifest
file_pattern: 'WebAPI-update.json'
secrets: { GPG_PRIVATE_KEY: "${{ secrets.GPG_PRIVATE_KEY }}" }
+36
View File
@@ -0,0 +1,36 @@
name: Update Certificate
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * *'
permissions:
contents: write
pull-requests: write
jobs:
update-cert:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup .NET
uses: actions/setup-dotnet@v6
with:
dotnet-version: 10.0.x
- name: Update Certificate
run: |
dotnet run .github/scripts/UpdateCert.cs
- name: Create Pull Request
uses: peter-evans/create-pull-request@v8
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "Update certificate"
title: "Update certificate"
body: "This pull request updates the certificate."
branch: github-actions/update-certificate
delete-branch: true
+3 -1
View File
@@ -19,7 +19,9 @@ jobs:
$wingetPackage = "2dust.v2rayN"
$gitToken = "${{ secrets.PT_WINGET }}"
$github = Invoke-RestMethod -uri "https://api.github.com/repos/2dust/v2rayN/releases"
$headers = @{ Authorization = "Bearer $gitToken"; Accept = "application/vnd.github+json" }
$github = Invoke-RestMethod -Uri "https://api.github.com/repos/2dust/v2rayN/releases" -Headers $headers
$targetRelease = $github | Where-Object -Property prerelease -match 'False' | Select -First 1
Regular → Executable
+2
View File
@@ -220,6 +220,8 @@ apply_channel_or_keep() {
VERSION="${VERSION#v}"
return 0
fi
git remote add upstream https://github.com/2dust/v2rayN || true
git fetch upstream
echo "[*] Resolving ${ch} tag from GitHub releases..."
Regular → Executable
View File
Regular → Executable
+9
View File
@@ -676,6 +676,15 @@ EOF
find "$stage/opt/v2rayN" -type f -exec chmod 0644 {} +
[[ -f "$stage/opt/v2rayN/v2rayN" ]] && chmod 0755 "$stage/opt/v2rayN/v2rayN" || true
# Core binaries must be executable before installation: ordinary users cannot
# chmod the root-owned files installed by dpkg.
local core
for core in xray/xray sing_box/sing-box mihomo/mihomo; do
if [[ -f "$stage/opt/v2rayN/bin/$core" ]]; then
chmod 0755 "$stage/opt/v2rayN/bin/$core"
fi
done
deb_out="$OUTPUT_DIR/v2rayn_${VERSION}_${deb_arch}.deb"
dpkg-deb --root-owner-group --build "$stage" "$deb_out"
Regular → Executable
+5 -3
View File
@@ -73,12 +73,12 @@ detect_environment() {
HOST_ARCH="$(uname -m)"
case "$OS_ID" in
rhel|rocky|almalinux|fedora|centos)
rhel|rocky|almalinux|fedora|centos|anolis)
echo "Detected supported system: ${OS_NAME:-$OS_ID} ${OS_VERSION_ID:-}"
;;
*)
die "Unsupported system: ${OS_NAME:-unknown} (${OS_ID:-unknown}).
This script only supports: RHEL / Rocky / AlmaLinux / Fedora / CentOS."
This script only supports: RHEL / Rocky / AlmaLinux / Fedora / CentOS / Anolis OS."
;;
esac
@@ -216,6 +216,8 @@ apply_channel_or_keep() {
VERSION="${VERSION#v}"
return 0
fi
git remote add upstream https://github.com/2dust/v2rayN || true
git fetch upstream
echo "[*] Resolving ${ch} tag from GitHub releases..."
@@ -521,7 +523,7 @@ Requires: freetype >= 2.13
%description
v2rayN Linux for Red Hat Enterprise Linux
Support vless / vmess / Trojan / http / socks / Anytls / Hysteria2 / Shadowsocks / tuic / WireGuard
Support Red Hat Enterprise Linux / Fedora Linux / Rocky Linux / AlmaLinux / CentOS
Support Red Hat Enterprise Linux / Fedora Linux / Rocky Linux / AlmaLinux / CentOS / Anolis OS
For more information, Please visit our website
https://github.com/2dust/v2rayN
Regular → Executable
View File
Regular → Executable
View File
+20 -5
View File
@@ -5,16 +5,31 @@
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Update="Resx\Resource.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resource.Designer.cs</LastGenOutput>
</EmbeddedResource>
<Compile Update="Resx\Resource.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resource.resx</DependentUpon>
</Compile>
<EmbeddedResource Update="Resx\Resource.resx">
<SubType>Designer</SubType>
<LastGenOutput>Resource.Designer.cs</LastGenOutput>
<Generator>ResXFileCodeGenerator</Generator>
</EmbeddedResource>
<EmbeddedResource Update="Resx\ResUI.zh-Hans.resx">
<SubType>Designer</SubType>
<Generator>ResXFileCodeGenerator</Generator>
</EmbeddedResource>
<EmbeddedResource Update="Resx\ResUI.zh-Hant.resx">
<SubType>Designer</SubType>
<Generator>ResXFileCodeGenerator</Generator>
</EmbeddedResource>
</ItemGroup>
<Target Name="RemoveAotSatelliteOutputs" AfterTargets="ComputeFilesToPublish" Condition="'$(PublishAot)' == 'true'">
<ItemGroup>
<ResolvedFileToPublish Remove="@(ResolvedFileToPublish)"
Condition="$([System.String]::Copy('%(ResolvedFileToPublish.RelativePath)').Contains('resources.dll'))" />
</ItemGroup>
</Target>
</Project>
+1 -1
View File
@@ -19,7 +19,7 @@ namespace AmazTool.Resx {
// 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。
// 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen
// (以 /str 作为命令选项),或重新生成 VS 项目。
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resource {
+10 -3
View File
@@ -45,6 +45,8 @@ internal class UpgradeApp
var thisAppOldFile = $"{Utils.GetExePath()}.tmp";
File.Delete(thisAppOldFile);
var splitKey = "/";
var allowedBaseDir = Utils.StartupPath();
var pathComparison = Utils.GetPathComparison();
using var archive = ZipFile.OpenRead(fileName);
foreach (var entry in archive.Entries)
@@ -65,16 +67,21 @@ internal class UpgradeApp
}
var fullName = string.Join(splitKey, lst[1..lst.Length]);
var entryOutputPath = Utils.GetPath(fullName);
if (!Utils.IsPathUnderDirectory(allowedBaseDir, entryOutputPath))
{
Console.WriteLine($"{Resx.Resource.FailedUpgrade} blocked potential path traversal: {entry.FullName}");
return;
}
if (string.Equals(Utils.GetExePath(), Utils.GetPath(fullName), StringComparison.OrdinalIgnoreCase))
if (string.Equals(Utils.GetExePath(), entryOutputPath, pathComparison))
{
File.Move(Utils.GetExePath(), thisAppOldFile);
}
var entryOutputPath = Utils.GetPath(fullName);
Directory.CreateDirectory(Path.GetDirectoryName(entryOutputPath)!);
//In the bin folder, if the file already exists, it will be skipped
if (fullName.StartsWith("bin") && File.Exists(entryOutputPath))
if (fullName.StartsWith("bin", pathComparison) && File.Exists(entryOutputPath))
{
continue;
}
+22
View File
@@ -24,6 +24,28 @@ internal class Utils
return Path.Combine(startupPath, fileName);
}
public static bool IsPathUnderDirectory(string baseDir, string targetPath)
{
if (string.IsNullOrWhiteSpace(baseDir) || string.IsNullOrWhiteSpace(targetPath))
{
return false;
}
var baseFull = Path.GetFullPath(baseDir);
var targetFull = Path.GetFullPath(targetPath);
baseFull = baseFull.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
return targetFull.StartsWith(baseFull, GetPathComparison());
}
public static StringComparison GetPathComparison()
{
return OperatingSystem.IsWindows()
? StringComparison.OrdinalIgnoreCase
: StringComparison.Ordinal;
}
public static string V2rayN => "v2rayN";
public static void StartV2RayN()
+11 -1
View File
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<Version>7.25.3</Version>
<Version>7.25.6</Version>
</PropertyGroup>
<PropertyGroup>
@@ -29,4 +29,14 @@
<PublishSingleFile>true</PublishSingleFile>
<PublishReadyToRun>false</PublishReadyToRun>
</PropertyGroup>
<PropertyGroup>
<IsAot Condition="'$(IsAot)' == ''">false</IsAot>
</PropertyGroup>
<PropertyGroup Condition="'$(IsAot)' == 'true'">
<PublishAot>true</PublishAot>
<PublishTrimmed>true</PublishTrimmed>
<DefineConstants>$(DefineConstants);V2RAYN_IS_AOT</DefineConstants>
</PropertyGroup>
</Project>
+12 -14
View File
@@ -6,29 +6,27 @@
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="12.0.0" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="12.1.2" />
<PackageVersion Include="Avalonia.Desktop" Version="12.1.3" />
<PackageVersion Include="Avalonia.Desktop" Version="12.1.4" />
<PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.3" />
<PackageVersion Include="DialogHost.Avalonia" Version="0.12.3" />
<PackageVersion Include="IPNetwork2" Version="4.3.0" />
<PackageVersion Include="ReactiveUI.Avalonia" Version="12.1.2" />
<PackageVersion Include="IPNetwork2" Version="5.0.1" />
<PackageVersion Include="ProDataGrid" Version="12.1.0.6" />
<PackageVersion Include="ReactiveUI.Avalonia" Version="12.1.6" />
<PackageVersion Include="CliWrap" Version="3.10.5" />
<PackageVersion Include="Downloader" Version="5.9.8" />
<PackageVersion Include="Downloader" Version="5.10.2" />
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" />
<PackageVersion Include="MaterialDesignThemes" Version="5.3.2" />
<PackageVersion Include="MaterialDesignThemes" Version="5.4.0" />
<PackageVersion Include="QRCoder" Version="1.8.0" />
<PackageVersion Include="ReactiveUI" Version="24.3.0" />
<PackageVersion Include="ReactiveUI.SourceGenerators" Version="3.2.0" />
<PackageVersion Include="ReactiveUI.WPF" Version="24.2.0" />
<PackageVersion Include="ReactiveUI" Version="26.0.1" />
<PackageVersion Include="ReactiveUI.WPF" Version="26.0.1" />
<PackageVersion Include="Semi.Avalonia" Version="12.1.0.1" />
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="12.0.0" />
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="12.1.0.1" />
<PackageVersion Include="NLog" Version="6.2.1" />
<PackageVersion Include="NLog" Version="6.2.2" />
<PackageVersion Include="sqlite-net-e" Version="1.11.285" />
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.4.1" />
<PackageVersion Include="TaskScheduler" Version="2.12.2" />
<PackageVersion Include="TUnit" Version="1.68.17" />
<PackageVersion Include="TUnit.Assertions.Should" Version="1.65.38-beta" />
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.54.1" />
<PackageVersion Include="TUnit" Version="1.73.19" />
<PackageVersion Include="TUnit.Assertions.Should" Version="1.72.16-beta" />
<PackageVersion Include="WebDav.Client" Version="2.9.0" />
<PackageVersion Include="YamlDotNet" Version="18.1.0" />
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.22" />
@@ -0,0 +1,42 @@
namespace ServiceLib.Tests.Common;
public class UnixFileModeTests
{
[Test]
public async Task AlreadyExecutableSystemBinary_ShouldNotRequireOwnership()
{
if (OperatingSystem.IsWindows())
{
return;
}
// /bin/sh is executable but owned by root on the Unix CI runners.
// A regular user must not need chmod permission just to launch it.
var mode = File.GetUnixFileMode("/bin/sh");
await Utils.SetUnixFileMode("/bin/sh").Should().BeTrue();
await File.GetUnixFileMode("/bin/sh").Should().BeEqualTo(mode);
}
[Test]
public async Task NonExecutableOwnedFile_ShouldGainExecuteBitsWithoutLosingOtherPermissions()
{
if (OperatingSystem.IsWindows())
{
return;
}
var path = Path.GetTempFileName();
try
{
var mode = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.GroupRead;
File.SetUnixFileMode(path, mode);
await Utils.SetUnixFileMode(path).Should().BeTrue();
await File.GetUnixFileMode(path).Should().BeEqualTo(mode | UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute);
}
finally
{
File.Delete(path);
}
}
}
@@ -1,8 +1,3 @@
using System.Reflection;
using ServiceLib.Enums;
using ServiceLib.Manager;
using ServiceLib.Models;
namespace ServiceLib.Tests.CoreConfig;
internal static class CoreConfigTestFactory
@@ -716,7 +716,6 @@ public class CoreConfigSingboxServiceTests
}
}
[Test]
public async Task GenerateClientConfigContent_CustomOutbound_ShouldReplaceWithUserCustomOutboundJson()
{
@@ -0,0 +1,128 @@
namespace ServiceLib.Tests.CoreConfig.V2ray;
public class CoreConfigV2rayIPNetworkTests
{
[Test]
[Arguments("0.0.0.0/0", "::/0")]
[Arguments("::/0", "0.0.0.0/0")]
[Arguments("0.0.0.0/1", "128.0.0.0/1,::/0")]
[Arguments("::/1", "0.0.0.0/0,8000::/1")]
public async Task GenerateClientConfigContent_TunExclusion_ShouldPreserveRemainingRoutes(
string excludedNetwork, string expectedRoutes)
{
var config = CoreConfigTestFactory.CreateConfigWithTun(ECoreType.Xray, true);
config.TunModeItem.RouteExcludeAddress = [excludedNetwork];
var generated = await GenerateConfig(config);
var routes = generated.inbounds.Single(i => i.protocol == "tun").settings.autoSystemRoutingTable;
await routes.Should().BeEquivalentTo(expectedRoutes.Split(','), StringComparer.Ordinal);
}
[Test]
public async Task GenerateClientConfigContent_TunExclusions_ShouldHandleOverlappingAndDuplicateNetworks()
{
var config = CoreConfigTestFactory.CreateConfigWithTun(ECoreType.Xray, true);
config.TunModeItem.RouteExcludeAddress =
[
"0.0.0.0/1", "128.0.0.1/2", "128.0.0.1/2", "128.0.0.1/32",
"::/1", "8000::1/2", "8000::1/128"
];
var generated = await GenerateConfig(config);
var routes = generated.inbounds.Single(i => i.protocol == "tun").settings.autoSystemRoutingTable;
await routes.Should().BeEquivalentTo(new[] { "192.0.0.0/2", "c000::/2" }, StringComparer.Ordinal);
}
[Test]
public async Task GenerateClientConfigContent_TunExclusions_ShouldAllowExcludingBothAddressFamilies()
{
var config = CoreConfigTestFactory.CreateConfigWithTun(ECoreType.Xray, true);
config.TunModeItem.RouteExcludeAddress = ["0.0.0.0/0", "::/0"];
var generated = await GenerateConfig(config);
var routes = generated.inbounds.Single(i => i.protocol == "tun").settings.autoSystemRoutingTable;
await routes.Should().BeEmpty();
}
[Test]
public async Task GenerateClientConfigContent_TunHostExclusions_ShouldPreserveNeighbouringAddresses()
{
var config = CoreConfigTestFactory.CreateConfigWithTun(ECoreType.Xray, true);
config.TunModeItem.RouteExcludeAddress =
[
"0.0.0.0/32", "255.255.255.255/32",
"::/128", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128"
];
var generated = await GenerateConfig(config);
var routes = generated.inbounds.Single(i => i.protocol == "tun").settings.autoSystemRoutingTable
.Select(IPNetwork.Parse).ToArray();
foreach (var address in new[]
{
"0.0.0.0", "255.255.255.255", "::", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"
})
{
await routes.Any(route => route.Contains(IPAddress.Parse(address))).Should().BeFalse();
}
foreach (var address in new[]
{
"0.0.0.1", "255.255.255.254", "::1", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe"
})
{
await routes.Any(route => route.Contains(IPAddress.Parse(address))).Should().BeTrue();
}
}
[Test]
[Arguments("198.18.0.0/15", 131071L)]
[Arguments("0.0.0.0/0", 4294967295L)]
[Arguments("198.18.0.0/31", 1L)]
[Arguments("198.18.0.1/32", 65535L)]
[Arguments("fd00::/64", long.MaxValue)]
[Arguments("fd00::/66", 4611686018427387903L)]
[Arguments("fd00::/127", 1L)]
[Arguments("fd00::1/128", 65535L)]
[Arguments("invalid-network", 65535L)]
[Arguments("198.18.0.0/33", 65535L)]
[Arguments("fd00::/129", 65535L)]
public async Task GenerateClientConfigContent_FakeDns_ShouldDerivePoolSizeOrUseFallback(
string range, long expectedPoolSize)
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
config.SimpleDNSItem.FakeIP = true;
config.SimpleDNSItem.FakeIPRange = range;
CoreConfigTestFactory.BindAppManagerConfig(config);
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray);
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray);
context.RoutingItem!.RuleSet = JsonUtils.Serialize(new List<RulesItem>
{
new()
{
Enabled = true,
RuleType = ERuleType.DNS,
OutboundTag = Global.ProxyTag,
Domain = ["full:example.com"],
}
});
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
await result.Success.Should().BeTrue();
var generated = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
await generated.fakedns.Should().NotBeNull();
await generated.fakedns!.ipPool.Should().BeEqualTo(range);
await generated.fakedns.poolSize.Should().BeEqualTo(expectedPoolSize);
}
private static async Task<V2rayConfig> GenerateConfig(Config config)
{
CoreConfigTestFactory.BindAppManagerConfig(config);
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray);
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray);
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
await result.Success.Should().BeTrue();
var generated = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString());
await generated.Should().NotBeNull();
return generated!;
}
}
@@ -2,6 +2,43 @@ namespace ServiceLib.Tests.CoreConfig.V2ray;
public class CoreConfigV2rayServiceTests
{
[Test]
[Arguments(null)]
[Arguments("")]
[Arguments("AGH+DQBdAAAgACBAi73MnT3WWXWj4M9MUiTygpjSPUfM/Rj3Iasve4uFQAAkAAEAAQABAAIAAQADAAIAAQACAAIAAgADAAMAAQADAAIAAwADAA5vdXRlci5lY2gudGVzdAAA")]
[Arguments("https://dns.example.com/dns-query")]
[Arguments("udp://127.0.0.1:5353")]
[Arguments("ech.example.com+https://dns.example.com/dns-query")]
public async Task GenerateClientConfigContent_Tls_ShouldPreserveEchConfigWithoutLegacyQueryOption(string? echConfigList)
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
CoreConfigTestFactory.BindAppManagerConfig(config);
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray);
node.StreamSecurity = Global.StreamSecurity;
node.Sni = "ech.example.com";
node.EchConfigList = echConfigList;
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray);
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
await result.Success.Should().BeTrue();
var generatedConfig = JsonNode.Parse(result.Data!.ToString()!)!;
var proxy = generatedConfig["outbounds"]!.AsArray()
.First(outbound => outbound!["tag"]!.GetValue<string>() == Global.ProxyTag)!;
var tls = proxy["streamSettings"]!["tlsSettings"]!.AsObject();
await tls["serverName"]!.GetValue<string>().Should().BeEqualTo("ech.example.com");
if (string.IsNullOrEmpty(echConfigList))
{
await tls.ContainsKey("echConfigList").Should().BeFalse();
}
else
{
await tls["echConfigList"]!.GetValue<string>().Should().BeEqualTo(echConfigList);
}
await tls.ContainsKey("echForceQuery").Should().BeFalse();
}
[Test]
public async Task GenerateClientConfigContent_ShouldGenerateBasicProxyConfig()
{
@@ -21,6 +58,61 @@ public class CoreConfigV2rayServiceTests
await v2rayConfig.inbounds.Should().Contain(i => i.protocol == nameof(EInboundProtocol.mixed));
}
[Test]
public async Task GenerateClientConfigContent_RealityFragment_ShouldUsePacketRangeOnlyForReality()
{
static ProfileItem CreateVlessNode(string indexId, string streamSecurity)
{
var node = new ProfileItem
{
IndexId = indexId,
ConfigType = EConfigType.VLESS,
CoreType = ECoreType.Xray,
Remarks = indexId,
Address = "example.com",
Port = 443,
Password = "01234567-89ab-cdef-0123-456789abcdef",
Network = nameof(ETransport.raw),
StreamSecurity = streamSecurity,
PublicKey = "test-public-key",
Sni = "example.com",
Fingerprint = "chrome",
};
node.SetProtocolExtra(new ProtocolExtraItem { VlessEncryption = Global.None });
node.SetTransportExtra(new TransportExtraItem { RawHeaderType = Global.None });
return node;
}
static string GetFragmentPackets(Outbounds4Ray outbound)
{
var finalmask = JsonUtils.ParseJson(JsonUtils.Serialize(outbound.streamSettings!.finalmask));
return finalmask!["tcp"]![0]!["settings"]!["packets"]!.GetValue<string>();
}
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
config.CoreBasicItem.EnableFragment = true;
CoreConfigTestFactory.BindAppManagerConfig(config);
var tlsNode = CreateVlessNode("tls-node", Global.StreamSecurity);
var realityNode = CreateVlessNode("reality-node", Global.StreamSecurityReality);
var group = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, "group", "group",
[tlsNode.IndexId, realityNode.IndexId]);
var context = CoreConfigTestFactory.CreateContext(config, group, ECoreType.Xray);
context.AllProxiesMap[tlsNode.IndexId] = tlsNode;
context.AllProxiesMap[realityNode.IndexId] = realityNode;
context.AllProxiesMap[group.IndexId] = group;
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var tlsOutbound = cfg.outbounds.Single(o => o.streamSettings?.security == Global.StreamSecurity);
var realityOutbound = cfg.outbounds.Single(o => o.streamSettings?.security == Global.StreamSecurityReality);
await GetFragmentPackets(tlsOutbound).Should().BeEqualTo("tlshello");
await GetFragmentPackets(realityOutbound).Should().BeEqualTo("1-3");
}
[Test]
public async Task GenerateClientConfigContent_HttpOutbound_ShouldEmitHeadersInSettings()
{
@@ -51,7 +143,7 @@ public class CoreConfigV2rayServiceTests
.Select(item => item!.GetValue<string>())
.Should().BeEquivalentTo(["a=1", "b=2"]);
}
[Test]
public async Task GenerateClientConfigContent_PolicyGroup_ShouldExpandChildrenAndBuildBalancer()
{
@@ -587,13 +679,11 @@ public class CoreConfigV2rayServiceTests
}
[Test]
[Arguments(true)]
[Arguments(false)]
public async Task GenerateClientConfigContent_Tun_ShouldSkipIPv6RouteWithoutGlobalIPv6(bool enableIPv6Address)
public async Task GenerateClientConfigContent_Tun_ShouldSkipIPv6RouteWithoutGlobalIPv6()
{
// A host without a global IPv6 address has no IPv6 traffic that could bypass the tunnel,
// while ::/0 would pull IPv6 attempts into a tunnel they cannot leave.
var config = CoreConfigTestFactory.CreateConfigWithTun(ECoreType.Xray, enableIPv6Address);
var config = CoreConfigTestFactory.CreateConfigWithTun(ECoreType.Xray, enableIPv6Address: false);
CoreConfigTestFactory.BindAppManagerConfig(config);
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main");
@@ -611,6 +701,26 @@ public class CoreConfigV2rayServiceTests
await ipv6Routes.Should().BeEmpty();
}
[Test]
public async Task GenerateClientConfigContent_Tun_ShouldRouteIPv6WhenEnabledWithoutGlobalIPv6()
{
// Hosts behind NAT66 hold only ULA addresses, so EnableIPv6Address must still route ::/0.
var config = CoreConfigTestFactory.CreateConfigWithTun(ECoreType.Xray, enableIPv6Address: true);
CoreConfigTestFactory.BindAppManagerConfig(config);
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main");
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray, hasGlobalIPv6Address: false);
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
await result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var tunInbound = cfg.inbounds.FirstOrDefault(i => i.protocol == "tun");
await tunInbound.Should().NotBeNull();
await tunInbound!.settings.autoSystemRoutingTable.Should().Contain("::/0");
}
[Test]
public async Task GenerateClientConfigContent_TunRouteExcludeAddress_ShouldSkipIPv6RangesWithoutGlobalIPv6()
{
@@ -0,0 +1,77 @@
using ServiceLib.Tests.CoreConfig;
namespace ServiceLib.Tests.Handler;
public class ConfigHandlerCustomSubscriptionTests
{
private const string FullXrayConfigs = """
[
{"remarks":"custom-1","inbounds":[{"tag":"socks","listen":"127.0.0.1","port":10808,"protocol":"socks","settings":{"auth":"noauth","udp":true}}],"outbounds":[{"tag":"proxy","protocol":"vless","settings":{"vnext":[{"address":"127.0.0.1","port":443,"users":[{"id":"00000000-0000-0000-0000-000000000001","encryption":"none"}]}]},"streamSettings":{"network":"tcp"}}],"routing":{"domainStrategy":"AsIs","rules":[]}},
{"remarks":"custom-2","inbounds":[{"tag":"socks","listen":"127.0.0.1","port":10808,"protocol":"socks","settings":{"auth":"noauth","udp":true}}],"outbounds":[{"tag":"proxy","protocol":"vless","settings":{"vnext":[{"address":"127.0.0.1","port":443,"users":[{"id":"00000000-0000-0000-0000-000000000001","encryption":"none"}]}]},"streamSettings":{"network":"tcp"}}],"routing":{"domainStrategy":"AsIs","rules":[]}}
]
""";
[Test]
public async Task AddBatchServers_FullConfigsFromSubscription_InheritSubscriptionPreSocksPort()
{
var config = CoreConfigTestFactory.CreateConfig();
CoreConfigTestFactory.BindAppManagerConfig(config);
SQLiteHelper.Instance.CreateTable<SubItem>();
SQLiteHelper.Instance.CreateTable<ProfileItem>();
var subId = $"sub-{Guid.NewGuid():N}";
await SQLiteHelper.Instance.ReplaceAsync(new SubItem
{
Id = subId,
Remarks = "sub",
Url = "http://127.0.0.1/sub",
PreSocksPort = 10808,
});
var oldProfile = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, $"profile-{Guid.NewGuid():N}", "old");
oldProfile.Subid = subId;
oldProfile.IsSub = true;
await SQLiteHelper.Instance.InsertAsync(oldProfile);
var count = await ConfigHandler.AddBatchServers(config, FullXrayConfigs, subId, true);
var profiles = await AppManager.Instance.ProfileItems(subId) ?? [];
await count.Should().BeEqualTo(2);
await profiles.Should().HaveCount(2);
await profiles.Any(profile => profile.IndexId == oldProfile.IndexId).Should().BeFalse();
foreach (var profile in profiles)
{
await profile.ConfigType.Should().BeEqualTo(EConfigType.Custom);
await profile.PreSocksPort.Should().BeEqualTo(10808);
}
}
[Test]
public async Task AddBatchServers_UnsuccessfulSubscriptionUpdate_PreservesExistingProfiles()
{
var config = CoreConfigTestFactory.CreateConfig();
CoreConfigTestFactory.BindAppManagerConfig(config);
SQLiteHelper.Instance.CreateTable<SubItem>();
SQLiteHelper.Instance.CreateTable<ProfileItem>();
var subId = $"sub-failed-{Guid.NewGuid():N}";
await SQLiteHelper.Instance.ReplaceAsync(new SubItem
{
Id = subId,
Remarks = "sub",
Url = "http://127.0.0.1/sub",
});
var existing = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, $"profile-{Guid.NewGuid():N}", "existing");
existing.Subid = subId;
existing.IsSub = true;
await SQLiteHelper.Instance.InsertAsync(existing);
var invalidSubscription = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("not a valid profile"));
var count = await ConfigHandler.AddBatchServers(config, invalidSubscription, subId, true);
var profiles = await AppManager.Instance.ProfileItems(subId) ?? [];
await (count < 1).Should().BeTrue();
await profiles.Should().HaveCount(1);
await profiles[0].IndexId.Should().BeEqualTo(existing.IndexId);
}
}
@@ -0,0 +1,132 @@
namespace ServiceLib.Tests.Manager;
public class ProfileExManagerConcurrencyTests
{
[Test]
public async Task SetTestDelay_FromManyThreads_ShouldNotThrow()
{
var manager = CreateManager();
var exceptions = new List<Exception>();
var threads = Environment.ProcessorCount * 2;
//Hammer the same manager from several streams at once: the queue is mutated while
//other threads keep pushing into it
var barrier = new Barrier(threads);
Parallel.For(0, threads, t =>
{
barrier.SignalAndWait();
for (var i = 0; i < 5000; i++)
{
try
{
manager.SetTestDelay($"index{i % 500}", i);
}
catch (Exception ex)
{
lock (exceptions)
{
exceptions.Add(ex);
}
}
}
});
await exceptions.Count.Should().BeEqualTo(0);
}
[Test]
public async Task SetTestDelay_FromManyThreads_ShouldNotLoseQueuedIds()
{
var manager = CreateManager();
var ids = 500;
Parallel.For(0, Environment.ProcessorCount * 2, t =>
{
for (var i = 0; i < 5000; i++)
{
manager.SetTestDelay($"id{i % ids}", i);
}
});
var drained = Drain(manager);
await drained.Count.Should().BeGreaterThanOrEqualTo(ids);
await drained.Distinct().Count().Should().BeEqualTo(ids);
}
[Test]
public async Task SetTestDelay_ShouldCreateOneEntryPerIndex()
{
var manager = CreateManager();
await RunConcurrentUpdates(8, 10, (_, index) => manager.SetTestDelay($"same{index}", index));
var items = await manager.GetProfileExs();
await items.Count.Should().BeEqualTo(10);
await items.Select(t => t.IndexId).Distinct().Count().Should().BeEqualTo(10);
}
[Test]
public async Task SetTestResults_ConcurrentUpdates_ShouldKeepAllValuesOnOneEntry()
{
var manager = CreateManager();
Action<string>[] updates =
[
indexId => manager.SetTestDelay(indexId, 123),
indexId => manager.SetTestSpeed(indexId, 45.6m),
indexId => manager.SetTestMessage(indexId, "result"),
indexId => manager.SetTestIpInfo(indexId, "203.0.113.1"),
indexId => manager.SetSort(indexId, 7)
];
await RunConcurrentUpdates(updates.Length, 100, (worker, index) => updates[worker]($"profile{index}"));
var items = await manager.GetProfileExs();
await items.Count.Should().BeEqualTo(100);
await items.Select(item => item.IndexId).Distinct().Count().Should().BeEqualTo(100);
foreach (var item in items)
{
await item.Delay.Should().BeEqualTo(123);
await item.Speed.Should().BeEqualTo(45.6m);
await item.Message.Should().BeEqualTo("result");
await item.IpInfo.Should().BeEqualTo("203.0.113.1");
await item.Sort.Should().BeEqualTo(7);
}
}
private static async Task RunConcurrentUpdates(int workers, int indexes, Action<int, int> update)
{
using var barrier = new Barrier(workers);
var tasks = Enumerable.Range(0, workers).Select(worker => Task.Factory.StartNew(() =>
{
for (var index = 0; index < indexes; index++)
{
if (!barrier.SignalAndWait(TimeSpan.FromSeconds(10)))
{
throw new TimeoutException("Concurrent update workers did not synchronize.");
}
update(worker, index);
}
}, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default)).ToArray();
await Task.WhenAll(tasks);
}
private static ProfileExManager CreateManager()
{
return (ProfileExManager)Activator.CreateInstance(typeof(ProfileExManager), nonPublic: true)!;
}
private static List<string> Drain(ProfileExManager manager)
{
var queue = (System.Collections.Concurrent.ConcurrentQueue<string>)typeof(ProfileExManager)
.GetField("_queIndexIds", BindingFlags.Instance | BindingFlags.NonPublic)!
.GetValue(manager)!;
var result = new List<string>();
while (queue.TryDequeue(out var id))
{
result.Add(id);
}
return result;
}
}
@@ -0,0 +1,92 @@
namespace ServiceLib.Tests.Manager;
public class ProfileExManagerPersistenceTests
{
[Test]
public async Task SaveTo_NewProfilesWithRepeatedResults_ShouldPersistEveryProfile()
{
SQLiteHelper.Instance.CreateTable<ProfileExItem>();
var manager = new ProfileExManager();
var ids = Enumerable.Range(0, 3).Select(_ => Guid.NewGuid().ToString("N")).ToArray();
try
{
foreach (var id in ids)
{
manager.SetTestDelay(id, 42);
manager.SetTestSpeed(id, 12.5m);
manager.SetTestMessage(id, "completed");
manager.SetTestIpInfo(id, "192.0.2.1");
manager.SetSort(id, 7);
}
await manager.SaveTo();
foreach (var id in ids)
{
var saved = await SQLiteHelper.Instance.TableAsync<ProfileExItem>()
.Where(t => t.IndexId == id).ToListAsync();
await saved.Count.Should().BeEqualTo(1);
await saved[0].Delay.Should().BeEqualTo(42);
await saved[0].Speed.Should().BeEqualTo(12.5m);
await saved[0].Message.Should().BeEqualTo("completed");
await saved[0].IpInfo.Should().BeEqualTo("192.0.2.1");
await saved[0].Sort.Should().BeEqualTo(7);
}
}
finally
{
foreach (var id in ids)
{
await SQLiteHelper.Instance.DeleteAsync(new ProfileExItem { IndexId = id });
}
}
}
[Test]
public async Task SaveTo_MixedNewAndExistingProfiles_ShouldInsertAndUpdateResults()
{
SQLiteHelper.Instance.CreateTable<ProfileExItem>();
var manager = new ProfileExManager();
var existingId = Guid.NewGuid().ToString("N");
var newId = Guid.NewGuid().ToString("N");
try
{
await SQLiteHelper.Instance.InsertAsync(new ProfileExItem
{
IndexId = existingId,
Delay = 1,
Speed = 2,
});
manager.SetTestDelay(existingId, 100);
manager.SetTestSpeed(existingId, 20);
manager.SetTestDelay(newId, 200);
manager.SetTestIpInfo(newId, "192.0.2.2");
await manager.SaveTo();
var existing = await SQLiteHelper.Instance.TableAsync<ProfileExItem>()
.Where(t => t.IndexId == existingId).FirstAsync();
await existing.Delay.Should().BeEqualTo(100);
await existing.Speed.Should().BeEqualTo(20m);
var inserted = await SQLiteHelper.Instance.TableAsync<ProfileExItem>()
.Where(t => t.IndexId == newId).ToListAsync();
await inserted.Count.Should().BeEqualTo(1);
await inserted[0].Delay.Should().BeEqualTo(200);
await inserted[0].IpInfo.Should().BeEqualTo("192.0.2.2");
manager.SetTestDelay(newId, 300);
await manager.SaveTo();
var updated = await SQLiteHelper.Instance.TableAsync<ProfileExItem>()
.Where(t => t.IndexId == newId).FirstAsync();
await updated.Delay.Should().BeEqualTo(300);
await updated.IpInfo.Should().BeEqualTo("192.0.2.2");
}
finally
{
await SQLiteHelper.Instance.DeleteAsync(new ProfileExItem { IndexId = existingId });
await SQLiteHelper.Instance.DeleteAsync(new ProfileExItem { IndexId = newId });
}
}
}
@@ -4,6 +4,7 @@
<OutputType>Exe</OutputType>
<IsPackable>false</IsPackable>
<ImplicitUsings>enable</ImplicitUsings>
<PublishAot>true</PublishAot>
</PropertyGroup>
<ItemGroup>
@@ -0,0 +1,59 @@
namespace ServiceLib.Tests.Services;
public class SpeedtestServiceBatchTests
{
[Test]
[Arguments(0)]
[Arguments(1000)]
public async Task EmptySelection_ShouldReturnNoBatches(int pageSize)
{
await GetBatches([], pageSize).Count.Should().BeEqualTo(0);
}
[Test]
[Arguments(0)]
[Arguments(-1)]
public async Task NonPositivePageSize_ShouldKeepEveryEligibleProfile(int pageSize)
{
var profiles = CreateProfiles();
var batches = GetBatches(profiles, pageSize);
await batches.Count.Should().BeEqualTo(profiles.Count);
await batches.SelectMany(batch => batch).Count().Should().BeEqualTo(profiles.Count);
foreach (var profile in profiles)
{
await batches.SelectMany(batch => batch).Count(item => ReferenceEquals(item, profile)).Should().BeEqualTo(1);
}
}
[Test]
public async Task NormalBatches_ShouldRespectPageSizeAndKeepCoreTypesSeparate()
{
var profiles = CreateProfiles();
var batches = GetBatches(profiles, 2);
await batches.Count.Should().BeEqualTo(3);
await batches.SelectMany(batch => batch).Count().Should().BeEqualTo(profiles.Count);
foreach (var batch in batches)
{
await (batch.Count > 0 && batch.Count <= 2).Should().BeTrue();
await batch.Select(item => item.CoreType).Distinct().Count().Should().BeEqualTo(1);
}
}
private static List<ServerTestItem> CreateProfiles() =>
[
new() { CoreType = ECoreType.Xray },
new() { CoreType = ECoreType.Xray },
new() { CoreType = ECoreType.Xray },
new() { CoreType = ECoreType.sing_box },
new() { CoreType = ECoreType.sing_box }
];
private static List<List<ServerTestItem>> GetBatches(List<ServerTestItem> profiles, int pageSize)
{
var service = new SpeedtestService(new Config { SpeedTestItem = new() }, _ => Task.CompletedTask);
var method = typeof(SpeedtestService).GetMethod("GetTestBatchItem", BindingFlags.Instance | BindingFlags.NonPublic)!;
return (List<List<ServerTestItem>>)method.Invoke(service, [profiles, pageSize])!;
}
}
@@ -0,0 +1,26 @@
namespace ServiceLib.Tests.Services;
public class SpeedtestServiceResultTests
{
[Test]
[Arguments(ESpeedActionType.Speedtest, 0)]
[Arguments(ESpeedActionType.Mixedtest, 0)]
[Arguments(ESpeedActionType.Realping, 12.5)]
public async Task StartingATest_ShouldClearTheLastSpeedOfSpeedTests(ESpeedActionType actionType, double expected)
{
var indexId = Guid.NewGuid().ToString();
ProfileExManager.Instance.SetTestSpeed(indexId, 12.5m);
await SetTestResult([new() { IndexId = indexId }], actionType, ResUI.Speedtesting);
var profileEx = (await ProfileExManager.Instance.GetProfileExs()).First(t => t.IndexId == indexId);
await profileEx.Speed.Should().BeEqualTo((decimal)expected);
}
private static Task SetTestResult(List<ServerTestItem> items, ESpeedActionType actionType, string message)
{
var service = new SpeedtestService(new Config { SpeedTestItem = new() }, _ => Task.CompletedTask);
var method = typeof(SpeedtestService).GetMethod("SetTestResultAsync", BindingFlags.Instance | BindingFlags.NonPublic)!;
return (Task)method.Invoke(service, [items, actionType, message])!;
}
}
@@ -0,0 +1,113 @@
using ReactiveUI.Builder;
using ServiceLib.Tests.CoreConfig;
using ServiceLib.ViewModels;
namespace ServiceLib.Tests.ViewModels;
[NotInParallel]
public class ClashProxiesViewModelTests
{
static ClashProxiesViewModelTests()
{
RxAppBuilder.CreateReactiveUIBuilder().WithCoreServices().BuildApp();
}
[Test]
[Arguments("Selector", true)]
[Arguments("URLTest", true)]
[Arguments("LoadBalance", true)]
[Arguments("Fallback", true)]
[Arguments("selector", true)]
[Arguments("SELECTOR", true)]
[Arguments("Direct", false)]
[Arguments(null, false)]
public async Task GroupDelay_ShouldTestAllowedGroupTypes(string? type, bool allowed)
{
var configField = typeof(AppManager).GetField("_config", BindingFlags.Instance | BindingFlags.NonPublic)!;
var portField = typeof(AppManager).GetField("_statePort2", BindingFlags.Instance | BindingFlags.NonPublic)!;
var viewConfigField = typeof(MyReactiveObject).GetField("_config", BindingFlags.Static | BindingFlags.NonPublic)!;
var previousConfig = configField.GetValue(AppManager.Instance);
var previousPort = portField.GetValue(AppManager.Instance);
var previousViewConfig = viewConfigField.GetValue(null);
await using var server = new DelayServer();
try
{
CoreConfigTestFactory.BindAppManagerConfig(CoreConfigTestFactory.CreateConfig());
portField.SetValue(AppManager.Instance, server.Port);
var viewModel = new ClashProxiesViewModel();
var item = new ClashItem();
item.Proxies.Add("test-group", new() { type = type, all = ["test-node"] });
item.Proxies.Add("test-node", new() { type = "Shadowsocks" });
typeof(ClashProxiesViewModel).GetField("_clashItem", BindingFlags.Instance | BindingFlags.NonPublic)!
.SetValue(viewModel, item);
viewModel.SelectedGroup = new ClashProxyModel { Name = "test-group" };
var method = typeof(ClashProxiesViewModel).GetMethod("TestGroupProxiesDelay", BindingFlags.Instance | BindingFlags.NonPublic)!;
await ((Task)method.Invoke(viewModel, null)!).WaitAsync(TimeSpan.FromSeconds(10));
await server.Requests.Count.Should().BeEqualTo(allowed ? 1 : 0);
await viewModel.ProxyDetails.Single().DelayName.Should().BeEqualTo(allowed ? "42ms" : string.Empty);
if (allowed)
{
await server.Requests.Single().StartsWith("GET /proxies/test-node/delay?").Should().BeTrue();
}
}
finally
{
configField.SetValue(AppManager.Instance, previousConfig);
portField.SetValue(AppManager.Instance, previousPort);
viewConfigField.SetValue(null, previousViewConfig);
}
}
private sealed class DelayServer : IAsyncDisposable
{
private readonly TcpListener _listener = new(IPAddress.Loopback, 0);
private readonly CancellationTokenSource _cancellation = new();
private readonly Task _serverTask;
public int Port => ((IPEndPoint)_listener.LocalEndpoint).Port;
public ConcurrentQueue<string> Requests { get; } = new();
public DelayServer()
{
_listener.Start();
_serverTask = ServeAsync();
}
private async Task ServeAsync()
{
var token = _cancellation.Token;
while (!token.IsCancellationRequested)
{
using var client = await _listener.AcceptTcpClientAsync(token);
await using var stream = client.GetStream();
using var reader = new StreamReader(stream, Encoding.ASCII, leaveOpen: true);
var request = await reader.ReadLineAsync(token);
while (await reader.ReadLineAsync(token) is { Length: > 0 })
{
}
Requests.Enqueue(request!);
const string body = "{\"delay\":42}";
var response = $"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {body.Length}\r\nConnection: close\r\n\r\n{body}";
await stream.WriteAsync(Encoding.ASCII.GetBytes(response), token);
}
}
public async ValueTask DisposeAsync()
{
await _cancellation.CancelAsync();
_listener.Stop();
try
{
await _serverTask;
}
catch (OperationCanceledException)
{
}
finally
{
_cancellation.Dispose();
}
}
}
}
@@ -43,9 +43,9 @@ public class McBeService : IUdpTest
{
// 0x1c | client alive time in ms (recorded from previous ping) |
// server GUID | Magic | string length | Edition
//
//
// Edition Example:
//
//
// MCPE;Dedicated Server;527;1.19.1;0;10;13253860892328930865;Bedrock level;Survival;1;19132;19133;
if (mcbeResponseBytes.Length < 48)
{
+3 -6
View File
@@ -135,16 +135,13 @@ public class UdpTestService
roundTripTime = currentRoundTripTime;
}
}
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
throw;
}
catch
catch (OperationCanceledException)
{
if (attempt == 1 && roundTripTime == TimeSpan.MaxValue)
{
throw;
}
// Timeout, continue to next attempt
}
}
+1 -1
View File
@@ -143,7 +143,7 @@ public static class Extension
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0)
{
return Signal.Defer(() => interaction.Handle(input))
return Signal.Defer(() => interaction.WhenHandled(input))
.Catch<TOutput, UnhandledInteractionException<TInput, TOutput>>(ex =>
{
Logging.SaveLog($"Unhandled interaction exception in {memberName} at {filePath}:{lineNumber}", ex);
+121 -10
View File
@@ -7,40 +7,45 @@ public class JsonUtils
private static readonly JsonSerializerOptions _defaultDeserializeOptions = new()
{
PropertyNameCaseInsensitive = true,
ReadCommentHandling = JsonCommentHandling.Skip
ReadCommentHandling = JsonCommentHandling.Skip,
TypeInfoResolver = AppJsonContext.Default,
};
private static readonly JsonSerializerOptions _defaultSerializeOptions = new()
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
TypeInfoResolver = AppJsonContext.Default,
};
private static readonly JsonSerializerOptions _defaultSerializeNoIndentedOptions = new()
{
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
TypeInfoResolver = AppJsonContext.Default,
};
private static readonly JsonSerializerOptions _nullValueSerializeOptions = new()
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
TypeInfoResolver = AppJsonContext.Default,
};
private static readonly JsonSerializerOptions _nullValueSerializeNoIndentedOptions = new()
{
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
TypeInfoResolver = AppJsonContext.Default,
};
private static readonly JsonDocumentOptions _defaultDocumentOptions = new()
{
CommentHandling = JsonCommentHandling.Skip
CommentHandling = JsonCommentHandling.Skip,
};
/// <summary>
@@ -109,7 +114,7 @@ public class JsonUtils
/// <param name="indented"></param>
/// <param name="nullValue"></param>
/// <returns></returns>
public static string Serialize(object? obj, bool indented = true, bool nullValue = false)
public static string Serialize<T>(T? obj, bool indented = true, bool nullValue = false)
{
var result = string.Empty;
try
@@ -140,7 +145,7 @@ public class JsonUtils
/// <param name="obj"></param>
/// <param name="options"></param>
/// <returns></returns>
public static string Serialize(object? obj, JsonSerializerOptions? options)
public static string SerializeCamelCase<T>(T? obj)
{
var result = string.Empty;
try
@@ -149,7 +154,14 @@ public class JsonUtils
{
return result;
}
result = JsonSerializer.Serialize(obj, options ?? _defaultSerializeOptions);
var options = new JsonSerializerOptions
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
TypeInfoResolver = AppJsonContext.Default,
};
result = JsonSerializer.Serialize(obj, options);
}
catch (Exception ex)
{
@@ -162,9 +174,108 @@ public class JsonUtils
/// SerializeToNode
/// </summary>
/// <param name="obj"></param>
/// <param name="nullValue"></param>
/// <returns></returns>
public static JsonNode? SerializeToNode(object? obj, JsonSerializerOptions? options = null)
public static JsonNode? SerializeToNode<T>(T? obj, bool nullValue = false)
{
var options = nullValue ? _nullValueSerializeOptions : _defaultSerializeOptions;
return JsonSerializer.SerializeToNode(obj, options);
}
public static JsonArray? SerializeToArray(IEnumerable<string>? values)
{
if (values is null)
{
return null;
}
var array = new JsonArray();
foreach (var value in values)
{
var valueNode = JsonSerializer.SerializeToNode(value, _defaultSerializeOptions);
array.Add(valueNode);
}
return array;
}
}
[JsonSerializable(typeof(Config))]
[JsonSerializable(typeof(RulesItem))]
[JsonSerializable(typeof(ProfileItem))]
[JsonSerializable(typeof(List<RulesItem>))]
[JsonSerializable(typeof(List<ProfileItem>))]
[JsonSerializable(typeof(ProfileItemModel))]
[JsonSerializable(typeof(List<ProfileItemModel>))]
[JsonSerializable(typeof(RoutingTemplate))]
[JsonSerializable(typeof(DNSItem))]
[JsonSerializable(typeof(SimpleDNSItem))]
[JsonSerializable(typeof(IPAPIInfo))]
[JsonSerializable(typeof(SsSIP008))]
[JsonSerializable(typeof(List<SsServer>))]
[JsonSerializable(typeof(SsServer))]
[JsonSerializable(typeof(VmessQRCode))]
[JsonSerializable(typeof(ClashProxies))]
[JsonSerializable(typeof(ClashProviders))]
[JsonSerializable(typeof(ClashConnections))]
[JsonSerializable(typeof(ProtocolExtraItem))]
[JsonSerializable(typeof(TransportExtraItem))]
[JsonSerializable(typeof(TrafficItem))]
[JsonSerializable(typeof(V2rayMetricsVars))]
[JsonSerializable(typeof(V2rayMetricsVarsLink))]
[JsonSerializable(typeof(GitHubRelease))]
[JsonSerializable(typeof(List<GitHubRelease>))]
[JsonSerializable(typeof(ServerStatItem))]
[JsonSerializable(typeof(WebDavItem))]
[JsonSerializable(typeof(KeyEventItem))]
[JsonSerializable(typeof(SubItem))]
[JsonSerializable(typeof(SingboxConfig))]
[JsonSerializable(typeof(Dns4Sbox))]
[JsonSerializable(typeof(Inbound4Sbox))]
[JsonSerializable(typeof(List<Inbound4Sbox>))]
[JsonSerializable(typeof(Endpoints4Sbox))]
[JsonSerializable(typeof(List<Endpoints4Sbox>))]
[JsonSerializable(typeof(Outbound4Sbox))]
[JsonSerializable(typeof(List<Outbound4Sbox>))]
[JsonSerializable(typeof(Rule4Sbox))]
[JsonSerializable(typeof(Ruleset4Sbox))]
[JsonSerializable(typeof(List<Rule4Sbox>))]
[JsonSerializable(typeof(List<Ruleset4Sbox>))]
[JsonSerializable(typeof(V2rayConfig))]
[JsonSerializable(typeof(Dns4Ray))]
[JsonSerializable(typeof(DnsServer4Ray))]
[JsonSerializable(typeof(Inbounds4Ray))]
[JsonSerializable(typeof(List<Inbounds4Ray>))]
[JsonSerializable(typeof(Outbounds4Ray))]
[JsonSerializable(typeof(List<Outbounds4Ray>))]
[JsonSerializable(typeof(RulesItem4Ray))]
[JsonSerializable(typeof(List<RulesItem4Ray>))]
[JsonSerializable(typeof(Finalmask4Ray))]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(long))]
[JsonSerializable(typeof(double))]
[JsonSerializable(typeof(decimal))]
[JsonSerializable(typeof(bool))]
[JsonSerializable(typeof(Guid))]
[JsonSerializable(typeof(DateTime))]
[JsonSerializable(typeof(DateTimeOffset))]
[JsonSerializable(typeof(TimeSpan))]
[JsonSerializable(typeof(int?))]
[JsonSerializable(typeof(long?))]
[JsonSerializable(typeof(double?))]
[JsonSerializable(typeof(bool?))]
[JsonSerializable(typeof(DateTime?))]
[JsonSerializable(typeof(Guid?))]
[JsonSerializable(typeof(string[]))]
[JsonSerializable(typeof(int[]))]
[JsonSerializable(typeof(byte[]))]
[JsonSerializable(typeof(List<string>))]
[JsonSerializable(typeof(List<int>))]
[JsonSerializable(typeof(Dictionary<string, string>))]
[JsonSerializable(typeof(Dictionary<string, object>))]
[JsonSerializable(typeof(object))]
[JsonSerializable(typeof(JsonElement))]
[JsonSerializable(typeof(JsonDocument))]
[JsonSerializable(typeof(JsonNode))]
[JsonSerializable(typeof(JsonObject))]
[JsonSerializable(typeof(JsonArray))]
internal partial class AppJsonContext : JsonSerializerContext;
+33 -5
View File
@@ -348,10 +348,9 @@ public class Utils
.ToList());
}
public static List<string> GetEnumNames<TEnum>() where TEnum : Enum
public static List<string> GetEnumNames<TEnum>() where TEnum : struct, Enum
{
return Enum.GetValues(typeof(TEnum))
.Cast<TEnum>()
return Enum.GetValues<TEnum>()
.Select(e => e.ToString())
.ToList();
}
@@ -501,6 +500,31 @@ public class Utils
};
}
public static List<(string, string)> ParseHeaders(string? headers)
{
var result = new List<(string, string)>();
if (headers.IsNullOrEmpty())
{
return result;
}
var lines = headers.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
var parts = line.Split(new[] { ':' }, 2);
if (parts.Length != 2)
{
continue;
}
var key = parts[0].Trim();
var value = parts[1].Trim();
if (!string.IsNullOrEmpty(key))
{
result.Add((key, value));
}
}
return result;
}
#endregion Conversion Functions
#region Data Checks
@@ -1106,7 +1130,7 @@ public class Utils
}
//Check if it is installed by Windows WinGet
if (IsWindows() && basePath.Contains("Users") && basePath.Contains("WinGet"))
if (IsWindows() && basePath.Contains("WinGet"))
{
return false;
}
@@ -1416,7 +1440,11 @@ public class Utils
if (File.Exists(fileName))
{
var currentMode = File.GetUnixFileMode(fileName);
File.SetUnixFileMode(fileName, currentMode | UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute);
var executableMode = currentMode | UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute;
if (currentMode != executableMode)
{
File.SetUnixFileMode(fileName, executableMode);
}
return true;
}
}
+38 -21
View File
@@ -1,6 +1,5 @@
using YamlDotNet.Core;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
using YamlDotNet.RepresentationModel;
namespace ServiceLib.Common;
@@ -13,45 +12,57 @@ public class YamlUtils
/// <summary>
/// Deserialize
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="str"></param>
/// <returns></returns>
public static T FromYaml<T>(string str)
public static YamlMappingNode FromYaml(string str)
{
var deserializer = new DeserializerBuilder()
.WithNamingConvention(PascalCaseNamingConvention.Instance)
.Build();
//var deserializer = new DeserializerBuilder()
// .WithNamingConvention(PascalCaseNamingConvention.Instance)
// .Build();
//try
//{
// var obj = deserializer.Deserialize<T>(str);
// return obj;
//}
//catch (Exception ex)
//{
// Logging.SaveLog(_tag, ex);
// return deserializer.Deserialize<T>("");
//}
try
{
var obj = deserializer.Deserialize<T>(str);
return obj;
var yaml = new YamlStream();
using var reader = new StringReader(str);
yaml.Load(reader);
var rootNode = (YamlMappingNode)yaml.Documents[0].RootNode;
return rootNode;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
return deserializer.Deserialize<T>("");
return new YamlMappingNode();
}
}
/// <summary>
/// Serialize
/// </summary>
/// <param name="obj"></param>
/// <param name="node"></param>
/// <returns></returns>
public static string ToYaml(object? obj)
public static string ToYaml(YamlMappingNode? node)
{
var result = string.Empty;
if (obj == null)
if (node == null)
{
return result;
}
var serializer = new SerializerBuilder()
.WithNamingConvention(HyphenatedNamingConvention.Instance)
.Build();
try
{
result = serializer.Serialize(obj);
var doc = new YamlDocument(node);
var stream = new YamlStream(doc);
using var writer = new StringWriter();
stream.Save(writer, assignAnchors: false);
result = writer.ToString();
}
catch (Exception ex)
{
@@ -64,9 +75,15 @@ public class YamlUtils
{
try
{
var mergingParser = new MergingParser(new Parser(new StringReader(str)));
var obj = new DeserializerBuilder().Build().Deserialize(mergingParser);
return ToYaml(obj);
using var reader = new StringReader(str);
var parser = new Parser(reader);
var mergingParser = new MergingParser(parser);
var yamlStream = new YamlStream();
yamlStream.Load(mergingParser);
using var writer = new StringWriter();
yamlStream.Save(writer, assignAnchors: false);
return writer.ToString();
}
catch (Exception ex)
{
+1
View File
@@ -15,6 +15,7 @@ public enum EConfigType
Anytls = 11,
Naive = 12,
Outbound = 13,
MASQUE = 14,
PolicyGroup = 101,
ProxyChain = 102,
}
+22 -4
View File
@@ -42,6 +42,7 @@ public class Global
public const string SingboxFakeIPFilterFileName = NamespaceSample + "singbox_fakeip_filter";
public const string ChromeRootCertFileName = NamespaceSample + "chrome_roots_pem";
public const string MozillaRootCertFileName = NamespaceSample + "mozilla_roots_pem";
public const string TaskXmlTemplateFileName = NamespaceSample + "TaskXmlTemplate";
public const string ChromeRootProvider = "chrome";
public const string MozillaRootProvider = "mozilla";
@@ -261,7 +262,8 @@ public class Global
{ EConfigType.TUIC, "tuic" },
{ EConfigType.WireGuard, "wireguard" },
{ EConfigType.Anytls, "anytls" },
{ EConfigType.Naive, "naive" }
{ EConfigType.Naive, "naive" },
{ EConfigType.MASQUE, "masque" },
};
public static readonly List<string> VmessSecurities =
@@ -371,6 +373,7 @@ public class Global
EConfigType.Trojan,
EConfigType.Hysteria2,
EConfigType.WireGuard,
EConfigType.MASQUE,
EConfigType.SOCKS,
EConfigType.HTTP,
];
@@ -386,6 +389,7 @@ public class Global
EConfigType.Anytls,
EConfigType.Naive,
EConfigType.WireGuard,
EConfigType.MASQUE,
EConfigType.SOCKS,
EConfigType.HTTP,
];
@@ -495,6 +499,20 @@ public class Global
"localhost"
];
public static readonly List<string> DomainDNS4PreResolveAddress =
[
"",
"https://doh.pub/dns-query",
"https://dns.alidns.com/dns-query",
"119.29.29.29",
"223.5.5.5",
"localhost",
"https://cloudflare-dns.com/dns-query",
"https://dns.google/dns-query",
"8.8.8.8",
"1.1.1.1",
];
public static readonly List<LanguageOption> LanguageOptions =
[
new("zh-Hans", "简体中文"),
@@ -653,10 +671,10 @@ public class Global
{ ECoreType.Xray, "XTLS/Xray-core" },
{ ECoreType.sing_box, "SagerNet/sing-box" },
{ ECoreType.mihomo, "MetaCubeX/mihomo" },
{ ECoreType.hysteria, "apernet/hysteria" },
{ ECoreType.hysteria2, "apernet/hysteria" },
{ ECoreType.hysteria, "HyNetworks/hysteria" },
{ ECoreType.hysteria2, "HyNetworks/hysteria" },
{ ECoreType.naiveproxy, "klzgrad/naiveproxy" },
{ ECoreType.tuic, "EAimTY/tuic" },
{ ECoreType.tuic, "tuic-protocol/tuic" },
{ ECoreType.juicity, "juicity/juicity" },
{ ECoreType.brook, "txthinking/brook" },
{ ECoreType.overtls, "ShadowsocksR-Live/overtls" },
@@ -87,6 +87,7 @@ public static class AutoStartupHandler
[SupportedOSPlatform("windows")]
public static void AutoStartTaskService(string taskName, string fileName, string description)
{
#if false
if (taskName.IsNullOrEmpty())
{
return;
@@ -117,6 +118,78 @@ public static class AutoStartupHandler
task.Actions.Add(new Microsoft.Win32.TaskScheduler.ExecAction(fileName.AppendQuotes(), null, Path.GetDirectoryName(fileName)));
taskService.RootFolder.RegisterTaskDefinition(taskName, task);
#endif
if (string.IsNullOrEmpty(taskName))
{
return;
}
RunSchtasks($"/Delete /TN \"{taskName}\" /F");
if (string.IsNullOrEmpty(fileName))
{
return;
}
var workingDir = Path.GetDirectoryName(fileName) ?? string.Empty;
var taskXml = EmbedUtils.GetEmbedText(Global.TaskXmlTemplateFileName);
taskXml = taskXml
.Replace("{{DESCRIPTION}}", System.Security.SecurityElement.Escape(description ?? ""))
.Replace("{{EXEC_PATH}}", System.Security.SecurityElement.Escape(fileName ?? ""))
.Replace("{{ARGUMENTS}}", string.Empty)
.Replace("{{WORKING_DIRECTORY}}", System.Security.SecurityElement.Escape(workingDir ?? ""));
var taskXmlFileName = Utils.GetTempPath($"{Utils.GetGuid(false)}.xml");
File.WriteAllText(taskXmlFileName, taskXml);
RunSchtasks(
"/Create",
"/TN", taskName,
"/XML", taskXmlFileName,
"/F"
);
}
private static bool RunSchtasks(params string[] arguments)
{
var psi = new ProcessStartInfo
{
FileName = "schtasks.exe",
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
};
foreach (var argument in arguments)
{
psi.ArgumentList.Add(argument);
}
using var process = Process.Start(psi);
if (process == null)
{
return false;
}
process.WaitForExit();
var output = process.StandardOutput.ReadToEnd();
var error = process.StandardError.ReadToEnd();
if (!string.IsNullOrEmpty(output))
{
Logging.SaveLog($"RunSchtasks output: {output}");
}
if (!string.IsNullOrEmpty(error))
{
Logging.SaveLog($"RunSchtasks error: {error}");
}
return process.ExitCode == 0;
}
private static string GetAutoRunNameWindows()
@@ -49,6 +49,7 @@ public class CoreConfigContextBuilder
RoutingItem = await ConfigHandler.GetDefaultRouting(config),
IsWindows = Utils.IsWindows(),
IsMacOS = Utils.IsMacOS(),
IsLinux = Utils.IsLinux(),
HasGlobalIPv6Address = Utils.HasGlobalIPv6Address(),
ProtectCoreTypeList = config.TunModeItem.EnableTun ? [ECoreType.Xray, ECoreType.sing_box] : []
};
+113 -4
View File
@@ -292,6 +292,7 @@ public static class ConfigHandler
EConfigType.WireGuard => await AddWireguardServer(config, item),
EConfigType.Anytls => await AddAnytlsServer(config, item),
EConfigType.Naive => await AddNaiveServer(config, item),
EConfigType.MASQUE => await AddMasqueServer(config, item),
_ => -1,
};
return ret;
@@ -539,6 +540,67 @@ public static class ConfigHandler
return await Task.FromResult(0);
}
public static Task<int> MoveServersTo(
Config config,
IReadOnlyList<string> orderedProfileIds,
IReadOnlyList<int> sourceIndices,
int insertIndex)
{
ArgumentNullException.ThrowIfNull(orderedProfileIds);
ArgumentNullException.ThrowIfNull(sourceIndices);
if (orderedProfileIds.Count == 0 ||
sourceIndices.Count == 0 ||
insertIndex < 0 ||
insertIndex > orderedProfileIds.Count)
{
return Task.FromResult(-1);
}
int[] movingIndices = sourceIndices
.Distinct()
.OrderBy(index => index)
.ToArray();
if (movingIndices.Length != sourceIndices.Count ||
movingIndices.Any(index =>
index < 0 || index >= orderedProfileIds.Count))
{
return Task.FromResult(-1);
}
var movingIndexSet = movingIndices.ToHashSet();
var movingIds = movingIndices
.Select(index => orderedProfileIds[index])
.ToList();
var remainingIds = orderedProfileIds
.Where((_, index) => !movingIndexSet.Contains(index))
.ToList();
if (insertIndex > remainingIds.Count)
{
return Task.FromResult(-1);
}
remainingIds.InsertRange(insertIndex, movingIds);
if (remainingIds.Count != orderedProfileIds.Count)
{
return Task.FromResult(-1);
}
for (var index = 0; index < remainingIds.Count; index++)
{
ProfileExManager.Instance.SetSort(
remainingIds[index],
(index + 1) * 10);
}
return Task.FromResult(0);
}
/// <summary>
/// Add a custom server configuration from a file
/// Copies the configuration file to the app's config directory
@@ -992,6 +1054,25 @@ public static class ConfigHandler
return 0;
}
public static async Task<int> AddMasqueServer(Config config, ProfileItem profileItem, bool toFile = true)
{
profileItem.ConfigType = EConfigType.MASQUE;
profileItem.Address = profileItem.Address.TrimEx();
profileItem.Username = profileItem.Username.TrimEx();
profileItem.Password = profileItem.Password.TrimEx();
if (profileItem.StreamSecurity.IsNullOrEmpty())
{
profileItem.StreamSecurity = Global.StreamSecurity;
}
if (profileItem.Password.IsNullOrEmpty())
{
return -1;
}
await AddServerCommon(config, profileItem, toFile);
return 0;
}
/// <summary>
/// Sort the server list by the specified column
/// Updates the sort order in the profile extension data
@@ -1632,11 +1713,16 @@ public static class ConfigHandler
}
var subFilter = string.Empty;
var preResolver = string.Empty;
if (isSub && subid.IsNotEmpty())
{
subFilter = (await AppManager.Instance.GetSubItem(subid))?.Filter ?? "";
preResolver = (await AppManager.Instance.GetSubItem(subid))?.PreResolver ?? "";
}
var dnsClient = preResolver.IsNullOrEmpty() ? null : EndpointResolveManager.BuildDnsClient(preResolver);
var dnsCache = new Dictionary<string, string>();
var countServers = 0;
List<ProfileItem> lstAdd = [];
var arrData = strData.Split(Environment.NewLine.ToCharArray()).Where(t => !t.IsNullOrEmpty());
@@ -1672,6 +1758,21 @@ public static class ConfigHandler
profileItem.Subid = subid;
profileItem.IsSub = isSub;
if (dnsClient is not null && profileItem.Address.IsNotEmpty() && !Utils.IsIpAddress(profileItem.Address))
{
if (!dnsCache.TryGetValue(profileItem.Address, out var resolvedAddress))
{
var ipList = await dnsClient.ResolveIpv46Async(profileItem.Address);
resolvedAddress = ipList.FirstOrDefault()?.ToString() ?? profileItem.Address;
dnsCache[profileItem.Address] = resolvedAddress;
}
if (!resolvedAddress.IsNullOrEmpty())
{
profileItem.Address = resolvedAddress;
}
// TODO: Override the host and sni if empty
}
var addStatus = profileItem.ConfigType switch
{
EConfigType.VMess => await AddVMessServer(config, profileItem, false),
@@ -1684,6 +1785,7 @@ public static class ConfigHandler
EConfigType.WireGuard => await AddWireguardServer(config, profileItem, false),
EConfigType.Anytls => await AddAnytlsServer(config, profileItem, false),
EConfigType.Naive => await AddNaiveServer(config, profileItem, false),
EConfigType.MASQUE => await AddMasqueServer(config, profileItem, false),
_ => -1,
};
@@ -1753,7 +1855,7 @@ public static class ConfigHandler
}
if (lstProfiles.Count > 0)
{
var count = await AddBatchCustomServers(config, lstProfiles, subid, isSub);
var count = await AddBatchCustomServers(config, lstProfiles, subid, isSub, subItem);
if (count > 0)
{
return count;
@@ -1799,7 +1901,7 @@ public static class ConfigHandler
if (lstProfiles?.Count > 0)
{
var count = await AddBatchCustomServers(config, lstProfiles, subid, isSub);
var count = await AddBatchCustomServers(config, lstProfiles, subid, isSub, subItem);
if (count > 0)
{
return count;
@@ -1813,7 +1915,8 @@ public static class ConfigHandler
Config config,
List<ProfileItem> lstProfiles,
string subid,
bool isSub)
bool isSub,
SubItem? subItem)
{
var count = 0;
foreach (var it in lstProfiles)
@@ -1823,6 +1926,7 @@ public static class ConfigHandler
if (it.ConfigType == EConfigType.Custom)
{
it.PreSocksPort = subItem?.PreSocksPort;
if (await AddCustomServer(config, it, true) == 0)
{
count++;
@@ -2046,7 +2150,6 @@ public static class ConfigHandler
{
lstOriSub = await AppManager.Instance.ProfileItems(subid);
activeProfile = lstOriSub?.FirstOrDefault(t => t.IndexId == config.IndexId);
await RemoveServersViaSubid(config, subid, true);
}
var counter = 0;
@@ -2106,6 +2209,11 @@ public static class ConfigHandler
counter = await AddBatchServers4Custom(config, strData, subid, isSub);
}
if (counter > 0 && lstOriSub is { Count: > 0 })
{
await RemoveServers(config, lstOriSub.Where(item => item.IsSub).ToList());
}
//Select active node
if (activeProfile != null)
{
@@ -2209,6 +2317,7 @@ public static class ConfigHandler
item.PreSocksPort = subItem.PreSocksPort;
item.Memo = subItem.Memo;
item.CustomCoreType = subItem.CustomCoreType;
item.PreResolver = subItem.PreResolver;
}
if (item.Id.IsNullOrEmpty())
+4 -24
View File
@@ -80,12 +80,7 @@ public class BaseFmt
{
var node = JsonUtils.ParseJson(item.Finalmask);
var finalmask = node != null
? JsonUtils.Serialize(node, new JsonSerializerOptions
{
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
})
? JsonUtils.Serialize(node, indented: false, nullValue: true)
: item.Finalmask;
dicQuery.Add("fm", Utils.UrlEncode(finalmask));
}
@@ -154,12 +149,7 @@ public class BaseFmt
{
var node = JsonUtils.ParseJson(transport.XhttpExtra);
var extra = node != null
? JsonUtils.Serialize(node, new JsonSerializerOptions
{
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
})
? JsonUtils.Serialize(node, indented: false, nullValue: true)
: transport.XhttpExtra;
dicQuery.Add("extra", UrlEncodeSafe(extra));
}
@@ -215,12 +205,7 @@ public class BaseFmt
{
var node = JsonUtils.ParseJson(finalmaskDecoded);
item.Finalmask = node != null
? JsonUtils.Serialize(node, new JsonSerializerOptions
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
})
? JsonUtils.Serialize(node, indented: true, nullValue: true)
: finalmaskDecoded;
}
else
@@ -278,12 +263,7 @@ public class BaseFmt
var node = JsonUtils.ParseJson(xhttpExtra);
if (node != null)
{
xhttpExtra = JsonUtils.Serialize(node, new JsonSerializerOptions
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
});
xhttpExtra = JsonUtils.Serialize(node, indented: true, nullValue: true);
}
}
+1 -6
View File
@@ -179,12 +179,7 @@ public class InnerFmt : BaseFmt
if (jsonObj.TryGetPropertyValue("CustomOutboundObj", out var customOutboundNode)
&& customOutboundNode is JsonObject customOutboundObj)
{
var customOutboundContent = JsonUtils.Serialize(customOutboundObj, new JsonSerializerOptions
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
});
var customOutboundContent = JsonUtils.Serialize(customOutboundObj, indented: true, nullValue: true);
customOutboundFilePath = WriteAllText(customOutboundContent);
jsonObj.Remove("CustomOutboundObj");
}
+2 -12
View File
@@ -29,12 +29,7 @@ public class WireguardFmt : BaseFmt
{
var node = JsonUtils.ParseJson(finalmaskDecoded);
item.Finalmask = node != null
? JsonUtils.Serialize(node, new JsonSerializerOptions
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
})
? JsonUtils.Serialize(node, indented: true, nullValue: true)
: finalmaskDecoded;
}
else
@@ -98,12 +93,7 @@ public class WireguardFmt : BaseFmt
{
var node = JsonUtils.ParseJson(item.Finalmask);
var finalmask = node != null
? JsonUtils.Serialize(node, new JsonSerializerOptions
{
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
})
? JsonUtils.Serialize(node, indented: false, nullValue: true)
: item.Finalmask;
dicQuery.Add("fm", Utils.UrlEncode(finalmask));
}
@@ -133,7 +133,7 @@ public static class ProxySettingWindows
list.dwOptionCount = options.Length;
list.dwOptionError = 0;
var optSize = Marshal.SizeOf(typeof(InternetConnectionOption));
var optSize = Marshal.SizeOf<InternetConnectionOption>();
// make a pointer out of all that ...
var optionsPtr = Marshal.AllocCoTaskMem(optSize * options.Length); // !! remember to deallocate memory 4
// copy the array over into that spot in memory ...
@@ -207,17 +207,17 @@ public static class ProxySettingWindows
var entries = 0;
// attempt to query with 1 entry buffer
var rasEntryNames = new RASENTRYNAME[1];
var bufferSize = Marshal.SizeOf(typeof(RASENTRYNAME));
rasEntryNames[0].dwSize = Marshal.SizeOf(typeof(RASENTRYNAME));
var bufferSize = Marshal.SizeOf<RASENTRYNAME>();
rasEntryNames[0].dwSize = Marshal.SizeOf<RASENTRYNAME>();
var result = NativeMethods.RasEnumEntries(null, null, rasEntryNames, ref bufferSize, ref entries);
// increase buffer if the buffer is not large enough
if (result == (uint)ErrorCode.ERROR_BUFFER_TOO_SMALL)
{
rasEntryNames = new RASENTRYNAME[bufferSize / Marshal.SizeOf(typeof(RASENTRYNAME))];
rasEntryNames = new RASENTRYNAME[bufferSize / Marshal.SizeOf<RASENTRYNAME>()];
for (var i = 0; i < rasEntryNames.Length; i++)
{
rasEntryNames[i].dwSize = Marshal.SizeOf(typeof(RASENTRYNAME));
rasEntryNames[i].dwSize = Marshal.SizeOf<RASENTRYNAME>();
}
result = NativeMethods.RasEnumEntries(null, null, rasEntryNames, ref bufferSize, ref entries);
@@ -258,7 +258,7 @@ public static class ProxySettingWindows
static InternetConnectionOption()
{
Size = Marshal.SizeOf(typeof(InternetConnectionOption));
Size = Marshal.SizeOf<InternetConnectionOption>();
}
// Nested Types
+5
View File
@@ -28,6 +28,11 @@ public sealed class SQLiteHelper
return await _dbAsync.InsertAllAsync(models, runInTransaction: true).ConfigureAwait(false);
}
public async Task<int> ReplaceAllAsync(IEnumerable models)
{
return await _dbAsync.InsertAllAsync(models, extra: "OR REPLACE", runInTransaction: true).ConfigureAwait(false);
}
public async Task<int> InsertAsync(object model)
{
return await _dbAsync.InsertAsync(model);
+1 -1
View File
@@ -229,7 +229,7 @@ public sealed class CoreInfoManager
DownloadUrlLinuxLoong64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-loong64.tar.gz",
DownloadUrlOSX64 = urlSingbox + "/download/{0}/sing-box-{1}-darwin-amd64.tar.gz",
DownloadUrlOSXArm64 = urlSingbox + "/download/{0}/sing-box-{1}-darwin-arm64.tar.gz",
LockedMaxVersion = new SemanticVersion(1, 14, int.MaxValue),
//LockedMaxVersion = new SemanticVersion(1, 14, int.MaxValue),
Match = "sing-box",
VersionArg = "version",
},
@@ -0,0 +1,523 @@
using System.Net.Http.Headers;
namespace ServiceLib.Manager;
public class EndpointResolveManager
{
public static IDnsResolver BuildDnsClient(string dnsServer)
{
if (string.IsNullOrEmpty(dnsServer))
{
throw new ArgumentNullException(nameof(dnsServer));
}
if (dnsServer is "local" or "localhost" or "system" or "dhcp" ||
dnsServer.StartsWith("127.0.0.1") ||
dnsServer.StartsWith("::1"))
{
return new SystemDnsResolver();
}
var (domain, scheme, port, path) = Utils.ParseUrl(dnsServer);
if (string.IsNullOrEmpty(scheme))
{
return new UdpDnsResolver(new IPEndPoint(IPAddress.Parse(domain), 53));
}
var processedScheme = scheme.ToLowerInvariant().Replace("+local", "");
switch (processedScheme)
{
case "udp":
return new UdpDnsResolver(new IPEndPoint(IPAddress.Parse(domain), port != 0 ? port : 53));
case "tcp":
return new TcpDnsResolver(new IPEndPoint(IPAddress.Parse(domain), port != 0 ? port : 53));
case "doh" or "https":
{
var uri = new Uri($"https://{domain}:{(port != 0 ? port : 443)}{path}");
var httpClient = new HttpClient();
return new DohDnsResolver(httpClient, uri);
}
default:
throw new NotSupportedException($"Unsupported DNS scheme: {scheme}");
}
}
public class DnsWire
{
#region Build DNS Query
public static byte[] BuildQuery(string host, ushort queryType)
{
if (queryType is not (1 or 28))
{
throw new ArgumentOutOfRangeException(nameof(queryType));
}
if (string.IsNullOrWhiteSpace(host))
{
throw new ArgumentException("Host is empty.", nameof(host));
}
host = host.TrimEnd('.');
if (host.Length == 0)
{
host = ".";
}
using var stream = new MemoryStream();
// Header
WriteUInt16(stream, 0);
WriteUInt16(stream, 0x0100);
WriteUInt16(stream, 1);
WriteUInt16(stream, 0);
WriteUInt16(stream, 0);
WriteUInt16(stream, 0);
// Question: QNAME
if (host != ".")
{
var labels = host.Split('.');
foreach (var label in labels)
{
if (label.Length is < 1 or > 63)
{
throw new ArgumentException($"Invalid DNS label length: {label.Length}.", nameof(host));
}
foreach (var c in label)
{
if (c > 0x7F)
{
throw new ArgumentException("Non-ASCII domain names must be converted to A-labels first.",
nameof(host));
}
}
stream.WriteByte((byte)label.Length);
var labelBytes = Encoding.ASCII.GetBytes(label);
stream.Write(labelBytes, 0, labelBytes.Length);
}
}
stream.WriteByte(0);
// Question: QTYPE & QCLASS
WriteUInt16(stream, queryType);
WriteUInt16(stream, 1);
return stream.ToArray();
}
private static void WriteUInt16(Stream stream, ushort value)
{
stream.WriteByte((byte)(value >> 8));
stream.WriteByte((byte)(value & 0xFF));
}
#endregion
#region Parse DNS Response
public static List<IPAddress> ParseAddresses(
ReadOnlySpan<byte> message,
ushort expectedType)
{
return ParseResponse(message, expectedType).Addresses;
}
public static DnsResponse ParseResponse(
ReadOnlySpan<byte> message,
ushort expectedType)
{
if (expectedType is not (1 or 28))
{
throw new ArgumentOutOfRangeException(nameof(expectedType));
}
if (message.Length < 12)
{
throw new InvalidDataException("DNS message is too short.");
}
var flags = ReadUInt16(message, 2);
var questionCount = ReadUInt16(message, 4);
var answerCount = ReadUInt16(message, 6);
// QR = 1
if ((flags & 0x8000) == 0)
{
throw new InvalidDataException("Not a DNS response.");
}
// RCODE
var rcode = flags & 0x000F;
if (rcode != 0)
{
return new DnsResponse([], null);
}
var offset = 12;
// Parse the Question section to obtain the query name.
string? queryName = null;
for (var i = 0; i < questionCount; i++)
{
var name = ReadName(message, offset, out offset);
queryName ??= NormalizeName(name);
if (offset + 4 > message.Length)
{
throw new InvalidDataException("Invalid DNS question.");
}
offset += 4; // QTYPE + QCLASS
}
var addresses = new List<IPAddress>();
string? cname = null;
for (var i = 0; i < answerCount; i++)
{
// NAME
var ownerName = ReadName(message, offset, out offset);
if (offset + 10 > message.Length)
{
throw new InvalidDataException("Invalid DNS answer.");
}
var type = ReadUInt16(message, offset);
var @class = ReadUInt16(message, offset + 2);
// TTL
var rdLength = ReadUInt16(message, offset + 8);
offset += 10;
if (offset + rdLength > message.Length)
{
throw new InvalidDataException("Invalid DNS RDATA.");
}
if (@class != 1)
{
offset += rdLength;
continue;
}
var ownerMatchesQuery = NamesEqual(ownerName, queryName);
var ownerMatchesCname = NamesEqual(ownerName, cname);
if (type == expectedType)
{
if (ownerMatchesQuery || ownerMatchesCname)
{
if (expectedType == 1 && rdLength == 4)
{
addresses.Add(new IPAddress(
message.Slice(offset, 4)));
}
else if (expectedType == 28 && rdLength == 16)
{
addresses.Add(new IPAddress(
message.Slice(offset, 16)));
}
}
}
else if (type == 5 && ownerMatchesQuery && cname is null)
{
cname = ReadName(message, offset, out _);
}
offset += rdLength;
}
return new DnsResponse(addresses, cname);
}
public static async Task<List<IPAddress>> ResolveWithCnameAsync(
string host,
bool ipv6,
Func<string, ushort, CancellationToken, Task<byte[]>> queryAsync,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(host))
{
throw new ArgumentException("Host is empty.", nameof(host));
}
var queryType = ipv6 ? (ushort)28 : (ushort)1;
var currentName = host.TrimEnd('.');
var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
const int maxHops = 8;
for (var hop = 0; hop < maxHops; hop++)
{
if (!visited.Add(currentName))
{
// CNAME loop detected.
return [];
}
var response = await queryAsync(currentName, queryType, cancellationToken);
var result = ParseResponse(response, queryType);
if (result.Addresses.Count > 0)
{
return result.Addresses;
}
if (string.IsNullOrWhiteSpace(result.Cname))
{
return [];
}
currentName = result.Cname.TrimEnd('.');
}
return [];
}
public static string ReadName(
ReadOnlySpan<byte> message,
int offset,
out int nextOffset)
{
var name = new StringBuilder();
var position = offset;
var endOffset = -1;
var pointerCount = 0;
while (true)
{
if ((uint)position >= (uint)message.Length)
{
throw new InvalidDataException("Invalid DNS name.");
}
var length = message[position];
// End of name.
if (length == 0)
{
position++;
if (endOffset < 0)
{
endOffset = position;
}
break;
}
// Compression pointer.
if ((length & 0xC0) == 0xC0)
{
if (position + 1 >= message.Length)
{
throw new InvalidDataException("Invalid DNS pointer.");
}
if (++pointerCount > 64)
{
throw new InvalidDataException("Too many DNS compression pointers.");
}
if (endOffset < 0)
{
endOffset = position + 2;
}
var pointer = ((length & 0x3F) << 8) | message[position + 1];
position = pointer;
continue;
}
// Reserved label types.
if ((length & 0xC0) != 0)
{
throw new InvalidDataException("Invalid DNS label.");
}
position++;
if (position + length > message.Length)
{
throw new InvalidDataException("Invalid DNS label length.");
}
if (name.Length > 0)
{
name.Append('.');
}
name.Append(Encoding.ASCII.GetString(message.Slice(position, length)));
position += length;
}
nextOffset = endOffset;
return name.ToString();
}
public sealed record DnsResponse(List<IPAddress> Addresses, string? Cname);
private static string? NormalizeName(string name)
{
name = name.TrimEnd('.');
return name.Length == 0 ? null : name;
}
private static bool NamesEqual(string? left, string? right)
{
return left is not null &&
right is not null &&
string.Equals(
left.TrimEnd('.'),
right.TrimEnd('.'),
StringComparison.OrdinalIgnoreCase);
}
private static ushort ReadUInt16(
ReadOnlySpan<byte> data,
int offset)
{
return (ushort)(
(data[offset] << 8) |
data[offset + 1]);
}
#endregion
}
public interface IDnsResolver
{
public Task<List<IPAddress>> ResolveAsync(
string host,
bool ipv6,
CancellationToken cancellationToken = default);
public async Task<List<IPAddress>> ResolveIpv46Async(
string host,
CancellationToken cancellationToken = default)
{
var ipv4Task = ResolveAsync(host, false, cancellationToken);
var ipv6Task = ResolveAsync(host, true, cancellationToken);
var results = await Task.WhenAll(ipv4Task, ipv6Task);
return results.SelectMany(x => x).ToList();
}
}
public class UdpDnsResolver(IPEndPoint dnsServer) : IDnsResolver
{
public async Task<List<IPAddress>> ResolveAsync(
string host,
bool ipv6,
CancellationToken cancellationToken = default)
{
return await DnsWire.ResolveWithCnameAsync(
host,
ipv6,
async (name, queryType, ct) =>
{
var query = DnsWire.BuildQuery(name, queryType);
using var udpClient = new UdpClient();
udpClient.Connect(dnsServer);
await udpClient.SendAsync(query, query.Length);
var result = await udpClient.ReceiveAsync(ct);
return result.Buffer;
},
cancellationToken);
}
}
public class TcpDnsResolver(IPEndPoint dnsServer) : IDnsResolver
{
public async Task<List<IPAddress>> ResolveAsync(
string host,
bool ipv6,
CancellationToken cancellationToken = default)
{
return await DnsWire.ResolveWithCnameAsync(
host,
ipv6,
async (name, queryType, ct) =>
{
var query = DnsWire.BuildQuery(name, queryType);
using var tcpClient = new TcpClient();
await tcpClient.ConnectAsync(dnsServer.Address, dnsServer.Port, ct);
await using var networkStream = tcpClient.GetStream();
await networkStream.WriteAsync(query, 0, query.Length, ct);
// Read the length of the response (2 bytes)
var lengthBuffer = new byte[2];
await networkStream.ReadExactlyAsync(lengthBuffer, 0, 2, ct);
var responseLength = (lengthBuffer[0] << 8) | lengthBuffer[1];
// Read the actual response
var responseBuffer = new byte[responseLength];
await networkStream.ReadExactlyAsync(responseBuffer, 0, responseLength, ct);
return responseBuffer;
},
cancellationToken);
}
}
public class DohDnsResolver(HttpClient httpClient, Uri dohServer) : IDnsResolver
{
public async Task<List<IPAddress>> ResolveAsync(
string host,
bool ipv6,
CancellationToken cancellationToken = default)
{
return await DnsWire.ResolveWithCnameAsync(
host,
ipv6,
async (name, queryType, ct) =>
{
var query = DnsWire.BuildQuery(name, queryType);
using var request = new HttpRequestMessage(HttpMethod.Post, dohServer);
request.Content = new ByteArrayContent(query)
{
Headers =
{
ContentType = new MediaTypeHeaderValue("application/dns-message"),
},
};
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/dns-message"));
using var response =
await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ct);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsByteArrayAsync(ct);
},
cancellationToken);
}
}
public class SystemDnsResolver : IDnsResolver
{
public async Task<List<IPAddress>> ResolveAsync(
string host,
bool ipv6,
CancellationToken cancellationToken = default)
{
var addresses = await Dns.GetHostAddressesAsync(host, cancellationToken);
return addresses.Where(ip => (!ipv6 && ip.AddressFamily == AddressFamily.InterNetwork) ||
(ipv6 && ip.AddressFamily == AddressFamily.InterNetworkV6))
.ToList();
}
public async Task<List<IPAddress>> ResolveIpv46Async(
string host,
CancellationToken cancellationToken = default)
{
var addresses = await Dns.GetHostAddressesAsync(host, cancellationToken);
return addresses.ToList();
}
}
}
+11 -5
View File
@@ -65,11 +65,17 @@ public class PacManager
{
while (!token.IsCancellationRequested)
{
using var client = await listener.AcceptTcpClientAsync(token).ConfigureAwait(false);
await using var stream = client.GetStream();
_ = await stream.ReadAsync(buffer, token).ConfigureAwait(false);
await stream.WriteAsync(_writeContent, token).ConfigureAwait(false);
await stream.FlushAsync(token).ConfigureAwait(false);
try
{
using var client = await listener.AcceptTcpClientAsync(token).ConfigureAwait(false);
await using var stream = client.GetStream();
_ = await stream.ReadAsync(buffer, token).ConfigureAwait(false);
await stream.WriteAsync(_writeContent, token).ConfigureAwait(false);
await stream.FlushAsync(token).ConfigureAwait(false);
}
catch
{
}
}
}
catch (OperationCanceledException) when (token.IsCancellationRequested)
+37 -28
View File
@@ -4,7 +4,8 @@ public class ProfileExManager
{
private static readonly Lazy<ProfileExManager> _instance = new(() => new());
private ConcurrentBag<ProfileExItem> _lstProfileEx = [];
private readonly Queue<string> _queIndexIds = new();
private readonly ConcurrentQueue<string> _queIndexIds = new();
private readonly Lock _profileExLock = new();
public static ProfileExManager Instance => _instance.Value;
private static readonly string _tag = "ProfileExHandler";
@@ -27,56 +28,51 @@ public class ProfileExManager
{
await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileExItem where indexId not in ( select indexId from ProfileItem )");
_lstProfileEx = new(await SQLiteHelper.Instance.TableAsync<ProfileExItem>().ToListAsync());
var lstProfileEx = new ConcurrentBag<ProfileExItem>(await SQLiteHelper.Instance.TableAsync<ProfileExItem>().ToListAsync());
lock (_profileExLock)
{
_lstProfileEx = lstProfileEx;
}
}
private void IndexIdEnqueue(string indexId)
{
if (indexId.IsNotEmpty() && !_queIndexIds.Contains(indexId))
if (indexId.IsNotEmpty())
{
//Speed tests touch this from many threads; the save loop deduplicates IDs.
_queIndexIds.Enqueue(indexId);
}
}
private async Task SaveQueueIndexIds()
{
var cnt = _queIndexIds.Count;
if (cnt > 0)
var lstIds = new HashSet<string>();
while (_queIndexIds.TryDequeue(out var id))
{
var lstExists = await SQLiteHelper.Instance.TableAsync<ProfileExItem>().ToListAsync();
List<ProfileExItem> lstInserts = [];
List<ProfileExItem> lstUpdates = [];
lstIds.Add(id);
}
for (var i = 0; i < cnt; i++)
if (lstIds.Count > 0)
{
List<ProfileExItem> lstSaves = [];
foreach (var id in lstIds)
{
var id = _queIndexIds.Dequeue();
var item = lstExists.FirstOrDefault(t => t.IndexId == id);
var itemNew = _lstProfileEx?.FirstOrDefault(t => t.IndexId == id);
if (itemNew is null)
{
continue;
}
if (item is not null)
{
lstUpdates.Add(itemNew);
}
else
{
lstInserts.Add(itemNew);
}
lstSaves.Add(itemNew);
}
try
{
if (lstInserts.Count > 0)
if (lstSaves.Count > 0)
{
await SQLiteHelper.Instance.InsertAllAsync(lstInserts);
}
if (lstUpdates.Count > 0)
{
await SQLiteHelper.Instance.UpdateAllAsync(lstUpdates);
//Another save may have inserted these rows since the queue was drained.
await SQLiteHelper.Instance.ReplaceAllAsync(lstSaves);
}
}
catch (Exception ex)
@@ -103,13 +99,26 @@ public class ProfileExManager
private ProfileExItem GetProfileExItem(string? indexId)
{
return _lstProfileEx.FirstOrDefault(t => t.IndexId == indexId) ?? AddProfileEx(indexId);
var profileEx = _lstProfileEx.FirstOrDefault(t => t.IndexId == indexId);
if (profileEx is not null)
{
return profileEx;
}
lock (_profileExLock)
{
profileEx = _lstProfileEx.FirstOrDefault(t => t.IndexId == indexId);
return profileEx ?? AddProfileEx(indexId);
}
}
public async Task ClearAll()
{
await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileExItem ");
_lstProfileEx = [];
lock (_profileExLock)
{
_lstProfileEx = [];
}
}
public async Task SaveTo()
+1 -1
View File
@@ -134,7 +134,7 @@ public class TaskManager
var updateService = new UpdateService(_config, async (success, msg) => await Task.CompletedTask);
var msgs = await updateService.CheckHasUpdateOnlyAll(_config.CheckUpdateItem.CheckPreReleaseUpdate, _config.CheckUpdateItem.UpdateViaProxy);
var msgs = await updateService.CheckHasUpdateOnlyAll(_config.CheckUpdateItem.UpdateViaProxy);
foreach (var msg in msgs)
{
await _updateFunc?.Invoke(false, msg);
@@ -1,6 +1,5 @@
namespace ServiceLib.Models.Configs;
[Serializable]
public class Config
{
#region property
@@ -1,6 +1,5 @@
namespace ServiceLib.Models.Configs;
[Serializable]
public class CoreBasicItem
{
public bool LogEnabled { get; set; }
@@ -22,7 +21,6 @@ public class CoreBasicItem
public bool EnableCacheFile4Sbox { get; set; } = true;
}
[Serializable]
public class InItem
{
public int LocalPort { get; set; }
@@ -38,7 +36,6 @@ public class InItem
public bool SecondLocalPortEnabled { get; set; }
}
[Serializable]
public class KcpItem
{
public int Mtu { get; set; }
@@ -54,7 +51,6 @@ public class KcpItem
public int MaxSendingWindow { get; set; }
}
[Serializable]
public class GrpcItem
{
public int? IdleTimeout { get; set; }
@@ -63,7 +59,6 @@ public class GrpcItem
public int? InitialWindowsSize { get; set; }
}
[Serializable]
public class GUIItem
{
public bool AutoRun { get; set; }
@@ -77,14 +72,12 @@ public class GUIItem
public string? RootCertProvider { get; set; }
}
[Serializable]
public class MsgUIItem
{
public string? MainMsgFilter { get; set; }
public bool? AutoRefresh { get; set; }
}
[Serializable]
public class UIItem
{
public bool EnableAutoAdjustMainLvColWidth { get; set; }
@@ -106,7 +99,6 @@ public class UIItem
public bool HideColumnIpInfo { get; set; }
}
[Serializable]
public class ConstItem
{
public string? SubConvertUrl { get; set; }
@@ -115,7 +107,6 @@ public class ConstItem
public string? RouteRulesTemplateSourceUrl { get; set; }
}
[Serializable]
public class KeyEventItem
{
public EGlobalHotkey EGlobalHotkey { get; set; }
@@ -129,7 +120,6 @@ public class KeyEventItem
public int? KeyCode { get; set; }
}
[Serializable]
public class CoreTypeItem
{
public EConfigType ConfigType { get; set; }
@@ -137,7 +127,6 @@ public class CoreTypeItem
public ECoreType CoreType { get; set; }
}
[Serializable]
public class TunModeItem
{
public bool EnableTun { get; set; }
@@ -153,7 +142,6 @@ public class TunModeItem
public string IPv6Address { get; set; }
}
[Serializable]
public class SpeedTestItem
{
public int SpeedTestTimeout { get; set; }
@@ -166,7 +154,6 @@ public class SpeedTestItem
public int? SpeedTestDelayInterval { get; set; }
}
[Serializable]
public class RoutingBasicItem
{
public string DomainStrategy { get; set; }
@@ -174,7 +161,6 @@ public class RoutingBasicItem
public string RoutingIndexId { get; set; }
}
[Serializable]
public class ColumnItem
{
public string Name { get; set; }
@@ -182,7 +168,6 @@ public class ColumnItem
public int Index { get; set; }
}
[Serializable]
public class Mux4RayItem
{
public int? Concurrency { get; set; }
@@ -190,7 +175,6 @@ public class Mux4RayItem
public string? XudpProxyUDP443 { get; set; }
}
[Serializable]
public class Mux4SboxItem
{
public string Protocol { get; set; }
@@ -198,7 +182,6 @@ public class Mux4SboxItem
public bool? Padding { get; set; }
}
[Serializable]
public class HysteriaItem
{
public int UpMbps { get; set; }
@@ -206,7 +189,6 @@ public class HysteriaItem
public int HopInterval { get; set; } = Global.Hysteria2DefaultHopInt;
}
[Serializable]
public class ClashUIItem
{
public bool EnableIPv6 { get; set; }
@@ -219,7 +201,6 @@ public class ClashUIItem
public List<ColumnItem> ConnectionsColumnItem { get; set; }
}
[Serializable]
public class SystemProxyItem
{
public ESysProxyType SysProxyType { get; set; }
@@ -230,7 +211,6 @@ public class SystemProxyItem
public string? CustomSystemProxyScriptPath { get; set; }
}
[Serializable]
public class WebDavItem
{
public string? Url { get; set; }
@@ -239,15 +219,13 @@ public class WebDavItem
public string? DirName { get; set; }
}
[Serializable]
public class CheckUpdateItem
{
public bool CheckPreReleaseUpdate { get; set; }
public bool UpdateViaProxy { get; set; } = true;
public List<string>? SelectedCoreTypes { get; set; }
public List<string>? CheckPreReleaseCoreTypes { get; set; }
}
[Serializable]
public class Fragment4RayItem
{
public string? Packets { get; set; }
@@ -262,7 +240,6 @@ public class Fragment4RayItem
// migration end
}
[Serializable]
public class WindowSizeItem
{
public string TypeName { get; set; }
@@ -270,7 +247,6 @@ public class WindowSizeItem
public int Height { get; set; }
}
[Serializable]
public class SimpleDNSItem
{
public bool? UseSystemHosts { get; set; }
@@ -293,7 +269,6 @@ public class SimpleDNSItem
public bool? EnableHappyEyeballs { get; set; }
}
[Serializable]
public class HappyEyeballs4RayItem
{
public int? TryDelayMs { get; set; }
@@ -24,6 +24,7 @@ public record CoreConfigContext
public bool IsWindows { get; init; }
public bool IsMacOS { get; init; }
public bool IsLinux { get; init; }
// Defaults to true so that a context built without this flag keeps routing IPv6 into the
// tunnel; only a positive detection of the host having no global IPv6 address turns it off.
@@ -1,6 +1,5 @@
namespace ServiceLib.Models.CoreConfigs;
[Serializable]
public class CoreInfo
{
public ECoreType CoreType { get; set; }
@@ -44,7 +44,6 @@ public class Route4Sbox
public string? final { get; set; }
}
[Serializable]
public class Rule4Sbox
{
public string? outbound { get; set; }
@@ -103,7 +102,6 @@ public class Rule4Sbox
public bool? speculative { get; set; }
}
[Serializable]
public class Inbound4Sbox
{
public string type { get; set; }
@@ -128,8 +126,6 @@ public class User4Sbox
public class Outbound4Sbox : BaseServer4Sbox
{
public string? server { get; set; }
public int? server_port { get; set; }
public List<string>? server_ports { get; set; }
public string? uuid { get; set; }
public string? security { get; set; }
@@ -171,6 +167,10 @@ public class Endpoints4Sbox : BaseServer4Sbox
public string? udp_timeout { get; set; }
public int? workers { get; set; }
public List<Peer4Sbox> peers { get; set; }
public string? username { get; set; }
public string? password { get; set; }
public string? path { get; set; }
public Dictionary<string, List<string>>? headers { get; set; }
}
public class Peer4Sbox
@@ -230,7 +230,7 @@ public class Reality4Sbox
public class Transport4Sbox
{
public string? type { get; set; }
public object? host { get; set; }
public JsonNode? host { get; set; }
public string? path { get; set; }
public Headers4Sbox? headers { get; set; }
@@ -271,10 +271,8 @@ public class Server4Sbox : BaseServer4Sbox
public string? inet4_range { get; set; }
public string? inet6_range { get; set; }
public string? client_subnet { get; set; }
public string? server { get; set; }
public new string? domain_resolver { get; set; }
[JsonPropertyName("interface")] public string? Interface { get; set; }
public int? server_port { get; set; }
public string? path { get; set; }
public Headers4Sbox? headers { get; set; }
@@ -359,4 +357,6 @@ public abstract class BaseServer4Sbox : DialFields4Sbox
{
public string type { get; set; }
public string tag { get; set; }
public string? server { get; set; }
public int? server_port { get; set; }
}
@@ -3,7 +3,7 @@ namespace ServiceLib.Models.CoreConfigs;
public class V2rayConfig
{
public Log4Ray log { get; set; }
public object dns { get; set; }
public JsonObject? dns { get; set; }
public FakeDns4Ray? fakedns { get; set; }
public List<Inbounds4Ray> inbounds { get; set; }
public List<Outbounds4Ray> outbounds { get; set; }
@@ -92,6 +92,10 @@ public class Inboundsettings4Ray
public string? autoOutboundsInterface { get; set; }
public List<string>? dns { get; set; }
public bool? autoSystemDnsToGateway { get; set; }
public List<string>? autoSystemWfpBlockLeak { get; set; }
}
public class Sniffing4Ray
@@ -126,7 +130,7 @@ public class Outboundsettings4Ray
public string? secretKey { get; set; }
public object? address { get; set; }
public JsonNode? address { get; set; }
public int? port { get; set; }
@@ -138,7 +142,7 @@ public class Outboundsettings4Ray
public string? email { get; set; }
public object? headers { get; set; }
public JsonNode? headers { get; set; }
public List<WireguardPeer4Ray>? peers { get; set; }
@@ -195,8 +199,8 @@ public class Response4Ray
public class Dns4Ray
{
public Dictionary<string, object>? hosts { get; set; }
public List<object> servers { get; set; }
public Dictionary<string, JsonNode>? hosts { get; set; }
public List<JsonNode> servers { get; set; }
public bool? serveStale { get; set; }
public bool? enableParallelQuery { get; set; }
public string? queryStrategy { get; set; }
@@ -210,6 +214,7 @@ public class DnsServer4Ray
public List<string>? domains { get; set; }
public bool? skipFallback { get; set; }
public List<string>? expectedIPs { get; set; }
public bool? finalQuery { get; set; }
public string? tag { get; set; }
}
@@ -222,7 +227,6 @@ public class Routing4Ray
public List<BalancersItem4Ray>? balancers { get; set; }
}
[Serializable]
public class RulesItem4Ray
{
public string? type { get; set; }
@@ -315,8 +319,9 @@ public class StreamSettings4Ray
public GrpcSettings4Ray? grpcSettings { get; set; }
public HysteriaSettings4Ray? hysteriaSettings { get; set; }
public MasqueSettings4Ray? masqueSettings { get; set; }
public object? finalmask { get; set; }
public JsonObject? finalmask { get; set; }
public Sockopt4Ray? sockopt { get; set; }
}
@@ -339,7 +344,6 @@ public class TlsSettings4Ray
public string? pinnedPeerCertSha256 { get; set; }
public bool? disableSystemRoot { get; set; }
public string? echConfigList { get; set; }
public string? echForceQuery { get; set; }
public Sockopt4Ray? echSockopt { get; set; }
}
@@ -358,9 +362,9 @@ public class Header4Ray
{
public string type { get; set; }
public object request { get; set; }
public JsonObject request { get; set; }
public object response { get; set; }
public JsonObject response { get; set; }
}
public class KcpSettings4Ray
@@ -406,7 +410,7 @@ public class XhttpSettings4Ray
public string? path { get; set; }
public string? host { get; set; }
public string? mode { get; set; }
public object? extra { get; set; }
public JsonObject? extra { get; set; }
}
public class HttpSettings4Ray
@@ -443,6 +447,20 @@ public class HysteriaSettings4Ray
public string? auth { get; set; }
}
public class MasqueSettings4Ray
{
public string? path { get; set; }
public Dictionary<string, string>? headers { get; set; }
public MasqueWarpSettings4Ray? warp { get; set; }
}
public class MasqueWarpSettings4Ray
{
public string? privateKey { get; set; }
public string? publicKey { get; set; }
public List<string>? address { get; set; }
}
public class UdpHop4Ray
{
public string? ports { get; set; }
@@ -3,6 +3,8 @@ namespace ServiceLib.Models.Dto;
public partial class CheckUpdateModel : ReactiveObject
{
public bool? IsSelected { get; set; }
public bool IsCheckPreRelease { get; set; }
public bool ShowCheckPreRelease { get; set; }
public ECoreType? CoreType { get; set; }
[Reactive] public partial string? Remarks { get; set; }
public string? FileName { get; set; }
@@ -1,6 +1,5 @@
namespace ServiceLib.Models.Dto;
[Serializable]
public partial class ClashProxyModel : ReactiveObject
{
public string Name { get; set; }
@@ -1,6 +1,5 @@
namespace ServiceLib.Models.Dto;
[Serializable]
public partial class ProfileItemModel : ReactiveObject
{
public bool IsActive { get; set; }
@@ -1,6 +1,5 @@
namespace ServiceLib.Models.Dto;
[Serializable]
public class RoutingItemModel : RoutingItem
{
}
@@ -1,6 +1,5 @@
namespace ServiceLib.Models.Dto;
[Serializable]
public class RoutingTemplate
{
public string Version { get; set; }
@@ -1,6 +1,5 @@
namespace ServiceLib.Models.Dto;
[Serializable]
public class RulesItemModel : RulesItem
{
public string InboundTags { get; set; }
@@ -1,6 +1,5 @@
namespace ServiceLib.Models.Dto;
[Serializable]
public class ServerSpeedItem : ServerStatItem
{
public long ProxyUp { get; set; }
@@ -12,7 +11,6 @@ public class ServerSpeedItem : ServerStatItem
public long DirectDown { get; set; }
}
[Serializable]
public class TrafficItem
{
public ulong Up { get; set; }
@@ -1,6 +1,5 @@
namespace ServiceLib.Models.Dto;
[Serializable]
public class ServerTestItem
{
public string? IndexId { get; set; }
@@ -1,6 +1,5 @@
namespace ServiceLib.Models.Dto;
[Serializable]
public class SpeedTestResult
{
public string? IndexId { get; set; }
-1
View File
@@ -5,7 +5,6 @@ public class SsSIP008
public List<SsServer>? servers { get; set; }
}
[Serializable]
public class SsServer
{
public string? remarks { get; set; }
@@ -3,7 +3,6 @@ namespace ServiceLib.Models.Dto;
/// <summary>
/// https://github.com/2dust/v2rayN/wiki/
/// </summary>
[Serializable]
public class VmessQRCode
{
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
@@ -1,6 +1,5 @@
namespace ServiceLib.Models.Entities;
[Serializable]
public class DNSItem
{
[PrimaryKey]
@@ -1,6 +1,5 @@
namespace ServiceLib.Models.Entities;
[Serializable]
public class FullConfigTemplateItem
{
[PrimaryKey]
@@ -1,6 +1,5 @@
namespace ServiceLib.Models.Entities;
[Serializable]
public class ProfileExItem
{
[PrimaryKey]
@@ -1,7 +1,6 @@
namespace ServiceLib.Models.Entities;
[Obsolete("Use ProtocolExtraItem instead.")]
[Serializable]
public class ProfileGroupItem
{
[PrimaryKey]
@@ -1,6 +1,5 @@
namespace ServiceLib.Models.Entities;
[Serializable]
public class ProfileItem
{
private ProtocolExtraItem? _protocolExtraCache;
@@ -46,6 +46,13 @@ public record ProtocolExtraItem
public int? InsecureConcurrency { get; init; }
public bool? NaiveQuic { get; init; }
// MASQUE
public string? MasquePath { get; init; }
public string? MasqueHeaders { get; init; }
public string? WarpPublicKey { get; init; }
public string? WarpPrivateKey { get; init; }
public string? WarpAddress { get; init; }
// group profile
public string? GroupType { get; init; }
public string? ChildItems { get; init; }
@@ -1,6 +1,5 @@
namespace ServiceLib.Models.Entities;
[Serializable]
public class RoutingItem
{
[PrimaryKey]
@@ -1,6 +1,5 @@
namespace ServiceLib.Models.Entities;
[Serializable]
public class RulesItem
{
public string Id { get; set; }
@@ -1,6 +1,5 @@
namespace ServiceLib.Models.Entities;
[Serializable]
public class ServerStatItem
{
[PrimaryKey]
+2 -1
View File
@@ -1,6 +1,5 @@
namespace ServiceLib.Models.Entities;
[Serializable]
public class SubItem
{
[PrimaryKey]
@@ -37,4 +36,6 @@ public class SubItem
public string? Memo { get; set; }
public ECoreType? CustomCoreType { get; set; }
public string? PreResolver { get; set; }
}
+45
View File
@@ -528,6 +528,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Pre-resolve server domains 的本地化字符串。
/// </summary>
public static string LvPreResolve {
get {
return ResourceManager.GetString("LvPreResolve", resourceCulture);
}
}
/// <summary>
/// 查找类似 Previous proxy remarks 的本地化字符串。
/// </summary>
@@ -789,6 +798,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Add [MASQUE] 的本地化字符串。
/// </summary>
public static string menuAddMasqueServer {
get {
return ResourceManager.GetString("menuAddMasqueServer", resourceCulture);
}
}
/// <summary>
/// 查找类似 Add [NaïveProxy] 的本地化字符串。
/// </summary>
@@ -924,6 +942,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Check and Update 的本地化字符串。
/// </summary>
public static string menuCheckAndUpdate {
get {
return ResourceManager.GetString("menuCheckAndUpdate", resourceCulture);
}
}
/// <summary>
/// 查找类似 Only Check 的本地化字符串。
/// </summary>
@@ -960,6 +987,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Columns 的本地化字符串。
/// </summary>
public static string menuColumns {
get {
return ResourceManager.GetString("menuColumns", resourceCulture);
}
}
/// <summary>
/// 查找类似 Close Connection 的本地化字符串。
/// </summary>
@@ -5139,6 +5175,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Warp 的本地化字符串。
/// </summary>
public static string TbWarp {
get {
return ResourceManager.GetString("TbWarp", resourceCulture);
}
}
/// <summary>
/// 查找类似 Xray Only 的本地化字符串。
/// </summary>
+16 -1
View File
@@ -399,6 +399,9 @@
<data name="MsgServerTitle" xml:space="preserve">
<value>Filter, press Enter to execute</value>
</data>
<data name="menuCheckAndUpdate" xml:space="preserve">
<value>Check and Update</value>
</data>
<data name="menuCheckUpdate" xml:space="preserve">
<value>Check Update</value>
</data>
@@ -1899,4 +1902,16 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbSortingProcess" xml:space="preserve">
<value>Process</value>
</data>
</root>
<data name="menuAddMasqueServer" xml:space="preserve">
<value>Add [MASQUE]</value>
</data>
<data name="menuColumns" xml:space="preserve">
<value>Columns</value>
</data>
<data name="TbWarp" xml:space="preserve">
<value>Warp</value>
</data>
<data name="LvPreResolve" xml:space="preserve">
<value>Pre-resolve server domains</value>
</data>
</root>
+8 -2
View File
@@ -399,8 +399,11 @@
<data name="MsgServerTitle" xml:space="preserve">
<value>Фильтр, нажмите Enter для выполнения</value>
</data>
<data name="menuCheckAndUpdate" xml:space="preserve">
<value>Проверить и обновить</value>
</data>
<data name="menuCheckUpdate" xml:space="preserve">
<value>Обновить</value>
<value>Проверить обновления</value>
</data>
<data name="menuClose" xml:space="preserve">
<value>Закрыть</value>
@@ -1780,7 +1783,7 @@
<value>Доступна новая версия {0}: {1}</value>
</data>
<data name="menuCheckOnly" xml:space="preserve">
<value>Проверить</value>
<value>Только проверить</value>
</data>
<data name="MsgNotSupport" xml:space="preserve">
<value>Не поддерживается</value>
@@ -1899,4 +1902,7 @@
<data name="TbSortingProcess" xml:space="preserve">
<value>Процесс</value>
</data>
<data name="menuAddMasqueServer" xml:space="preserve">
<value>Добавить сервер [MASQUE]</value>
</data>
</root>
+16 -1
View File
@@ -399,6 +399,9 @@
<data name="MsgServerTitle" xml:space="preserve">
<value>过滤器,按回车执行</value>
</data>
<data name="menuCheckAndUpdate" xml:space="preserve">
<value>检查并更新</value>
</data>
<data name="menuCheckUpdate" xml:space="preserve">
<value>检查更新</value>
</data>
@@ -1891,4 +1894,16 @@
<data name="TbSortingProcess" xml:space="preserve">
<value>进程</value>
</data>
</root>
<data name="menuAddMasqueServer" xml:space="preserve">
<value>添加 [MASQUE]</value>
</data>
<data name="menuColumns" xml:space="preserve">
<value>列</value>
</data>
<data name="TbWarp" xml:space="preserve">
<value>Warp</value>
</data>
<data name="LvPreResolve" xml:space="preserve">
<value>预解析节点域名</value>
</data>
</root>
@@ -399,6 +399,9 @@
<data name="MsgServerTitle" xml:space="preserve">
<value>過濾器,按 Enter 執行</value>
</data>
<data name="menuCheckAndUpdate" xml:space="preserve">
<value>檢查並更新</value>
</data>
<data name="menuCheckUpdate" xml:space="preserve">
<value>檢查更新</value>
</data>
+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<RegistrationInfo>
<Description>{{DESCRIPTION}}</Description>
</RegistrationInfo>
<Triggers>
<LogonTrigger>
<Enabled>true</Enabled>
</LogonTrigger>
</Triggers>
<Principals>
<Principal id="Author">
<LogonType>InteractiveToken</LogonType>
<RunLevel>HighestAvailable</RunLevel>
</Principal>
</Principals>
<Actions Context="Author">
<Exec>
<Command>{{EXEC_PATH}}</Command>
<Arguments>{{ARGUMENTS}}</Arguments>
<WorkingDirectory>{{WORKING_DIRECTORY}}</WorkingDirectory>
</Exec>
</Actions>
</Task>
-254
View File
@@ -171,70 +171,6 @@ ivAwhZTJryQCL2/W3Wf+47BVTwSYT6RBVuKT0Gro1vP7ZeDOdcQxWQzugsgMYDNK
GbqEZycPvEJdvSRUDewdcAZfpLz6IHxV
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd
MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg
Q2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow
TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw
HgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB
BQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr
6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV
L4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91
1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx
MlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ
QmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB
arcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr
Us3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi
FRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS
P/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN
9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP
AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz
uvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h
9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s
A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t
OluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo
+fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7
KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2
DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us
H8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ
I+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7
5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h
3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz
Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd
MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg
Q2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow
TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw
HgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB
BQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRHsJ8Y
ZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3E
N3coTRiR5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9
tznDDgFHmV0ST9tD+leh7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX
0DJq1l1sDPGzbjniazEuOQAnFN44wOwZZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c
/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH2xc519woe2v1n/MuwU8X
KhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV/afmiSTY
zIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvS
O1UQRwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D
34xFMFbG02SrZvPAXpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgP
K9Dx2hzLabjKSWJtyNBjYt1gD1iqj6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3
AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEe4zf/lb+74suwv
Tg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAACAj
QTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV
cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXS
IGrs/CIBKM+GuIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2
HJLw5QY33KbmkJs4j1xrG0aGQ0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsa
O5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8ZORK15FTAaggiG6cX0S5y2CBNOxv
033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2KSb12tjE8nVhz36u
dmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz6MkE
kbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41
3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD
u79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq
4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNV
BAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04g
@@ -300,40 +236,6 @@ AAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ
5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBe
MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0
ZC4xKjAoBgNVBAsMIWVQS0kgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe
Fw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMxMjdaMF4xCzAJBgNVBAYTAlRXMSMw
IQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEqMCgGA1UECwwhZVBL
SSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEF
AAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAH
SyZbCUNsIZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAh
ijHyl3SJCRImHJ7K2RKilTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3X
DZoTM1PRYfl61dd4s5oz9wCGzh1NlDivqOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1
TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX12ruOzjjK9SXDrkb5wdJ
fzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0OWQqraffA
sgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uU
WH1+ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLS
nT0IFaUQAS2zMnaolQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pH
dmX2Os+PYhcZewoozRrSgx4hxyy/vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJip
NiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXiZo1jDiVN1Rmy5nk3pyKdVDEC
AwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/QkqiMAwGA1UdEwQF
MAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH
ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGB
uvl2ICO1J2B01GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6Yl
PwZpVnPDimZI+ymBV3QGypzqKOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkP
JXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdVxrsStZf0X4OFunHB2WyBEXYKCrC/
gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEPNXubrjlpC2JgQCA2
j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+rGNm6
5ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUB
o2M3IUxExJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS
/jQ6fbjpKdx2qcgw+BRxgMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2z
Gp1iro2C6pSe3VkQw63d4k3jMdXH7OjysP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTE
W9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmODBCEIZ43ygknQW/2xzQ+D
hNQ+IIX3Sj0rnP0qCglN6oH4EZw=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF
MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD
@@ -434,43 +336,6 @@ e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p
TpPDpFQUWw==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAw
WjELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAw
MiA0ODE0NjMwODEwMDAzNjEZMBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0x
MzEwMDEwODMyMjdaFw0zMzEwMDEwODMyMjdaMFoxCzAJBgNVBAYTAkZSMRIwEAYD
VQQKDAlEaGlteW90aXMxHDAaBgNVBAsMEzAwMDIgNDgxNDYzMDgxMDAwMzYxGTAX
BgNVBAMMEENlcnRpZ25hIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw
ggIKAoICAQDNGDllGlmx6mQWDoyUJJV8g9PFOSbcDO8WV43X2KyjQn+Cyu3NW9sO
ty3tRQgXstmzy9YXUnIo245Onoq2C/mehJpNdt4iKVzSs9IGPjA5qXSjklYcoW9M
CiBtnyN6tMbaLOQdLNyzKNAT8kxOAkmhVECe5uUFoC2EyP+YbNDrihqECB63aCPu
I9Vwzm1RaRDuoXrC0SIxwoKF0vJVdlB8JXrJhFwLrN1CTivngqIkicuQstDuI7pm
TLtipPlTWmR7fJj6o0ieD5Wupxj0auwuA0Wv8HT4Ks16XdG+RCYyKfHx9WzMfgIh
C59vpD++nVPiz32pLHxYGpfhPTc3GGYo0kDFUYqMwy3OU4gkWGQwFsWq4NYKpkDf
ePb1BHxpE4S80dGnBs8B92jAqFe7OmGtBIyT46388NtEbVncSVmurJqZNjBBe3Yz
IoejwpKGbvlw7q6Hh5UbxHq9MfPU0uWZ/75I7HX1eBYdpnDBfzwboZL7z8g81sWT
Co/1VTp2lc5ZmIoJlXcymoO6LAQ6l73UL77XbJuiyn1tJslV1c/DeVIICZkHJC1k
JWumIWmbat10TWuXekG9qxf5kBdIjzb5LdXF2+6qhUVB+s06RbFo5jZMm5BX7CO5
hwjCxAnxl4YqKE3idMDaxIzb3+KhF1nOJFl0Mdp//TBt2dzhauH8XwIDAQABo4IB
GjCCARYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE
FBiHVuBud+4kNTxOc5of1uHieX4rMB8GA1UdIwQYMBaAFBiHVuBud+4kNTxOc5of
1uHieX4rMEQGA1UdIAQ9MDswOQYEVR0gADAxMC8GCCsGAQUFBwIBFiNodHRwczov
L3d3d3cuY2VydGlnbmEuZnIvYXV0b3JpdGVzLzBtBgNVHR8EZjBkMC+gLaArhilo
dHRwOi8vY3JsLmNlcnRpZ25hLmZyL2NlcnRpZ25hcm9vdGNhLmNybDAxoC+gLYYr
aHR0cDovL2NybC5kaGlteW90aXMuY29tL2NlcnRpZ25hcm9vdGNhLmNybDANBgkq
hkiG9w0BAQsFAAOCAgEAlLieT/DjlQgi581oQfccVdV8AOItOoldaDgvUSILSo3L
6btdPrtcPbEo/uRTVRPPoZAbAh1fZkYJMyjhDSSXcNMQH+pkV5a7XdrnxIxPTGRG
HVyH41neQtGbqH6mid2PHMkwgu07nM3A6RngatgCdTer9zQoKJHyBApPNeNgJgH6
0BGM+RFq7q89w1DTj18zeTyGqHNFkIwgtnJzFyO+B2XleJINugHA64wcZr+shncB
lA2c5uk5jR+mUYyZDDl34bSb+hxnV29qao6pK0xXeXpXIs/NX2NGjVxZOob4Mkdi
o2cNGJHc+6Zr9UhhcyNZjgKnvETq9Emd8VRY+WCv2hikLyhF3HqgiIZd8zvn/yk1
gPxkQ5Tm4xxvvq0OKmOZK8l+hfZx6AYDlf7ej0gcWtSS6Cvu5zHbugRqh5jnxV/v
faci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaYtlu3zM63
Nwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayh
jWZSaX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw
3kAP+HwV96LOPNdeE4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw
CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu
@@ -565,22 +430,6 @@ RQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx
iN66zB+Afko=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQG
EwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNo
bm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g
RzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJ
TjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9s
b2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMw
djAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0
WXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xyS
fvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB
zhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggq
hkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWB
CUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD
+JbNR6iC8hZVdyR+EhCVBCyj
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UE
BhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ
@@ -732,20 +581,6 @@ LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI
4uJEvlz36hz1
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIICCTCCAY6gAwIBAgINAgPluILrIPglJ209ZjAKBggqhkjOPQQDAzBHMQswCQYD
VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG
A1UEAxMLR1RTIFJvb3QgUjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw
WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz
IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjOPQIBBgUrgQQAIgNi
AAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout736G
jOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL2
4CejQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW
BBTB8Sa6oC2uhYHP0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEA9uEglRR7
VKOQFhG/hMjqb2sXnh5GmCCbn9MN2azTL818+FsuVbu/3ZL3pAzcMeGiAjEA/Jdm
ZuVDFhOD3cffL74UOO0BzrEXGhF16b0DjyZ+hOXJYKaV11RZt+cRLInUue4X
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw
CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU
@@ -805,38 +640,6 @@ HQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVLB7rUW44kB/
bmF0774BxL4YSFlhgjICICadVGNA3jdgUM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFVzCCAz+gAwIBAgINAgPlrsWNBCUaqxElqjANBgkqhkiG9w0BAQwFADBHMQsw
CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU
MBIGA1UEAxMLR1RTIFJvb3QgUjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw
MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp
Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwggIiMA0GCSqGSIb3DQEBAQUA
A4ICDwAwggIKAoICAQDO3v2m++zsFDQ8BwZabFn3GTXd98GdVarTzTukk3LvCvpt
nfbwhYBboUhSnznFt+4orO/LdmgUud+tAWyZH8QiHZ/+cnfgLFuv5AS/T3KgGjSY
6Dlo7JUle3ah5mm5hRm9iYz+re026nO8/4Piy33B0s5Ks40FnotJk9/BW9BuXvAu
MC6C/Pq8tBcKSOWIm8Wba96wyrQD8Nr0kLhlZPdcTK3ofmZemde4wj7I0BOdre7k
RXuJVfeKH2JShBKzwkCX44ofR5GmdFrS+LFjKBC4swm4VndAoiaYecb+3yXuPuWg
f9RhD1FLPD+M2uFwdNjCaKH5wQzpoeJ/u1U8dgbuak7MkogwTZq9TwtImoS1mKPV
+3PBV2HdKFZ1E66HjucMUQkQdYhMvI35ezzUIkgfKtzra7tEscszcTJGr61K8Yzo
dDqs5xoic4DSMPclQsciOzsSrZYuxsN2B6ogtzVJV+mSSeh2FnIxZyuWfoqjx5RW
Ir9qS34BIbIjMt/kmkRtWVtd9QCgHJvGeJeNkP+byKq0rxFROV7Z+2et1VsRnTKa
G73VululycslaVNVJ1zgyjbLiGH7HrfQy+4W+9OmTN6SpdTi3/UGVN4unUu0kzCq
gc7dGtxRcw1PcOnlthYhGXmy5okLdWTK1au8CcEYof/UVKGFPP0UJAOyh9OktwID
AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E
FgQUu//KjiOfT5nK2+JopqUVJxce2Q4wDQYJKoZIhvcNAQEMBQADggIBAB/Kzt3H
vqGf2SdMC9wXmBFqiN495nFWcrKeGk6c1SuYJF2ba3uwM4IJvd8lRuqYnrYb/oM8
0mJhwQTtzuDFycgTE1XnqGOtjHsB/ncw4c5omwX4Eu55MaBBRTUoCnGkJE+M3DyC
B19m3H0Q/gxhswWV7uGugQ+o+MePTagjAiZrHYNSVc61LwDKgEDg4XSsYPWHgJ2u
NmSRXbBoGOqKYcl3qJfEycel/FVL8/B/uWU9J2jQzGv6U53hkRrJXRqWbTKH7QMg
yALOWr7Z6v2yTcQvG99fevX4i8buMTolUVVnjWQye+mew4K6Ki3pHrTgSAai/Gev
HyICc/sgCq+dVEuhzf9gR7A/Xe8bVr2XIZYtCtFenTgCR2y59PYjJbigapordwj6
xLEokCZYCDzifqrXPW+6MYgKBesntaFJ7qBFVHvmJ2WZICGoo7z7GJa7Um8M7YNR
TOlZ4iBgxcJlkoKM8xAfDoqXvneCbT+PHV28SSe9zE8P4c52hgQjxcCMElv924Sg
JPFI/2R80L5cFtHvma3AH/vLrrw4IgYmZNralw4/KBVEqE8AyvCazM90arQ+POuV
7LXTWtiBmelDGDfrs7vRWGJB82bSj6p4lVQgw1oudCvV0b4YacCs1aTPObpRhANl
6WLAYv7YTVWW4tAR+kg0Eeye7QUd5MjWHYbL
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQEL
BQAwbzELMAkGA1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJ
@@ -1189,31 +992,6 @@ AHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D5KbvtwEwXlGjefVwaaZB
RA+GsCyRxj3qrg+E
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG
EwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3
MDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl
cnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR
dGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB
pzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM
b2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm
aWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz
IEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT
lF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz
AZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5
VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG
ILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2
BJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG
AQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M
U9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh
bvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C
+C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC
bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F
uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2
XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBt
MQswCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUg
@@ -1827,38 +1605,6 @@ xw/ogM4cKGR0GQjTQuPOAF1/sdwTsOEFy9EgqoZ0njnnkf3/W9b3raYvAwtt41dU
63ZTGI0RmLo=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFajCCA1KgAwIBAgIQLd2szmKXlKFD6LDNdmpeYDANBgkqhkiG9w0BAQsFADBP
MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0
ZC4xGzAZBgNVBAMMEkhpUEtJIFJvb3QgQ0EgLSBHMTAeFw0xOTAyMjIwOTQ2MDRa
Fw0zNzEyMzExNTU5NTlaME8xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3
YSBUZWxlY29tIENvLiwgTHRkLjEbMBkGA1UEAwwSSGlQS0kgUm9vdCBDQSAtIEcx
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9B5/UnMyDHPkvRN0o9Qw
qNCuS9i233VHZvR85zkEHmpwINJaR3JnVfSl6J3VHiGh8Ge6zCFovkRTv4354twv
Vcg3Px+kwJyz5HdcoEb+d/oaoDjq7Zpy3iu9lFc6uux55199QmQ5eiY29yTw1S+6
lZgRZq2XNdZ1AYDgr/SEYYwNHl98h5ZeQa/rh+r4XfEuiAU+TCK72h8q3VJGZDnz
Qs7ZngyzsHeXZJzA9KMuH5UHsBffMNsAGJZMoYFL3QRtU6M9/Aes1MU3guvklQgZ
KILSQjqj2FPseYlgSGDIcpJQ3AOPgz+yQlda22rpEZfdhSi8MEyr48KxRURHH+CK
FgeW0iEPU8DtqX7UTuybCeyvQqww1r/REEXgphaypcXTT3OUM3ECoWqj1jOXTyFj
HluP2cFeRXF3D4FdXyGarYPM+l7WjSNfGz1BryB1ZlpK9p/7qxj3ccC2HTHsOyDr
y+K49a6SsvfhhEvyovKTmiKe0xRvNlS9H15ZFblzqMF8b3ti6RZsR1pl8w4Rm0bZ
/W3c1pzAtH2lsN0/Vm+h+fbkEkj9Bn8SV7apI09bA8PgcSojt/ewsTu8mL3WmKgM
a/aOEmem8rJY5AIJEzypuxC00jBF8ez3ABHfZfjcK0NVvxaXxA/VLGGEqnKG/uY6
fsI/fe78LxQ+5oXdUG+3Se0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNV
HQ4EFgQU8ncX+l6o/vY9cdVouslGDDjYr7AwDgYDVR0PAQH/BAQDAgGGMA0GCSqG
SIb3DQEBCwUAA4ICAQBQUfB13HAE4/+qddRxosuej6ip0691x1TPOhwEmSKsxBHi
7zNKpiMdDg1H2DfHb680f0+BazVP6XKlMeJ45/dOlBhbQH3PayFUhuaVevvGyuqc
SE5XCV0vrPSltJczWNWseanMX/mF+lLFjfiRFOs6DRfQUsJ748JzjkZ4Bjgs6Fza
ZsT0pPBWGTMpWmWSBUdGSquEwx4noR8RkpkndZMPvDY7l1ePJlsMu5wP1G4wB9Tc
XzZoZjmDlicmisjEOf6aIW/Vcobpf2Lll07QJNBAsNB1CI69aO4I1258EHBGG3zg
iLKecoaZAeO/n0kZtCW+VmWuF2PlHt/o/0elv+EmBYTksMCv5wiZqAxeJoBF1Pho
L5aPruJKHJwWDBNvOIf2u8g0X5IDUXlwpt/L9ZlNec1OvFefQ05rLisY+GpzjLrF
Ne85akEez3GoorKGB1s6yeHvP2UEgEcyRHCVTjFnanRbEEV16rCf0OY1/k6fi8wr
kkVbbiVghUbN0aqwdmaTd5a+g744tiROJgvM7XpWGuDpWsZkrUx6AEhEL7lAuxM+
vhV4nYWBSipX3tUZQ9rbyltHhoMLP7YNdnhzeSJesYAfz77RP1YQmCuVh6EfnWQU
YDksswBVLuT1sw5XxJFBAJw/6KXf6vb/yPCtbVKoF6ubYfwSUTXkJf2vqmqGOQ==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw
CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg
@@ -104,59 +104,6 @@ RQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx
iN66zB+Afko=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQG
EwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNo
bm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g
RzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJ
TjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9s
b2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMw
djAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0
WXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xyS
fvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB
zhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggq
hkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWB
CUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD
+JbNR6iC8hZVdyR+EhCVBCyj
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDczCCAlugAwIBAgILAK7PALrEzzL4Q7IwDQYJKoZIhvcNAQELBQAwVjELMAkG
A1UEBhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEg
SW5jMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEMxMB4XDTE4MDIxODE4MzAw
MFoXDTQzMDIxODE4MzAwMFowVjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln
biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQDExNlbVNpZ24gUm9v
dCBDQSAtIEMxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+upufGZ
BczYKCFK83M0UYRWEPWgTywS4/oTmifQz/l5GnRfHXk5/Fv4cI7gklL35CX5VIPZ
HdPIWoU/Xse2B+4+wM6ar6xWQio5JXDWv7V7Nq2s9nPczdcdioOl+yuQFTdrHCZH
3DspVpNqs8FqOp099cGXOFgFixwR4+S0uF2FHYP+eF8LRWgYSKVGczQ7/g/IdrvH
GPMF0Ybzhe3nudkyrVWIzqa2kbBPrH4VI5b2P/AgNBbeCsbEBEV5f6f9vtKppa+c
xSMq9zwhbL2vj07FOrLzNBL834AaSaTUqZX3noleoomslMuoaJuvimUnzYnu3Yy1
aylwQ6BpC+S5DwIDAQABo0IwQDAdBgNVHQ4EFgQU/qHgcB4qAzlSWkK+XJGFehiq
TbUwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL
BQADggEBAMJKVvoVIXsoounlHfv4LcQ5lkFMOycsxGwYFYDGrK9HWS8mC+M2sO87
/kOXSTKZEhVb3xEp/6tT+LvBeA+snFOvV71ojD1pM/CjoCNjO2RnIkSt1XHLVip4
kqNPEjE2NuLe/gDEo2APJ62gsIq1NnpSob0n9CAnYuhNlCQT5AoE6TyrLshDCUrG
YQTlSTR+08TI9Q/Aqum6VF7zYytPT1DU/rl7mYw9wC68AivTxEDkigcxHpvOJpkT
+xHqmiIMERnHXhuBUDDIlhJu58tBf5E7oke3VIAb3ADMmpDqw8NQBmIMMMAVSKeo
WXzhriKi4gp6D/piq1JM4fHfyr6DDUI=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIICKzCCAbGgAwIBAgIKe3G2gla4EnycqDAKBggqhkjOPQQDAzBaMQswCQYDVQQG
EwJVUzETMBEGA1UECxMKZW1TaWduIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJbmMx
IDAeBgNVBAMTF2VtU2lnbiBFQ0MgUm9vdCBDQSAtIEMzMB4XDTE4MDIxODE4MzAw
MFoXDTQzMDIxODE4MzAwMFowWjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln
biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMSAwHgYDVQQDExdlbVNpZ24gRUND
IFJvb3QgQ0EgLSBDMzB2MBAGByqGSM49AgEGBSuBBAAiA2IABP2lYa57JhAd6bci
MK4G9IGzsUJxlTm801Ljr6/58pc1kjZGDoeVjbk5Wum739D+yAdBPLtVb4Ojavti
sIGJAnB9SMVK4+kiVCJNk7tCDK93nCOmfddhEc5lx/h//vXyqaNCMEAwHQYDVR0O
BBYEFPtaSNCAIEDyqOkAB2kZd6fmw/TPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB
Af8EBTADAQH/MAoGCCqGSM49BAMDA2gAMGUCMQC02C8Cif22TGK6Q04ThHK1rt0c
3ta13FaPWEBaLd4gTCKDypOofu4SQMfWh0/434UCMBwUZOR8loMRnLDRWmFLpg9J
0wD8ofzkpf9/rdcw0Md3f76BB1UwUCAU9Vc4CqgxUQ==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQEL
BQAwbzELMAkGA1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJ
+2 -6
View File
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
@@ -10,10 +10,6 @@
<PackageReference Include="ReactiveUI">
<TreatAsUsed>true</TreatAsUsed>
</PackageReference>
<PackageReference Include="ReactiveUI.SourceGenerators">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="sqlite-net-e" />
<PackageReference Include="Repobot.SQLite.Unofficial" />
<PackageReference Include="NLog" />
@@ -22,7 +18,6 @@
<PackageReference Include="QRCoder" />
<PackageReference Include="CliWrap" />
<PackageReference Include="ZXing.Net.Bindings.SkiaSharp" />
<PackageReference Include="TaskScheduler" />
</ItemGroup>
<ItemGroup>
@@ -49,6 +44,7 @@
<EmbeddedResource Include="Sample\SampleTunRules" />
<EmbeddedResource Include="Sample\SingboxSampleClientConfig" />
<EmbeddedResource Include="Sample\SingboxSampleOutbound" />
<EmbeddedResource Include="Sample\TaskXmlTemplate" />
<EmbeddedResource Include="Sample\tun_singbox_dns" />
<EmbeddedResource Include="Sample\tun_singbox_inbound" />
<EmbeddedResource Include="Sample\tun_singbox_rules" />
@@ -61,10 +61,10 @@ public class CoreConfigClashService(Config config, bool isTunEnabled)
//YAML anchors
if (txtFile.Contains("<<:") && txtFile.Contains('*') && txtFile.Contains('&'))
{
txtFile = YamlUtils.PreprocessYaml(txtFile);
txtFile = YamlUtils.PreprocessYaml(txtFile) ?? "";
}
var fileContent = YamlUtils.FromYaml<Dictionary<string, object>>(txtFile);
var fileContent = YamlUtils.FromYaml(txtFile);
if (fileContent == null)
{
ret.Msg = ResUI.FailedConversionConfiguration;
@@ -72,29 +72,38 @@ public class CoreConfigClashService(Config config, bool isTunEnabled)
}
//mixed-port
fileContent["mixed-port"] = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
//fileContent["mixed-port"] = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
fileContent.Children[new YamlScalarNode("mixed-port")] = new YamlScalarNode(AppManager.Instance.GetLocalPort(EInboundProtocol.socks).ToString());
//log-level
fileContent["log-level"] = GetLogLevel(config.CoreBasicItem.Loglevel);
//fileContent["log-level"] = GetLogLevel(config.CoreBasicItem.Loglevel);
fileContent.Children[new YamlScalarNode("log-level")] = new YamlScalarNode(GetLogLevel(config.CoreBasicItem.Loglevel));
//external-controller
fileContent["external-controller"] = $"{Global.Loopback}:{AppManager.Instance.StatePort2}";
fileContent.Remove("secret");
//fileContent["external-controller"] = $"{Global.Loopback}:{AppManager.Instance.StatePort2}";
fileContent.Children[new YamlScalarNode("external-controller")] = new YamlScalarNode($"{Global.Loopback}:{AppManager.Instance.StatePort2}");
//fileContent.Remove("secret");
fileContent.Children.Remove(new YamlScalarNode("secret"));
//allow-lan
if (config.Inbound.First().AllowLANConn)
{
fileContent["allow-lan"] = "true";
fileContent["bind-address"] = "*";
//fileContent["allow-lan"] = "true";
fileContent.Children[new YamlScalarNode("allow-lan")] = new YamlScalarNode("true");
//fileContent["bind-address"] = "*";
fileContent.Children[new YamlScalarNode("bind-address")] = new YamlScalarNode("*");
}
else
{
fileContent["allow-lan"] = "false";
//fileContent["allow-lan"] = "false";
fileContent.Children[new YamlScalarNode("allow-lan")] = new YamlScalarNode("false");
}
//ipv6
fileContent["ipv6"] = config.ClashUIItem.EnableIPv6;
//fileContent["ipv6"] = config.ClashUIItem.EnableIPv6;
fileContent.Children[new YamlScalarNode("ipv6")] = new YamlScalarNode(config.ClashUIItem.EnableIPv6.ToString().ToLowerInvariant());
//mode
fileContent.TryAdd("mode", nameof(ERuleMode.Rule));
//fileContent.TryAdd("mode", nameof(ERuleMode.Rule));
fileContent.Children.TryAdd(new YamlScalarNode("mode"), new YamlScalarNode(nameof(ERuleMode.Rule)));
//enable tun mode
if (isTunEnabled)
@@ -102,10 +111,11 @@ public class CoreConfigClashService(Config config, bool isTunEnabled)
var tun = EmbedUtils.GetEmbedText(Global.ClashTunYaml);
if (tun.IsNotEmpty())
{
var tunContent = YamlUtils.FromYaml<Dictionary<string, object>>(tun);
var tunContent = YamlUtils.FromYaml(tun);
if (tunContent != null)
{
fileContent["tun"] = tunContent["tun"];
//fileContent["tun"] = tunContent["tun"];
fileContent.Children[new YamlScalarNode("tun")] = tunContent.Children[new YamlScalarNode("tun")];
}
}
}
@@ -120,27 +130,27 @@ public class CoreConfigClashService(Config config, bool isTunEnabled)
Logging.SaveLog($"{_tag}-Mixin", ex);
}
// Mihomo parses plain values such as 815458e4 as floats, so quote REALITY short IDs.
var originalRealityShortIds = new List<(Dictionary<object, object> RealityOptions, string ShortId)>();
if (fileContent.GetValueOrDefault("proxies") is List<object> proxies)
{
foreach (var proxy in proxies.OfType<Dictionary<object, object>>())
{
if (proxy.GetValueOrDefault("reality-opts") is Dictionary<object, object> realityOptions
&& realityOptions.GetValueOrDefault("short-id") is string shortId
&& !shortId.StartsWith(tagYamlStr2, StringComparison.Ordinal))
{
originalRealityShortIds.Add((realityOptions, shortId));
realityOptions["short-id"] = new YamlScalarNode(shortId) { Style = ScalarStyle.DoubleQuoted };
}
}
}
//// Mihomo parses plain values such as 815458e4 as floats, so quote REALITY short IDs.
//var originalRealityShortIds = new List<(Dictionary<object, object> RealityOptions, string ShortId)>();
//if (fileContent.GetValueOrDefault("proxies") is List<object> proxies)
//{
// foreach (var proxy in proxies.OfType<Dictionary<object, object>>())
// {
// if (proxy.GetValueOrDefault("reality-opts") is Dictionary<object, object> realityOptions
// && realityOptions.GetValueOrDefault("short-id") is string shortId
// && !shortId.StartsWith(tagYamlStr2, StringComparison.Ordinal))
// {
// originalRealityShortIds.Add((realityOptions, shortId));
// realityOptions["short-id"] = new YamlScalarNode(shortId) { Style = ScalarStyle.DoubleQuoted };
// }
// }
//}
var txtFileNew = YamlUtils.ToYaml(fileContent).Replace(tagYamlStr2, tagYamlStr3);
foreach (var (realityOptions, shortId) in originalRealityShortIds)
{
realityOptions["short-id"] = shortId;
}
//foreach (var (realityOptions, shortId) in originalRealityShortIds)
//{
// realityOptions["short-id"] = shortId;
//}
await File.WriteAllTextAsync(fileName, txtFileNew);
//check again
if (!File.Exists(fileName))
@@ -161,7 +171,7 @@ public class CoreConfigClashService(Config config, bool isTunEnabled)
}
}
private async Task MixinContent(Dictionary<string, object> fileContent, ProfileItem node)
private async Task MixinContent(YamlMappingNode fileContent, ProfileItem node)
{
if (!config.ClashUIItem.EnableMixinContent)
{
@@ -177,80 +187,116 @@ public class CoreConfigClashService(Config config, bool isTunEnabled)
var txtFile = await File.ReadAllTextAsync(Utils.GetConfigPath(Global.ClashMixinConfigFileName));
var mixinContent = YamlUtils.FromYaml<Dictionary<string, object>>(txtFile);
var mixinContent = YamlUtils.FromYaml(txtFile);
if (mixinContent == null)
{
return;
}
foreach (var item in mixinContent)
foreach (var (keyNode, valueNode) in mixinContent.Children)
{
if (!isTunEnabled && item.Key == "tun")
if (keyNode is not YamlScalarNode scalarKey)
{
continue;
}
var key = scalarKey.Value ?? "";
if (!isTunEnabled && key == "tun")
{
continue;
}
if (item.Key.StartsWith("prepend-")
|| item.Key.StartsWith("append-")
|| item.Key.StartsWith("removed-"))
if (key.StartsWith("prepend-")
|| key.StartsWith("append-")
|| key.StartsWith("removed-"))
{
ModifyContentMerge(fileContent, item.Key, item.Value);
ModifyContentMerge(fileContent, key, valueNode);
}
else
{
fileContent[item.Key] = item.Value;
fileContent.Children[keyNode] = valueNode;
}
}
return;
}
private void ModifyContentMerge(Dictionary<string, object> fileContent, string key, object value)
private void ModifyContentMerge(YamlMappingNode fileContent, YamlNode keyNode, YamlNode valueNode)
{
if (keyNode is not YamlScalarNode { Value: string key })
{
return;
}
var blPrepend = false;
var blRemoved = false;
string realKey;
if (key.StartsWith("prepend-"))
{
blPrepend = true;
key = key.Replace("prepend-", "");
realKey = key["prepend-".Length..];
}
else if (key.StartsWith("append-"))
{
blPrepend = false;
key = key.Replace("append-", "");
realKey = key["append-".Length..];
}
else if (key.StartsWith("removed-"))
{
blRemoved = true;
key = key.Replace("removed-", "");
realKey = key["removed-".Length..];
}
else
{
return;
}
if (!blRemoved && fileContent.TryAdd(key, value))
var targetKeyNode = new YamlScalarNode(realKey);
if (!blRemoved && fileContent.Children.TryAdd(targetKeyNode, valueNode))
{
return;
}
if (!fileContent.Children.TryGetValue(targetKeyNode, out var existingNode) ||
existingNode is not YamlSequenceNode lstOri ||
valueNode is not YamlSequenceNode lstValue)
{
return;
}
var lstOri = (List<object>)fileContent[key];
var lstValue = (List<object>)value;
if (blRemoved)
{
foreach (var item in lstValue)
var removePrefixes = lstValue.Children
.OfType<YamlScalarNode>()
.Select(s => s.Value)
.Where(v => !string.IsNullOrEmpty(v))
.ToList();
for (var i = lstOri.Children.Count - 1; i >= 0; i--)
{
lstOri.RemoveAll(t => t.ToString().StartsWith(item.ToString()));
if (lstOri.Children[i] is YamlScalarNode { Value: not null } itemScalar)
{
if (removePrefixes.Any(prefix => itemScalar.Value.StartsWith(prefix!)))
{
lstOri.Children.RemoveAt(i);
}
}
}
return;
}
if (blPrepend)
{
lstValue.Reverse();
lstValue.ForEach(item => lstOri.Insert(0, item));
// Insert in reverse order to maintain original sequence at index 0
for (var i = lstValue.Children.Count - 1; i >= 0; i--)
{
lstOri.Children.Insert(0, lstValue.Children[i]);
}
}
else
{
lstValue.ForEach(lstOri.Add);
foreach (var item in lstValue.Children)
{
lstOri.Children.Add(item);
}
}
}
@@ -63,7 +63,7 @@ public partial class CoreConfigSingboxService
}
var tunInbound = JsonUtils.Deserialize<Inbound4Sbox>(EmbedUtils.GetEmbedText(Global.TunSingboxInboundFileName)) ?? new Inbound4Sbox { };
tunInbound.interface_name = context.IsMacOS ? $"utun{new Random().Next(99)}" : "singbox_tun";
tunInbound.interface_name = context.IsMacOS ? null : "singbox_tun";
tunInbound.mtu = _config.TunModeItem.Mtu;
tunInbound.auto_route = _config.TunModeItem.AutoRoute;
tunInbound.strict_route = _config.TunModeItem.StrictRoute;
@@ -77,7 +77,7 @@ public partial class CoreConfigSingboxService
}
}
if (_node.ConfigType == EConfigType.WireGuard)
if (_node.ConfigType is EConfigType.WireGuard or EConfigType.MASQUE)
{
var endpoint = JsonUtils.Deserialize<Endpoints4Sbox>(txtOutbound);
FillEndpoint(endpoint);
@@ -350,13 +350,15 @@ public partial class CoreConfigSingboxService
{
var protocolExtra = _node.GetProtocolExtra();
endpoint.address = Utils.String2List(protocolExtra.WgInterfaceAddress)?.Select(s => s.Trim()).ToList() ?? ["172.16.0.2/32"];
endpoint.type = Global.ProtocolTypes[_node.ConfigType];
switch (_node.ConfigType)
{
case EConfigType.WireGuard:
{
endpoint.server = null;
endpoint.server_port = null;
endpoint.address = Utils.String2List(protocolExtra.WgInterfaceAddress)?.Select(s => s.Trim()).ToList() ?? ["172.16.0.2/32"];
var peer = new Peer4Sbox
{
public_key = protocolExtra.WgPublicKey ?? string.Empty,
@@ -371,6 +373,28 @@ public partial class CoreConfigSingboxService
endpoint.peers = [peer];
break;
}
case EConfigType.MASQUE:
{
endpoint.type = "masque-client";
endpoint.server = _node.Address;
endpoint.server_port = _node.Port;
endpoint.username = _node.Username;
endpoint.password = _node.Password;
endpoint.path = protocolExtra?.MasquePath.NullIfEmpty();
if (!string.IsNullOrEmpty(protocolExtra?.MasqueHeaders))
{
var headers = Utils.ParseHeaders(protocolExtra.MasqueHeaders)
.GroupBy(x => x.Item1, StringComparer.OrdinalIgnoreCase)
.ToDictionary(
g => g.Key,
g => g.Select(x => x.Item2).ToList(),
StringComparer.OrdinalIgnoreCase
);
endpoint.headers = headers.Count > 0 ? headers : null;
}
FillOutboundTls(endpoint);
break;
}
}
}
catch (Exception ex)
@@ -402,7 +426,7 @@ public partial class CoreConfigSingboxService
}
}
private void FillOutboundTls(Outbound4Sbox outbound)
private void FillOutboundTls(BaseServer4Sbox server)
{
try
{
@@ -476,7 +500,7 @@ public partial class CoreConfigSingboxService
{
tls.ech = ech;
}
outbound.tls = tls;
server.tls = tls;
}
catch (Exception ex)
{
@@ -501,7 +525,7 @@ public partial class CoreConfigSingboxService
transport.type = nameof(ETransport.http);
transport.host = transportExtra.Host.IsNullOrEmpty()
? null
: Utils.String2List(transportExtra.Host);
: JsonUtils.SerializeToArray(Utils.String2List(transportExtra.Host));
transport.path = transportExtra.Path.NullIfEmpty();
if (!useragentValue.IsNullOrEmpty())
{
@@ -176,13 +176,13 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
var tag = Global.ProxyTag + inbound.port.ToString();
var isBalancer = false;
//outbound
var proxyOutbounds =
new CoreConfigV2rayService(context with { Node = item }).BuildAllProxyOutbounds(tag);
var service = new CoreConfigV2rayService(context with { Node = item });
var proxyOutbounds = service.BuildAllProxyOutbounds(tag);
_coreConfig.outbounds.AddRange(proxyOutbounds);
if (proxyOutbounds.Count(n => n.tag.StartsWith(tag)) > 1)
{
isBalancer = true;
var multipleLoad = _node.GetProtocolExtra().MultipleLoad ?? EMultipleLoad.LeastPing;
var multipleLoad = service.FindFirstPolicyGroupMultipleLoad() ?? EMultipleLoad.LeastPing;
GenObservatory(multipleLoad, tag);
GenBalancer(multipleLoad, tag);
}
@@ -17,14 +17,14 @@ public partial class CoreConfigV2rayService
}
// DNS routing
var dnsObj = JsonUtils.SerializeToNode(_coreConfig.dns);
if (dnsObj == null)
var dnsNode = JsonUtils.SerializeToNode(_coreConfig.dns);
if (dnsNode is not JsonObject dnsObj)
{
return;
}
dnsObj["tag"] = Global.DnsTag;
_coreConfig.dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(dnsObj));
_coreConfig.dns = dnsObj;
_coreConfig.routing.rules.Add(new RulesItem4Ray
{
type = "field",
@@ -34,7 +34,7 @@ public partial class CoreConfigV2rayService
return;
}
var simpleDnsItem = context.SimpleDnsItem;
var dnsItem = _coreConfig.dns as Dns4Ray ?? new Dns4Ray();
var dnsItem = new Dns4Ray();
var strategy4Freedom = simpleDnsItem?.Strategy4Freedom ?? Global.AsIs;
//Outbound Freedom domainStrategy
@@ -117,7 +117,7 @@ public partial class CoreConfigV2rayService
balancerTag = finalRule.balancerTag,
});
_coreConfig.dns = dnsItem;
_coreConfig.dns = (JsonObject)JsonUtils.SerializeToNode(dnsItem);
}
catch (Exception ex)
{
@@ -317,7 +317,7 @@ public partial class CoreConfigV2rayService
if (!useDirectDns)
{
dnsItem.servers.AddRange(remoteDNSAddress);
dnsItem.servers.AddRange(JsonUtils.SerializeToArray(remoteDNSAddress));
}
else
{
@@ -326,8 +326,7 @@ public partial class CoreConfigV2rayService
var dnsServer = CreateDnsServer(dns, []);
dnsServer.tag = $"{Global.DirectDnsTag}-{directDnsTagIndex++}";
dnsServer.skipFallback = false;
dnsItem.servers.Add(JsonUtils.SerializeToNode(dnsServer,
new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }));
dnsItem.servers.Add(JsonUtils.SerializeToNode(dnsServer));
}
}
return;
@@ -375,15 +374,21 @@ public partial class CoreConfigV2rayService
{
return;
}
foreach (var dnsAddress in dnsAddresses)
//foreach (var dnsAddress in dnsAddresses)
for (var i = 0; i < dnsAddresses.Count; i++)
{
var dnsAddress = dnsAddresses[i];
var dnsServer = CreateDnsServer(dnsAddress, domains, expectedIPs);
if (isDirectDns)
{
dnsServer.tag = $"{Global.DirectDnsTag}-{directDnsTagIndex++}";
}
var dnsServerNode = JsonUtils.SerializeToNode(dnsServer,
new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull });
if (expectedIPs is not null
&& i == expectedIPs.Count - 1)
{
dnsServer.finalQuery = true;
}
var dnsServerNode = JsonUtils.SerializeToNode(dnsServer);
dnsItem.servers.Add(dnsServerNode);
}
}
@@ -396,12 +401,12 @@ public partial class CoreConfigV2rayService
{
return;
}
dnsItem.hosts ??= new Dictionary<string, object>();
dnsItem.hosts ??= new Dictionary<string, JsonNode>();
if (simpleDNSItem.AddCommonHosts == true)
{
dnsItem.hosts = Global.PredefinedHosts.ToDictionary(
kvp => kvp.Key,
kvp => (object)kvp.Value
kvp => (JsonNode)JsonUtils.SerializeToArray(kvp.Value)
);
}
@@ -414,14 +419,14 @@ public partial class CoreConfigV2rayService
{
foreach (var host in systemHosts)
{
normalHost.TryAdd(host.Key, new List<string> { host.Value });
normalHost.TryAdd(host.Key, JsonUtils.SerializeToArray(new List<string> { host.Value }));
}
}
}
foreach (var kvp in Utils.ParseHostsToDictionary(simpleDNSItem.Hosts))
{
dnsItem.hosts[kvp.Key] = kvp.Value;
dnsItem.hosts[kvp.Key] = JsonUtils.SerializeToArray(kvp.Value);
}
}
@@ -449,8 +454,8 @@ public partial class CoreConfigV2rayService
}
}
var obj = JsonUtils.ParseJson(customDNS);
if (obj is null)
var node = JsonUtils.ParseJson(customDNS);
if (node is not JsonObject obj)
{
List<string> servers = [];
var arrDNS = customDNS.Split(',');
@@ -458,8 +463,7 @@ public partial class CoreConfigV2rayService
{
servers.Add(str);
}
obj = JsonUtils.ParseJson("{}");
obj["servers"] = JsonUtils.SerializeToNode(servers);
obj = new JsonObject { ["servers"] = JsonUtils.SerializeToNode(servers) };
}
// Append to dns settings
@@ -67,12 +67,10 @@ public partial class CoreConfigV2rayService
var address = _config.TunModeItem.IPv4Address.NullIfEmpty() ?? Global.TunIPv4Address.First();
tunInbound.settings.gateway = [address];
// Route both families into the tunnel regardless of EnableIPv6Address. That option only
// controls whether the interface gets an IPv6 address; leaving ::/0 out of the routing
// table makes IPv6 follow the system default route and bypass the tunnel entirely.
// A host without a global IPv6 address is the exception: it has nothing to leak,
// and IPv6 sent into the tunnel would have no way back out.
tunInbound.settings.autoSystemRoutingTable = context.HasGlobalIPv6Address
// Without ::/0, IPv6 bypasses the tunnel. Skip it only when the host has no global IPv6
// address and EnableIPv6Address is off; NAT66 hosts have only ULA addresses.
var routeIPv6 = _config.TunModeItem.EnableIPv6Address || context.HasGlobalIPv6Address;
tunInbound.settings.autoSystemRoutingTable = routeIPv6
? ["0.0.0.0/0", "::/0"]
: ["0.0.0.0/0"];
if (_config.TunModeItem.EnableIPv6Address == true)
@@ -86,10 +84,19 @@ public partial class CoreConfigV2rayService
{
tunInbound.settings.autoOutboundsInterface = bindInterface;
}
tunInbound.sniffing = inbound.sniffing;
tunInbound.sniffing = JsonUtils.DeepCopy(inbound.sniffing);
// tunInbound.sniffing.routeOnly = inbound.sniffing.routeOnly;
tunInbound.sniffing.routeOnly = true;
if (context.IsWindows && _config.TunModeItem.StrictRoute == true)
{
tunInbound.settings.autoSystemWfpBlockLeak = ["dns", "misconfigtun"];
}
if (context.IsLinux)
{
tunInbound.settings.autoSystemDnsToGateway = true;
}
if (_config.TunModeItem.RouteExcludeAddress is { Count: > 0 })
{
var wholeInternet = IPNetwork2.Parse("0.0.0.0/0");
@@ -99,7 +106,7 @@ public partial class CoreConfigV2rayService
.Where(x => x != null).ToList();
var includeList = new List<IPNetwork2> { wholeInternet };
var includeListV6 = context.HasGlobalIPv6Address
var includeListV6 = routeIPv6
? new List<IPNetwork2> { wholeInternetV6 }
: new List<IPNetwork2>();
@@ -8,7 +8,7 @@ public partial class CoreConfigV2rayService
_coreConfig.outbounds.InsertRange(0, proxyOutboundList);
if (proxyOutboundList.Count(n => n.tag.StartsWith(Global.ProxyTag)) > 1)
{
var multipleLoad = _node.GetProtocolExtra().MultipleLoad ?? EMultipleLoad.LeastPing;
var multipleLoad = FindFirstPolicyGroupMultipleLoad() ?? EMultipleLoad.LeastPing;
GenObservatory(multipleLoad);
GenBalancer(multipleLoad);
}
@@ -182,6 +182,12 @@ public partial class CoreConfigV2rayService
outboundSettings.version = 2;
break;
}
case EConfigType.MASQUE:
{
outboundSettings.address = _node.Address;
outboundSettings.port = _node.Port;
break;
}
case EConfigType.WireGuard:
{
var address = _node.Address;
@@ -197,7 +203,7 @@ public partial class CoreConfigV2rayService
};
var setting = new Outboundsettings4Ray
{
address = Utils.String2List(protocolExtra.WgInterfaceAddress)?.Select(s => s.Trim()).ToList() ?? ["172.16.0.2/32"],
address = JsonUtils.SerializeToArray(Utils.String2List(protocolExtra.WgInterfaceAddress)?.Select(s => s.Trim()).ToList() ?? ["172.16.0.2/32"]),
secretKey = _node.Password,
reserved = Utils.String2List(protocolExtra.WgReserved)?.Select(s => s.Trim()).Select(int.Parse).ToList(),
mtu = protocolExtra.WgMtu > 0 ? protocolExtra.WgMtu : Global.TunMtus.First(),
@@ -257,8 +263,13 @@ public partial class CoreConfigV2rayService
{
network = "hysteria";
}
else if (_node.ConfigType == EConfigType.MASQUE)
{
network = "masque";
}
streamSettings.network = network;
var transport = _node.GetTransportExtra();
var protocolExtra = _node.GetProtocolExtra();
var host = string.Empty;
var path = string.Empty;
var kcpSeed = string.Empty;
@@ -326,11 +337,6 @@ public partial class CoreConfigV2rayService
{
tlsSettings.serverName = Utils.String2List(host)?.First();
}
if (!tlsSettings.echConfigList.IsNullOrEmpty())
{
// For legacy xray compatibility, remove this in the future
tlsSettings.echForceQuery = "full";
}
var certs = CertPemManager.ParsePemChain(_node.Cert);
if (certs.Count > 0)
{
@@ -417,7 +423,7 @@ public partial class CoreConfigV2rayService
}
kcpFinalmask.udp?.Reverse();
streamSettings.kcpSettings = kcpSettings;
streamSettings.finalmask = kcpFinalmask;
streamSettings.finalmask = (JsonObject)JsonUtils.SerializeToNode(kcpFinalmask);
break;
//ws
case nameof(ETransport.ws):
@@ -478,7 +484,7 @@ public partial class CoreConfigV2rayService
}
if (xhttpExtra.IsNotEmpty())
{
xhttpSettings.extra = JsonUtils.ParseJson(xhttpExtra);
xhttpSettings.extra = JsonUtils.ParseJson(xhttpExtra) as JsonObject;
}
streamSettings.xhttpSettings = xhttpSettings;
@@ -502,7 +508,6 @@ public partial class CoreConfigV2rayService
break;
case "hysteria":
var protocolExtra = _node.GetProtocolExtra();
var ports = protocolExtra?.Ports;
int? upMbps = protocolExtra?.UpMbps is { } su and >= 0
? su
@@ -571,7 +576,43 @@ public partial class CoreConfigV2rayService
auth = _node.Password,
};
hy2Finalmask.udp?.Reverse();
streamSettings.finalmask = hy2Finalmask;
streamSettings.finalmask = (JsonObject)JsonUtils.SerializeToNode(hy2Finalmask);
break;
case "masque":
var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var authValue = $"Basic {Utils.Base64Encode($"{_node.Username}:{_node.Password}")}";
headers.Add("Authorization", authValue);
if (!string.IsNullOrEmpty(protocolExtra?.MasqueHeaders))
{
var userHeaders = Utils.ParseHeaders(protocolExtra.MasqueHeaders)
.GroupBy(x => x.Item1, StringComparer.OrdinalIgnoreCase)
.ToDictionary(
g => g.Key,
g => string.Join(", ", g.Select(x => x.Item2)),
StringComparer.OrdinalIgnoreCase
);
if (userHeaders.TryGetValue("Authorization", out var userAuthValue))
{
headers["Authorization"] = userAuthValue;
}
headers = userHeaders;
}
var masqueSettings = new MasqueSettings4Ray
{
path = protocolExtra?.MasquePath.NullIfEmpty(),
headers = headers.Count > 0 ? headers : null,
};
if (!protocolExtra.WarpPrivateKey.IsNullOrEmpty())
{
masqueSettings.warp = new MasqueWarpSettings4Ray
{
publicKey = protocolExtra.WarpPublicKey,
privateKey = protocolExtra.WarpPrivateKey,
address = Utils.String2List(protocolExtra.WarpAddress)?.Select(s => s.Trim()).ToList(),
};
}
streamSettings.masqueSettings = masqueSettings;
break;
default:
@@ -601,7 +642,7 @@ public partial class CoreConfigV2rayService
pathHttp = string.Join(",".AppendQuotes(), arrPath);
}
request = request.Replace("$requestPath$", $"{pathHttp.AppendQuotes()}");
rawSettings.header.request = JsonUtils.Deserialize<object>(request);
rawSettings.header.request = JsonUtils.SerializeToNode(request) as JsonObject;
streamSettings.rawSettings = rawSettings;
}
@@ -610,7 +651,7 @@ public partial class CoreConfigV2rayService
if (!_node.Finalmask.IsNullOrEmpty())
{
streamSettings.finalmask = JsonUtils.ParseJson(_node.Finalmask);
streamSettings.finalmask = JsonUtils.ParseJson(_node.Finalmask) as JsonObject;
}
}
catch (Exception ex)
@@ -773,8 +814,6 @@ public partial class CoreConfigV2rayService
&& (n.streamSettings?.sockopt?.dialerProxy?.IsNullOrEmpty() ?? true))
.ToList();
var fragmentMask = BuildFragmentsMasks();
foreach (var outbound in actOutboundWithTlsList)
{
var finalMaskJsonObj = JsonUtils.ParseJson(JsonUtils.Serialize(outbound.streamSettings?.finalmask)) as JsonObject ?? new JsonObject();
@@ -782,6 +821,13 @@ public partial class CoreConfigV2rayService
var tcpFinalmaskList = finalMaskJsonObj["tcp"] as JsonArray ?? [];
if (tcpFinalmaskList.Count == 0)
{
var fragmentMask = BuildFragmentsMasks();
// Xray's tlshello fragment is incompatible with REALITY.
if (outbound.streamSettings.security == Global.StreamSecurityReality
&& fragmentMask.settings?.packets == "tlshello")
{
fragmentMask.settings!.packets = "1-3";
}
tcpFinalmaskList.Add(JsonUtils.SerializeToNode(fragmentMask));
finalMaskJsonObj["tcp"] = tcpFinalmaskList;
}
@@ -801,10 +847,10 @@ public partial class CoreConfigV2rayService
protocol = "freedom",
streamSettings = new StreamSettings4Ray
{
finalmask = new Finalmask4Ray
finalmask = JsonUtils.SerializeToNode(new Finalmask4Ray
{
tcp = [fragmentMask],
},
}) as JsonObject,
},
};
@@ -878,4 +924,32 @@ public partial class CoreConfigV2rayService
}
return clonedOutbounds;
}
private EMultipleLoad? FindFirstPolicyGroupMultipleLoad()
{
if (!_node.ConfigType.IsGroupType())
{
return null;
}
// BFS: the nearest policy group in the group tree decides the multiple load strategy.
var queue = new Queue<ProfileItem>([_node]);
while (queue.Count > 0)
{
var node = queue.Dequeue();
if (node.ConfigType == EConfigType.PolicyGroup)
{
return node.GetProtocolExtra().MultipleLoad;
}
foreach (var childId in Utils.String2List(node.GetProtocolExtra().ChildItems) ?? [])
{
if (context.AllProxiesMap.TryGetValue(childId, out var childNode)
&& childNode.ConfigType.IsGroupType())
{
queue.Enqueue(childNode);
}
}
}
return null;
}
}
@@ -200,11 +200,12 @@ public partial class CoreConfigV2rayService
return tag;
}
var proxyOutbounds = new CoreConfigV2rayService(context with { Node = node, }).BuildAllProxyOutbounds(tag);
var service = new CoreConfigV2rayService(context with { Node = node, });
var proxyOutbounds = service.BuildAllProxyOutbounds(tag);
_coreConfig.outbounds.AddRange(proxyOutbounds);
if (proxyOutbounds.Count(n => n.tag.StartsWith(tag)) > 1)
{
var multipleLoad = node.GetProtocolExtra().MultipleLoad ?? EMultipleLoad.LeastPing;
var multipleLoad = service.FindFirstPolicyGroupMultipleLoad() ?? EMultipleLoad.LeastPing;
GenObservatory(multipleLoad, tag);
GenBalancer(multipleLoad, tag);
}

Some files were not shown because too many files have changed in this diff Show More