Compare commits

...

44 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
270 changed files with 32023 additions and 1105 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
View File
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
View File
@@ -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>
+11 -13
View File
@@ -6,27 +6,25 @@
</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.2" />
<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="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" />
@@ -34,4 +32,4 @@
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.22" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" />
</ItemGroup>
</Project>
</Project>
@@ -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()
{
@@ -27,16 +27,51 @@ public class ConfigHandlerCustomSubscriptionTests
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,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();
}
}
}
}
+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;
+3 -4
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();
}
@@ -1131,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;
}
+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)
{
+18 -3
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";
@@ -498,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", "简体中文"),
@@ -656,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()
+87 -5
View File
@@ -540,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
@@ -1000,10 +1061,6 @@ public static class ConfigHandler
profileItem.Address = profileItem.Address.TrimEx();
profileItem.Username = profileItem.Username.TrimEx();
profileItem.Password = profileItem.Password.TrimEx();
profileItem.Fingerprint = string.Empty;
profileItem.Alpn = string.Empty;
profileItem.Network = string.Empty;
profileItem.AllowInsecure = string.Empty;
if (profileItem.StreamSecurity.IsNullOrEmpty())
{
profileItem.StreamSecurity = Global.StreamSecurity;
@@ -1656,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());
@@ -1696,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),
@@ -2073,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;
@@ -2133,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)
{
@@ -2236,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);
@@ -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,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,7 +219,6 @@ public class WebDavItem
public string? DirName { get; set; }
}
[Serializable]
public class CheckUpdateItem
{
public bool UpdateViaProxy { get; set; } = true;
@@ -247,7 +226,6 @@ public class CheckUpdateItem
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; }
@@ -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; }
@@ -232,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; }
@@ -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; }
@@ -130,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; }
@@ -142,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; }
@@ -199,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; }
@@ -214,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; }
}
@@ -226,7 +227,6 @@ public class Routing4Ray
public List<BalancersItem4Ray>? balancers { get; set; }
}
[Serializable]
public class RulesItem4Ray
{
public string? type { get; set; }
@@ -321,7 +321,7 @@ public class StreamSettings4Ray
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; }
}
@@ -344,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; }
}
@@ -363,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
@@ -411,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
@@ -452,6 +451,14 @@ 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
@@ -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;
@@ -49,6 +49,9 @@ public record ProtocolExtraItem
// 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; }
@@ -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; }
}
+27
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>
@@ -978,6 +987,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Columns 的本地化字符串。
/// </summary>
public static string menuColumns {
get {
return ResourceManager.GetString("menuColumns", resourceCulture);
}
}
/// <summary>
/// 查找类似 Close Connection 的本地化字符串。
/// </summary>
@@ -5157,6 +5175,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Warp 的本地化字符串。
/// </summary>
public static string TbWarp {
get {
return ResourceManager.GetString("TbWarp", resourceCulture);
}
}
/// <summary>
/// 查找类似 Xray Only 的本地化字符串。
/// </summary>
+9
View File
@@ -1905,4 +1905,13 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<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>
@@ -1897,4 +1897,13 @@
<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>
+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>
-149
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
@@ -400,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
@@ -531,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
@@ -1722,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);
}
}
}
@@ -356,6 +356,8 @@ public partial class CoreConfigSingboxService
{
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
{
@@ -523,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
@@ -84,7 +84,7 @@ 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;
@@ -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);
}
@@ -203,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(),
@@ -337,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)
{
@@ -428,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):
@@ -489,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;
@@ -581,7 +576,7 @@ public partial class CoreConfigV2rayService
auth = _node.Password,
};
hy2Finalmask.udp?.Reverse();
streamSettings.finalmask = hy2Finalmask;
streamSettings.finalmask = (JsonObject)JsonUtils.SerializeToNode(hy2Finalmask);
break;
case "masque":
@@ -603,11 +598,21 @@ public partial class CoreConfigV2rayService
}
headers = userHeaders;
}
streamSettings.masqueSettings = new()
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:
@@ -637,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;
}
@@ -646,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)
@@ -809,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();
@@ -818,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;
}
@@ -837,10 +847,10 @@ public partial class CoreConfigV2rayService
protocol = "freedom",
streamSettings = new StreamSettings4Ray
{
finalmask = new Finalmask4Ray
finalmask = JsonUtils.SerializeToNode(new Finalmask4Ray
{
tcp = [fragmentMask],
},
}) as JsonObject,
},
};
@@ -914,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);
}
+17 -2
View File
@@ -188,10 +188,13 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
break;
case ESpeedActionType.Speedtest:
// A skipped or failed test must not leave the last speed behind for sorting.
ProfileExManager.Instance.SetTestSpeed(it.IndexId, 0);
await UpdateFunc(it.IndexId, "", message);
break;
case ESpeedActionType.Mixedtest:
ProfileExManager.Instance.SetTestSpeed(it.IndexId, 0);
await UpdateFunc(it.IndexId, message, message);
break;
}
@@ -538,8 +541,20 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
private async Task<int> DoUdpTest(ServerTestItem it,
ConcurrentDictionary<string, byte> completedIds, CancellationToken ct = default)
{
var udpService = UdpTestService.CreateFromTarget(_config?.SpeedTestItem.UdpTestTarget, out var udpTestUrl);
var responseTime = (int)(await udpService.SendUdpRequestAsync(udpTestUrl, it.Port, ct)).TotalMilliseconds;
var responseTime = -1;
try
{
var udpService = UdpTestService.CreateFromTarget(_config?.SpeedTestItem.UdpTestTarget, out var udpTestUrl);
responseTime = (int)(await udpService.SendUdpRequestAsync(udpTestUrl, it.Port, ct)).TotalMilliseconds;
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
throw;
}
catch
{
// Ignore
}
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
await UpdateFunc(it.IndexId, responseTime.ToString());
+5 -1
View File
@@ -335,9 +335,13 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
//Check for avalonia desktop windows version
if (File.Exists(Path.Combine(Utils.GetBaseDirectory(), "libHarfBuzzSharp.dll")))
{
return url?.Replace(".zip", "-desktop.zip");
url = url?.Replace(".zip", "-desktop.zip");
}
#if V2RAYN_IS_AOT
url = url?.Replace(".zip", "-aot.zip");
#endif
return url;
}
else if (Utils.IsLinux())
@@ -24,7 +24,7 @@ public sealed class WindowsJobService : IDisposable
try
{
var length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION));
var length = Marshal.SizeOf<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>();
extendedInfoPtr = Marshal.AllocHGlobal(length);
Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false);
@@ -103,6 +103,18 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable
[Reactive]
public partial string MasqueHeaders { get; set; }
[Reactive]
public partial string WarpPublicKey { get; set; }
[Reactive]
public partial string WarpPrivateKey { get; set; }
[Reactive]
public partial string WarpAddress { get; set; }
[Reactive]
public partial string Network { get; set; }
[Reactive]
public partial string RawHeaderType { get; set; }
@@ -274,7 +286,7 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable
this.WhenAnyValue(x => x.CertSha)
.Subscribe(_ => UpdateCertTip());
this.WhenAnyValue(x => x.SelectedSource.Network)
this.WhenAnyValue(x => x.Network)
.Subscribe(_ =>
{
this.RaisePropertyChanged(nameof(TransportHeaderType));
@@ -299,6 +311,7 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable
CoreType = SelectedSource?.CoreType?.ToString();
AllowInsecure = SelectedSource?.GetAllowInsecure() == true;
MuxEnabled = SelectedSource?.MuxEnabled == true;
Network = SelectedSource?.Network ?? Global.DefaultNetwork;
Cert = SelectedSource?.Cert ?? string.Empty;
CertSha = SelectedSource?.CertSha ?? string.Empty;
@@ -330,6 +343,9 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable
GeckoMaxPacketSize = protocolExtra.GeckoMaxPacketSize.ToInt();
MasquePath = protocolExtra.MasquePath ?? string.Empty;
MasqueHeaders = protocolExtra.MasqueHeaders ?? string.Empty;
WarpPublicKey = protocolExtra.WarpPublicKey ?? string.Empty;
WarpPrivateKey = protocolExtra.WarpPrivateKey ?? string.Empty;
WarpAddress = protocolExtra.WarpAddress ?? string.Empty;
RawHeaderType = transport.RawHeaderType ?? Global.None;
Host = transport.Host ?? string.Empty;
@@ -405,10 +421,11 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable
SelectedSource.MuxEnabled = MuxEnabled;
SelectedSource.Cert = Cert.IsNullOrEmpty() ? string.Empty : Cert;
SelectedSource.CertSha = CertSha.IsNullOrEmpty() ? string.Empty : CertSha;
if (!Global.Networks.Contains(SelectedSource.Network))
if (!Global.Networks.Contains(Network))
{
SelectedSource.Network = Global.DefaultNetwork;
Network = Global.DefaultNetwork;
}
SelectedSource.Network = Network;
var transport = new TransportExtraItem
{
@@ -453,6 +470,9 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable
GeckoMaxPacketSize = GeckoMaxPacketSize > 0 ? GeckoMaxPacketSize.ToString() : null,
MasquePath = MasquePath.NullIfEmpty(),
MasqueHeaders = MasqueHeaders.NullIfEmpty(),
WarpPublicKey = WarpPublicKey.NullIfEmpty(),
WarpPrivateKey = WarpPrivateKey.NullIfEmpty(),
WarpAddress = WarpAddress.NullIfEmpty(),
});
SelectedSource.SetTransportExtra(transport);
@@ -2,7 +2,7 @@ namespace ServiceLib.ViewModels;
public partial class ClashProxiesViewModel : MyReactiveObject
{
private readonly int _delayTimeout = 99999999;
private const int _delayTimeout = 99999999;
private ClashItem _clashItem = new();
public ClashProxiesViewModel()
@@ -235,6 +235,11 @@ public partial class ClashProxiesViewModel : MyReactiveObject
{
return;
}
var dicCurrentDetails = new Dictionary<string, ClashProxyModel>();
foreach (var item in ProxyDetails)
{
dicCurrentDetails.TryAdd(item.Name, item);
}
var lstDetails = new List<ClashProxyModel>();
foreach (var item in proxy.all)
{
@@ -243,15 +248,29 @@ public partial class ClashProxiesViewModel : MyReactiveObject
{
continue;
}
var delay = proxy2.history?.Count > 0 ? proxy2.history.Last().delay : -1;
var delay = -1;
var hasData = false;
if (proxy2.history is { Count: > 0 })
{
delay = proxy2.history.Last().delay;
hasData = true;
}
else if (dicCurrentDetails.TryGetValue(item, out var detail))
{
delay = detail.Delay;
hasData = true;
}
var isValidDelay = delay is > 0 and not _delayTimeout;
lstDetails.Add(new ClashProxyModel
{
IsActive = item == proxy.now,
Name = item,
Type = proxy2.type,
Delay = delay <= 0 ? _delayTimeout : delay,
DelayName = delay <= 0 ? string.Empty : $"{delay}ms",
Delay = isValidDelay ? delay : _delayTimeout,
DelayName = !hasData ? string.Empty : (isValidDelay ? $"{delay}ms" : "-1"),
});
}
// sort
@@ -340,7 +359,7 @@ public partial class ClashProxiesViewModel : MyReactiveObject
private async Task TestGroupProxiesDelay()
{
var groupProxy = TryGetProxy(SelectedGroup.Name);
if (!Global.allowSelectType.Contains(groupProxy?.type))
if (!Global.allowSelectType.Contains(groupProxy?.type, StringComparer.OrdinalIgnoreCase))
{
return;
}
@@ -362,8 +381,9 @@ public partial class ClashProxiesViewModel : MyReactiveObject
{
return;
}
detail.Delay = Convert.ToInt32(result.Delay);
detail.DelayName = $"{detail.Delay}ms";
var isValid = int.TryParse(result.Delay, out var delay) && delay > 0 && delay != _delayTimeout;
detail.Delay = isValid ? delay : _delayTimeout;
detail.DelayName = isValid ? $"{delay}ms" : "-1";
await Task.CompletedTask;
}
@@ -35,7 +35,7 @@ public partial class DNSSettingViewModel : MyReactiveObject, ICloseable
[Reactive] public partial bool RayCustomDNSEnableCompatible { get; set; }
[Reactive] public partial bool SBCustomDNSEnableCompatible { get; set; }
public bool IsSimpleDNSEnabled => !(RayCustomDNSEnableCompatible && SBCustomDNSEnableCompatible);
[ObservableAsProperty] public partial bool IsSimpleDNSEnabled { get; }
public ReactiveCommand<RxVoid, RxVoid> SaveCmd { get; }
public ReactiveCommand<RxVoid, RxVoid> ImportDefConfig4V2rayCompatibleCmd { get; }
@@ -60,8 +60,11 @@ public partial class DNSSettingViewModel : MyReactiveObject, ICloseable
await Task.CompletedTask;
});
this.WhenAnyValue(x => x.RayCustomDNSEnableCompatible, x => x.SBCustomDNSEnableCompatible)
.Subscribe(_ => this.RaisePropertyChanged(nameof(IsSimpleDNSEnabled)));
_isSimpleDNSEnabledHelper = this.WhenAnyValue(
x => x.RayCustomDNSEnableCompatible,
x => x.SBCustomDNSEnableCompatible,
(a, b) => !(a && b))
.ToProperty(this, nameof(IsSimpleDNSEnabled));
_ = Init();
}
@@ -448,7 +448,7 @@ public partial class ProfilesViewModel : MyReactiveObject
return null;
}
var orderProfiles = SelectedProfiles?.OrderBy(t => t.Sort);
var orderProfiles = SelectedProfiles?.OrderBy(t => t.Sort).ToList();
if (latest)
{
lstSelected.AddRange(await AppManager.Instance.GetProfileItemsOrderedByIndexIds(orderProfiles.Select(sp => sp?.IndexId)));
@@ -702,6 +702,33 @@ public partial class ProfilesViewModel : MyReactiveObject
}
}
public async Task MoveServersTo(
IReadOnlyList<int> sourceIndices,
int insertIndex)
{
if (sourceIndices.Count == 0 ||
insertIndex < 0 ||
insertIndex > ProfileItems.Count)
{
return;
}
var orderedProfileIds = ProfileItems
.Select(item => item.IndexId)
.ToList();
var result = await ConfigHandler.MoveServersTo(
_config,
orderedProfileIds,
sourceIndices,
insertIndex);
if (result == 0)
{
await RefreshServers();
}
}
public async Task ServerSpeedtest(ESpeedActionType actionType)
{
List<ProfileItem>? lstSelected;
@@ -712,7 +739,7 @@ public partial class ProfilesViewModel : MyReactiveObject
actionType = ESpeedActionType.Realping;
}
lstSelected = JsonUtils.Deserialize<List<ProfileItem>>(JsonUtils.Serialize(ProfileItems?.OrderBy(t => t.Sort)));
lstSelected = JsonUtils.Deserialize<List<ProfileItem>>(JsonUtils.Serialize(ProfileItems?.OrderBy(t => t.Sort).ToList()));
}
else
{
@@ -191,13 +191,7 @@ public partial class RoutingRuleSettingViewModel : MyReactiveObject, ICloseable
}
if (lst.Count > 0)
{
var options = new JsonSerializerOptions
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
await SetClipboardDataInteraction.HandleSafe(JsonUtils.Serialize(lst, options));
await SetClipboardDataInteraction.HandleSafe(JsonUtils.SerializeCamelCase(lst));
}
}
+2 -1
View File
@@ -5,6 +5,7 @@
xmlns:dialogHost="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
xmlns:semi="https://irihi.tech/semi"
xmlns:service="clr-namespace:ServiceLib;assembly=ServiceLib"
xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib"
Name="v2rayN"
x:DataType="vms:StatusBarViewModel"
@@ -37,7 +38,7 @@
<TrayIcon
Command="{Binding NotifyLeftClickCmd}"
Icon="/Assets/NotifyIcon1.ico"
ToolTipText="{Binding RunningServerToolTipText}">
ToolTipText="{Binding RunningServerToolTipText, FallbackValue={x:Static service:Global.AppName}}">
<TrayIcon.Menu>
<NativeMenu>
<NativeMenuItem
@@ -69,7 +69,7 @@ public partial class QRCodeAvaloniaUtils
var bmi = new BITMAPINFO
{
biSize = Marshal.SizeOf(typeof(BITMAPINFO)),
biSize = Marshal.SizeOf<BITMAPINFO>(),
biWidth = width,
biHeight = -height,
biPlanes = 1,
+6
View File
@@ -0,0 +1,6 @@
<linker>
<assembly fullname="ServiceLib">
<type fullname="ServiceLib.Models.*" preserve="all" />
<type fullname="ServiceLib.Enums.*" preserve="all" />
</assembly>
</linker>
@@ -148,11 +148,13 @@
CanUserReorderColumns="False"
CanUserResizeColumns="True"
CanUserSortColumns="False"
FrozenColumnCount="2"
GridLinesVisibility="All"
HeadersVisibility="Column"
HeadersVisibility="All"
IsReadOnly="True"
ItemsSource="{Binding ChildItemsObs}"
SelectionMode="Extended">
SelectionMode="Extended"
ShowRowNumbers="True">
<DataGrid.ContextMenu>
<ContextMenu>
<MenuItem x:Name="menuAddChildServer" Header="{x:Static resx:ResUI.menuAddChildServer}" />
@@ -220,11 +222,13 @@
CanUserReorderColumns="False"
CanUserResizeColumns="True"
CanUserSortColumns="False"
FrozenColumnCount="2"
GridLinesVisibility="All"
HeadersVisibility="Column"
HeadersVisibility="All"
IsReadOnly="True"
ItemsSource="{Binding AllProfilePreviewItemsObs}"
SelectionMode="Extended">
SelectionMode="Extended"
ShowRowNumbers="True">
<DataGrid.Columns>
<DataGridTextColumn
Width="150"
@@ -55,7 +55,6 @@ public partial class AddGroupServerWindow : WindowBase<AddGroupServerViewModel>
// Keyboard shortcuts when focus is within grid
AddHandler(KeyDownEvent, AddGroupServerWindow_KeyDown, RoutingStrategies.Tunnel);
lstChild.LoadingRow += LstChild_LoadingRow;
}
private void InitializeData(ProfileItem profileItem)
@@ -77,11 +76,6 @@ public partial class AddGroupServerWindow : WindowBase<AddGroupServerViewModel>
}
}
private void LstChild_LoadingRow(object? sender, DataGridRowEventArgs e)
{
e.Row.Header = $" {e.Row.Index + 1}";
}
private void Window_Loaded(object? sender, RoutedEventArgs e)
{
txtRemarks.Focus();
@@ -789,7 +789,7 @@
Grid.Row="2"
ColumnDefinitions="300,Auto"
IsVisible="False"
RowDefinitions="Auto,Auto,Auto,Auto,Auto">
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto">
<TextBlock
Grid.Row="1"
Grid.Column="0"
@@ -845,6 +845,64 @@
Classes="TextArea"
PlaceholderText="Header: Value"
TextWrapping="NoWrap" />
<TextBlock
Grid.Row="5"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbWarp}" />
<Button
Grid.Row="5"
Grid.Column="1"
Margin="{StaticResource MarginLr4}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Classes="IconButton">
<Button.Content>
<PathIcon Data="{StaticResource SemiIconMore}">
<PathIcon.RenderTransform>
<RotateTransform Angle="90" />
</PathIcon.RenderTransform>
</PathIcon>
</Button.Content>
<Button.Flyout>
<Flyout>
<StackPanel>
<TextBlock
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
FontWeight="Bold"
Text="{x:Static resx:ResUI.TbXrayOnly}" />
<TextBlock
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbPrivateKey}" />
<TextBox
x:Name="txtWarpPrivateKey"
Width="400"
Margin="{StaticResource Margin4}" />
<TextBlock
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbPublicKey}" />
<TextBox
x:Name="txtWarpPublicKey"
Width="400"
Margin="{StaticResource Margin4}" />
<TextBlock
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbLocalAddress}" />
<TextBox
x:Name="txtWarpAddress"
Width="400"
Margin="{StaticResource Margin4}"
PlaceholderText="Ipv4, Ipv6" />
</StackPanel>
</Flyout>
</Button.Flyout>
</Button>
</Grid>
<Separator
@@ -147,12 +147,15 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtSecurity14.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.MasquePath, v => v.txtPath14.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.MasqueHeaders, v => v.txtHttpHeaders14.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.WarpPublicKey, v => v.txtWarpPublicKey.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.WarpPrivateKey, v => v.txtWarpPrivateKey.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.WarpAddress, v => v.txtWarpAddress.Text).DisposeWith(currentTypeDisposables);
break;
}
})
.DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Network, v => v.cmbNetwork.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Network, v => v.cmbNetwork.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.RawHeaderType, v => v.cmbHeaderTypeRaw.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Host, v => v.txtRequestHostRaw.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Path, v => v.txtPathRaw.Text).DisposeWith(disposables);
@@ -295,8 +298,6 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
gridMasque.IsVisible = true;
sepa2.IsVisible = false;
gridTransport.IsVisible = false;
cmbFingerprint.IsEnabled = false;
cmbAlpn.IsEnabled = false;
break;
}
cmbStreamSecurity.ItemsSource = lstStreamSecurity;
@@ -67,14 +67,10 @@ public partial class DNSSettingWindow : WindowBase<DNSSettingViewModel>
this.BindCommand(ViewModel, vm => vm.ImportDefConfig4V2rayCompatibleCmd, v => v.btnImportDefConfig4V2rayCompatible).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.ImportDefConfig4SingboxCompatibleCmd, v => v.btnImportDefConfig4SingboxCompatible).DisposeWith(disposables);
this.WhenAnyValue(x => x.ViewModel.IsSimpleDNSEnabled)
.Select(b => !b)
.BindTo(txtBasicDNSSettingsInvalid, t => t.IsVisible);
this.WhenAnyValue(x => x.ViewModel.IsSimpleDNSEnabled)
.Select(b => !b)
.BindTo(txtAdvancedDNSSettingsInvalid, t => t.IsVisible);
this.Bind(ViewModel, vm => vm.IsSimpleDNSEnabled, v => v.gridBasicDNSSettings.IsEnabled).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.IsSimpleDNSEnabled, v => v.gridAdvancedDNSSettings.IsEnabled).DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.IsSimpleDNSEnabled, v => v.txtBasicDNSSettingsInvalid.IsVisible, isSimpleDnsEnabled => !isSimpleDnsEnabled).DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.IsSimpleDNSEnabled, v => v.txtAdvancedDNSSettingsInvalid.IsVisible, isSimpleDnsEnabled => !isSimpleDnsEnabled).DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.IsSimpleDNSEnabled, v => v.gridBasicDNSSettings.IsEnabled).DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.IsSimpleDNSEnabled, v => v.gridAdvancedDNSSettings.IsEnabled).DisposeWith(disposables);
});
}

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