Compare commits

..

155 Commits

Author SHA1 Message Date
Alireza Ahmadi 13da0b59a9 v1.2.0-beta.1 2025-01-07 02:33:06 +01:00
Alireza Ahmadi 5713f97e42 remove /core folder 2025-01-07 02:32:32 +01:00
Alireza Ahmadi 7621b7a348 fix vless flow in client configs #354 2025-01-07 02:25:20 +01:00
Alireza Ahmadi 9e63944d83 fix show shadowsocks password in outbound #362 2025-01-07 01:39:08 +01:00
Alireza Ahmadi 8c9736cf19 update frontend 2025-01-07 01:23:35 +01:00
Alireza Ahmadi 5e626f7c18 Merge pull request #406 from alireza0/consolidation
Consolidation
2025-01-07 01:14:43 +01:00
Alireza Ahmadi eb4c128350 adjust hy2 to sing-box 1.11.0 2025-01-07 01:12:55 +01:00
Alireza Ahmadi 99f555dc95 delete old codes 2025-01-06 15:50:14 +01:00
Alireza Ahmadi d53d9b80f5 adjustments for singbox 1.11.0 2025-01-06 15:49:43 +01:00
Alireza Ahmadi 2d8b56208d update dependencies 2025-01-06 00:10:36 +01:00
Alireza Ahmadi f2605a550f update scripts 2025-01-06 00:10:17 +01:00
Alireza Ahmadi e92cf56557 update docker and release workflow 2025-01-06 00:09:15 +01:00
Alireza Ahmadi 8073e8ab0a delete old codes 2025-01-06 00:08:27 +01:00
Alireza Ahmadi 8bab127f19 show uri 2025-01-06 00:06:33 +01:00
Alireza Ahmadi a77964afc0 move wg keygen to backend 2025-01-05 23:00:11 +01:00
Alireza Ahmadi b2195b72b9 visual fixes 2025-01-05 21:55:41 +01:00
Alireza Ahmadi 5dd0baad34 avoid unknown actions 2025-01-05 21:52:50 +01:00
Alireza Ahmadi d86adedd8b adjust bulk creation 2025-01-05 21:52:14 +01:00
Alireza Ahmadi a6dc0cc589 small fixes 2025-01-05 19:36:35 +01:00
Alireza Ahmadi 751066ac6c full tls override in inbound multi-domain 2025-01-05 19:36:01 +01:00
Alireza Ahmadi dbee22b637 fix change tag 2025-01-05 19:33:56 +01:00
Alireza Ahmadi 56710aef1e move LinkGenerator ro backend 2025-01-05 19:33:31 +01:00
Alireza Ahmadi 753d1f9256 move outJson build process to backend 2025-01-04 21:52:41 +01:00
Alireza Ahmadi fe428ed412 all adjustments 2025-01-04 21:40:03 +01:00
Alireza Ahmadi ed48cdca33 load from database 2024-12-25 10:57:17 +01:00
Alireza Ahmadi 7a047daf6f migrate database 2024-12-22 14:41:22 +01:00
Alireza Ahmadi ecd9348a0f integrate core codes 2024-12-16 00:12:09 +01:00
Alireza Ahmadi f1b6c8a131 fix mixed inbound data for json sub 2024-11-21 22:51:14 +01:00
Alireza Ahmadi 056c458753 fix typo #361 2024-11-17 15:38:55 +01:00
Alireza Ahmadi 572268e9f6 fix naive link #359 2024-11-17 15:12:14 +01:00
Alireza Ahmadi 0101e342a0 fix typo in bash scripts 2024-11-17 14:46:01 +01:00
Alireza Ahmadi 8b663df1ee manual installation readme #358 2024-11-17 14:44:03 +01:00
tinybug 4649bd42ae fix build problem (#347)
* fix build problem

* fix fully remove S-UI panel in README.md

* fix frontend build problem (npm install before build frontend)

* web/html folder dosent exist at the fisrt place
2024-11-17 12:50:26 +01:00
Alireza Ahmadi e89ac96885 v1.1.0 2024-10-29 20:07:35 +01:00
Alireza Ahmadi fe5b6cf922 Fix volume configuration in docker-compose.yml #295
Co-authored-by: admiralhr99 <admiralhr99@gmail.com>
2024-10-29 13:22:07 +01:00
Alireza Ahmadi 42f24c45c9 update dependencies 2024-10-29 13:16:47 +01:00
Alireza Ahmadi 282e244517 improve systemd or docker detection 2024-10-29 13:14:25 +01:00
Alireza Ahmadi 1d46d72186 [tun] disable auto_route in server #207 2024-10-29 12:51:11 +01:00
Alireza Ahmadi 7554b02a61 add tun inbound #207 2024-10-29 01:21:12 +01:00
Alireza Ahmadi fecb29f6ab [subJson] inbound options #240 #318 2024-10-28 21:59:48 +01:00
Alireza Ahmadi 7b7e5ac79d fix typo 2024-10-28 16:54:15 +01:00
Alireza Ahmadi e5fc14efd4 [rules] drag&drop rules on desktop view 2024-10-28 16:09:22 +01:00
Alireza Ahmadi 837150e065 [rules] better view 2024-10-28 15:37:02 +01:00
Alireza Ahmadi 93868b02d4 better auto start status #260 2024-10-28 13:56:20 +01:00
Alireza Ahmadi 50d1177443 restart singbox button #328 2024-10-28 13:49:38 +01:00
Alireza Ahmadi d255905907 bulk client creation #285 2024-10-28 13:48:06 +01:00
Alireza Ahmadi 5f3963ff1c update go and dependencies 2024-10-22 23:59:54 +02:00
Alireza Ahmadi bc6f356789 upgrade deprecated grpc call in v2rayAPI 2024-10-22 23:59:11 +02:00
Alireza Ahmadi 119cff3d85 separated inbound's client mux #257 2024-10-22 23:55:37 +02:00
Alireza Ahmadi 90b2876867 fix typo 2024-10-22 22:20:44 +02:00
Alireza Ahmadi 5105c138f7 [ss] fix 128 bit shadowsocks password #208 2024-10-22 22:11:14 +02:00
Alireza Ahmadi b019633c3f [ssTLS] fix handshake port #267 2024-10-22 22:09:28 +02:00
Alireza Ahmadi 419cce250f Latest working docker compose file on docs 2024-10-08 00:08:08 +02:00
Alireza Ahmadi bdc458dfa9 Merge pull request #277 from TheyCallMeSecond/main
Update jsonService.go
2024-10-07 22:05:56 +02:00
Alireza Ahmadi 3dff49d6e4 Merge pull request #310 from alireza0/dependabot/npm_and_yarn/frontend/rollup-4.22.4
Bump rollup from 4.20.0 to 4.22.4 in /frontend
2024-10-03 11:04:46 +02:00
dependabot[bot] 47e3c6944a Bump rollup from 4.20.0 to 4.22.4 in /frontend
Bumps [rollup](https://github.com/rollup/rollup) from 4.20.0 to 4.22.4.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.20.0...v4.22.4)

---
updated-dependencies:
- dependency-name: rollup
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-24 03:42:42 +00:00
Alireza Ahmadi 54e48c8c76 Merge pull request #304 from alireza0/dependabot/npm_and_yarn/frontend/vite-5.4.6
Bump vite from 5.3.5 to 5.4.6 in /frontend
2024-09-21 23:05:05 +02:00
dependabot[bot] 9c7814a765 Bump vite from 5.3.5 to 5.4.6 in /frontend
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.3.5 to 5.4.6.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.6/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.6/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-17 20:04:08 +00:00
Alireza Ahmadi 6a174cf4db fix uninstall #287 2024-08-31 16:14:32 +02:00
Alireza Ahmadi 19e060ad33 fix override tls #273 2024-08-31 16:09:50 +02:00
Alireza Ahmadi 5c09bc011e fix outbound tls text #276 2024-08-31 14:18:15 +02:00
Alireza Ahmadi f6f90b07d3 Merge pull request #291 from alireza0/dependabot/npm_and_yarn/frontend/axios-1.7.4
Bump axios from 1.7.3 to 1.7.4 in /frontend
2024-08-31 13:52:00 +02:00
dependabot[bot] 2da30dc596 Bump axios from 1.7.3 to 1.7.4 in /frontend
Bumps [axios](https://github.com/axios/axios) from 1.7.3 to 1.7.4.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.7.3...v1.7.4)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-31 11:42:20 +00:00
Alexander Chepurnoy bdad92fe01 feat: add ru locale (#280)
* feat: add ru locale

* feat: add russian lang to readme
2024-08-31 13:29:01 +02:00
TheyCallMeSecond 5ddee6aa12 Update jsonService.go
add IPV6 traffic to the tunnel
2024-08-18 18:22:12 +03:30
Alireza Ahmadi 6f0df2d555 [client] add table and filter 2024-08-06 00:31:14 +02:00
Alireza Ahmadi 0bb3a67f79 add client group 2024-07-23 22:57:19 +02:00
Alireza Ahmadi 869c51885f fix vmess remark #198 2024-07-23 18:18:56 +02:00
Alireza Ahmadi 7b58edeaaf small fixes 2024-07-23 17:28:43 +02:00
Alireza Ahmadi e287ced0e4 fix tls change affect on inData 2024-07-23 17:27:51 +02:00
Alireza Ahmadi c518bf5a86 fix random ShortId 2024-07-23 17:26:50 +02:00
Alireza Ahmadi 89b85f818d fix reality in json-sub #224 2024-07-23 16:34:35 +02:00
Alireza Ahmadi ea3ad15b76 fix tls in new inbound 2024-07-23 16:25:22 +02:00
Alireza Ahmadi 5cc3791f79 fix api call error 2024-07-23 16:24:56 +02:00
Alireza Ahmadi eef7e200ba better link generator 2024-07-23 16:24:26 +02:00
Alireza Ahmadi 39022c1b2d fix typo in shadowTLS #199 2024-07-18 23:34:16 +02:00
Alireza Ahmadi 2b6874a58d better docker solution #176 2024-07-18 23:13:09 +02:00
Alireza Ahmadi 1631ac0c30 fix container logger #176 2024-07-18 23:12:31 +02:00
Alireza Ahmadi d70006cd91 independent migration 2024-07-18 23:10:49 +02:00
Alireza Ahmadi 19901efeaa fix sessions 2024-07-18 23:08:55 +02:00
Alireza Ahmadi b2d0134567 fix domain validator 2024-07-18 22:44:59 +02:00
Alireza Ahmadi 58f4a676b5 fix json marshal 2024-07-18 22:44:33 +02:00
Alireza Ahmadi 7904cb3db0 update packages 2024-07-18 22:42:55 +02:00
Alireza Ahmadi 6222533594 multi os/arch #72 2024-07-12 23:16:30 +02:00
Alireza Ahmadi cb4a7fe6df reload sing-box instead of restart 2024-07-07 23:37:03 +02:00
Alireza Ahmadi 96564f1f86 v1.0.0 2024-07-04 22:04:44 +02:00
Alireza Ahmadi cf6b61fe96 fix change detection 2024-07-04 11:29:46 +02:00
Alireza Ahmadi e1aaa3d748 fix sing-box client dns 2024-07-04 11:29:02 +02:00
Alireza Ahmadi 209561497a fix tls and ech auto generate 2024-07-01 00:30:13 +02:00
Alireza Ahmadi 60b374e5d4 upate readme: dev guide #111 2024-06-30 23:29:00 +02:00
Alireza Ahmadi ea8538148f update frontend 2024-06-30 23:28:00 +02:00
Alireza Ahmadi b3a2078ed6 client filter by opacity 2024-06-30 22:32:35 +02:00
Alireza Ahmadi f169064fbc small fixes 2024-06-30 22:32:19 +02:00
Alireza Ahmadi 7d441723ba update qr-code with singbox 2024-06-30 22:30:34 +02:00
Alireza Ahmadi a41140190f clone TLS 2024-06-30 22:29:44 +02:00
Alireza Ahmadi bb5cd91bc9 update translation 2024-06-30 22:29:30 +02:00
Alireza Ahmadi 5b6f6daaa8 add outbound by link #156 2024-06-28 18:17:45 +02:00
Alireza Ahmadi ba06ad598d fix reality utls #173 2024-06-28 16:09:42 +02:00
Alireza Ahmadi 69725ee5af option button better color 2024-06-28 15:59:22 +02:00
Alireza Ahmadi 0d36b811dc omit migration on first install #171 2024-06-28 15:58:15 +02:00
Alireza Ahmadi 6672a2721f subjson and multidomain 2024-06-28 15:55:37 +02:00
Alireza Ahmadi 6b24506ddd Merge pull request #165 from alireza0/dependabot/github_actions/docker/build-push-action-6
Bump docker/build-push-action from 5 to 6
2024-06-18 12:21:14 +02:00
dependabot[bot] 3298fd4e0d Bump docker/build-push-action from 5 to 6
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5 to 6.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-17 16:22:20 +00:00
Alireza Ahmadi 6dc7c93030 filter clients #69 2024-06-15 23:26:06 +02:00
Alireza Ahmadi 7c127f07bb small changes 2024-06-15 23:22:23 +02:00
Alireza Ahmadi b5a2dd18f5 add dns resolver #159 2024-06-15 22:10:58 +02:00
Alireza Ahmadi 53ed86c373 better traffic chart #120 2024-06-15 21:53:45 +02:00
Alireza Ahmadi ccbd591b39 cn locales to vuetify standard 2024-06-15 20:11:39 +02:00
Alireza Ahmadi f5792c9d82 show/reset client traffics #129 2024-06-15 12:45:13 +02:00
Alireza Ahmadi 4a2ac30a95 Merge pull request #161 from leic4u/patch-1
Update zhcn.ts
2024-06-15 10:32:48 +02:00
Alireza Ahmadi 45b03d8472 move notification to bottom 2024-06-14 01:05:44 +02:00
Alireza Ahmadi db3270feaa small fixes 2024-06-13 20:16:29 +02:00
Alireza Ahmadi b144aecb6a auto generate cert keys 2024-06-13 20:16:04 +02:00
leic4u 474e5156bb Update zhcn.ts
Updated Simplified Chinese translation
2024-06-13 00:44:50 +08:00
Alireza Ahmadi d057076251 fix client inbounds #154 2024-06-10 17:19:32 +02:00
Alireza Ahmadi 29cc6fd3e3 using migration in docker #153 2024-06-10 16:33:55 +02:00
Alireza Ahmadi dd0f770c1a v0.0.5 2024-06-09 23:03:03 +02:00
Alireza Ahmadi 8e369535bf add migration to install script 2024-06-09 22:51:05 +02:00
Alireza Ahmadi 6a5e0a940b admin page enhancements 2024-06-09 22:48:49 +02:00
Alireza Ahmadi 3b24819309 change snackbar to nitivue 2024-06-09 22:48:32 +02:00
Alireza Ahmadi b5920cdc07 small fixes 2024-06-08 20:55:55 +02:00
Alireza Ahmadi cf620962bb alert on core error 2024-06-08 20:55:42 +02:00
Alireza Ahmadi 17f1126c23 [out] remove proxy from direct 2024-06-08 19:38:24 +02:00
Alireza Ahmadi 16203fdece update frontend 2024-06-08 17:51:24 +02:00
Alireza Ahmadi 12fe21906e show changes feature 2024-06-08 17:49:03 +02:00
Alireza Ahmadi 1b9d5e9378 update + translate log modal 2024-06-08 17:47:32 +02:00
Alireza Ahmadi c152a977c6 fix QrCode 2024-06-08 17:44:22 +02:00
Alireza Ahmadi 341baf69de [log] display logs 2024-06-06 23:01:48 +02:00
Alireza Ahmadi dedd4b3ee3 fix tls modal initiate 2024-06-06 22:08:33 +02:00
Alireza Ahmadi bfbf9777e9 db migration 2024-06-06 22:08:13 +02:00
Alireza Ahmadi 2cabf0aefb [refactor] string values to json 2024-06-06 22:07:26 +02:00
Alireza Ahmadi c994f4b24a add tls 2024-06-06 08:24:08 +02:00
Alireza Ahmadi f136229539 Create FUNDING.yml 2024-06-01 23:54:39 +02:00
Alireza Ahmadi 40fbb22b74 fix transmision in user links 2024-05-29 23:29:43 +02:00
Alireza Ahmadi 9547038164 avoid db check in updates 2024-05-29 23:27:21 +02:00
Alireza Ahmadi aca870e78f fix toggle enable client 2024-05-26 11:31:25 +02:00
Alireza Ahmadi dbf01c2086 fix edit client name #140 2024-05-26 11:31:05 +02:00
Alireza Ahmadi c3debcec5a restart core after crach #132 2024-05-26 10:35:08 +02:00
Alireza Ahmadi c179bf8a37 fix copy to clipboard #132 2024-05-26 10:32:58 +02:00
Alireza Ahmadi 21add1f3ce v0.0.4 2024-05-23 18:52:55 +02:00
Alireza Ahmadi 9968f3885f small fixes 2024-05-23 18:38:50 +02:00
Alireza Ahmadi 2ac13ef8f4 sing-box v1.8.14 2024-05-23 12:01:36 +02:00
Alireza Ahmadi 4900c14295 fix session key 2024-05-23 12:01:04 +02:00
Alireza Ahmadi 55a6d78114 avoid duplicate api call 2024-05-23 12:00:37 +02:00
Alireza Ahmadi caa115bbe3 http transmition interoperability with xray links 2024-05-23 12:00:13 +02:00
Alireza Ahmadi e3be3be9d9 fix users config in non-user based protocols 2024-05-23 11:59:28 +02:00
Alireza Ahmadi 988675a7a7 fix editing in/out tag 2024-05-23 11:57:51 +02:00
Alireza Ahmadi 458f0c20da fix numbers in settings 2024-05-23 11:56:55 +02:00
Alireza Ahmadi f8fbc3c329 fix typo in outbound port 2024-05-23 11:56:31 +02:00
Alireza Ahmadi 89bc3b5b23 fix gauge jumping on update 2024-05-23 11:55:33 +02:00
Alireza Ahmadi edfe0c86e7 [hy2] optional masquerade 2024-05-23 11:55:05 +02:00
Alireza Ahmadi 6865c8b49d fix panel ssl config 2024-05-23 11:54:27 +02:00
Alireza Ahmadi 07947c9665 update frontend 2024-05-23 11:53:27 +02:00
Alireza Ahmadi 09616b6fac update github workflow 2024-05-22 17:51:50 +02:00
Alireza Ahmadi 15105710bc update docker 2024-05-22 17:51:18 +02:00
147 changed files with 12567 additions and 6005 deletions
+1
View File
@@ -0,0 +1 @@
buy_me_a_coffee: alireza7
-57
View File
@@ -1,57 +0,0 @@
name: Sing-box Docker Image CI
on:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Get latest release
id: get_release
run: |
latest_release=$(curl -Ls "https://api.github.com/repos/sagernet/sing-box/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
echo "latest_release: $latest_release"
echo "latest_release=$latest_release" >> $GITHUB_OUTPUT
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
alireza7/s-ui-singbox
ghcr.io/alireza0/s-ui-singbox
tags: |
type=sha
type=pep440,pattern=${{ steps.get_release.outputs.latest_release }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: core/
push: true
build-args: SINGBOX_VER=${{ steps.get_release.outputs.latest_release }}
platforms: linux/amd64,linux/arm64/v8,linux/arm/v7,linux/386
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+2 -2
View File
@@ -44,10 +44,10 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v5 uses: docker/build-push-action@v6
with: with:
context: . context: .
push: true push: true
platforms: linux/amd64,linux/arm64/v8,linux/arm/v7,linux/386 platforms: linux/amd64, linux/arm64/v8, linux/arm/v7, linux/arm/v6, linux/386
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
+26 -22
View File
@@ -14,7 +14,10 @@ jobs:
- amd64 - amd64
- arm64 - arm64
- armv7 - armv7
- armv6
- armv5
- 386 - 386
- s390x
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- name: Checkout repository - name: Checkout repository
@@ -24,23 +27,29 @@ jobs:
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
cache: false cache: false
go-version: '1.22' go-version-file: backend/go.mod
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: '20' node-version: '22'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
- name: Install dependencies - name: Install dependencies
run: | run: |
sudo apt-get update && sudo apt-get install upx -yq sudo apt-get update
if [ "${{ matrix.platform }}" == "arm64" ]; then if [ "${{ matrix.platform }}" == "arm64" ]; then
sudo apt install gcc-aarch64-linux-gnu sudo apt install gcc-aarch64-linux-gnu
elif [ "${{ matrix.platform }}" == "armv7" ]; then elif [ "${{ matrix.platform }}" == "armv7" ]; then
sudo apt install gcc-arm-linux-gnueabihf sudo apt install gcc-arm-linux-gnueabihf
elif [ "${{ matrix.platform }}" == "armv6" ]; then
sudo apt install gcc-arm-linux-gnueabihf
elif [ "${{ matrix.platform }}" == "armv5" ]; then
sudo apt install gcc-arm-linux-gnueabi
elif [ "${{ matrix.platform }}" == "386" ]; then elif [ "${{ matrix.platform }}" == "386" ]; then
sudo apt install gcc-i686-linux-gnu sudo apt install gcc-i686-linux-gnu
elif [ "${{ matrix.platform }}" == "s390x" ]; then
sudo apt install gcc-s390x-linux-gnu
fi fi
- name: Build frontend - name: Build frontend
@@ -51,7 +60,7 @@ jobs:
cd .. cd ..
mv frontend/dist backend/web/html mv frontend/dist backend/web/html
- name: Build s-ui & singbox - name: Build s-ui
run: | run: |
export CGO_ENABLED=1 export CGO_ENABLED=1
export GOOS=linux export GOOS=linux
@@ -63,36 +72,31 @@ jobs:
export GOARCH=arm export GOARCH=arm
export GOARM=7 export GOARM=7
export CC=arm-linux-gnueabihf-gcc export CC=arm-linux-gnueabihf-gcc
elif [ "${{ matrix.platform }}" == "armv6" ]; then
export GOARCH=arm
export GOARM=6
export CC=arm-linux-gnueabihf-gcc
elif [ "${{ matrix.platform }}" == "armv5" ]; then
export GOARCH=arm
export GOARM=5
export CC=arm-linux-gnueabi-gcc
elif [ "${{ matrix.platform }}" == "386" ]; then elif [ "${{ matrix.platform }}" == "386" ]; then
export GOARCH=386 export GOARCH=386
export CC=i686-linux-gnu-gcc export CC=i686-linux-gnu-gcc
elif [ "${{ matrix.platform }}" == "s390x" ]; then
export GOARCH=s390x
export CC=s390x-linux-gnu-gcc
fi fi
#### Build Sing-Box
git clone -b v1.8.13 https://github.com/SagerNet/sing-box
cd sing-box
go build -v -gcflags=all="-l -B -C" -mod=mod -trimpath \
-ldflags "-s -w -buildid= -extldflags '-static'" -a \
-tags='netgo osusergo static_build with_quic with_grpc with_wireguard with_ech with_utls with_reality_server with_acme with_v2ray_api with_clash_api with_gvisor' \
-o sing-box ./cmd/sing-box
upx --ultra-brute -9 -v --lzma --best --force sing-box
cd ..
### Build s-ui ### Build s-ui
cd backend cd backend
go build -v -gcflags=all="-l -B -C" -mod=mod -trimpath \ go build -ldflags="-w -s" -tags "with_quic,with_grpc,with_ech,with_utls,with_reality_server,with_acme,with_gvisor" -o ../sui main.go
-ldflags "-s -w -buildid= -extldflags '-static'" -a -tags='netgo osusergo static_build sqlite_omit_load_extension' \
-o ../sui main.go
cd .. cd ..
upx --ultra-brute -9 -v --lzma --best --force sui
mkdir s-ui mkdir s-ui
cp sui s-ui/ cp sui s-ui/
cp s-ui.service s-ui/ cp s-ui.service s-ui/
cp sing-box.service s-ui/ cp s-ui.sh s-ui/
mkdir s-ui/bin
cp sing-box/sing-box s-ui/bin/
cp core/runSingbox.sh s-ui/bin/
- name: Package - name: Package
run: tar -zcvf s-ui-linux-${{ matrix.platform }}.tar.gz s-ui run: tar -zcvf s-ui-linux-${{ matrix.platform }}.tar.gz s-ui
+8 -6
View File
@@ -1,23 +1,25 @@
FROM --platform=$BUILDPLATFORM node:alpine as front-builder FROM --platform=$BUILDPLATFORM node:alpine AS front-builder
WORKDIR /app WORKDIR /app
COPY frontend/ ./ COPY frontend/ ./
RUN npm install && npm run build RUN npm install && npm run build
FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS backend-builder FROM golang:1.23-alpine AS backend-builder
WORKDIR /app WORKDIR /app
ARG TARGETARCH ARG TARGETARCH
ENV CGO_CFLAGS="-D_LARGEFILE64_SOURCE" ENV CGO_CFLAGS="-D_LARGEFILE64_SOURCE"
ENV CGO_ENABLED=1 ENV CGO_ENABLED=1
RUN apk --no-cache --update add build-base gcc wget unzip ENV GOARCH=$TARGETARCH
RUN apk update && apk --no-cache --update add build-base gcc wget unzip
COPY backend/ ./ COPY backend/ ./
COPY --from=front-builder /app/dist/ /app/web/html/ COPY --from=front-builder /app/dist/ /app/web/html/
RUN go build -o sui main.go RUN go build -ldflags="-w -s" -tags "with_quic,with_grpc,with_ech,with_utls,with_reality_server,with_acme,with_gvisor" -o sui main.go
FROM --platform=$BUILDPLATFORM alpine FROM --platform=$TARGETPLATFORM alpine
LABEL org.opencontainers.image.authors="alireza7@gmail.com" LABEL org.opencontainers.image.authors="alireza7@gmail.com"
ENV TZ=Asia/Tehran ENV TZ=Asia/Tehran
WORKDIR /app WORKDIR /app
RUN apk add --no-cache --update ca-certificates tzdata RUN apk add --no-cache --update ca-certificates tzdata
COPY --from=backend-builder /app/sui /app/ COPY --from=backend-builder /app/sui /app/
COPY entrypoint.sh /app/
VOLUME [ "s-ui" ] VOLUME [ "s-ui" ]
CMD [ "./sui" ] ENTRYPOINT [ "./entrypoint.sh" ]
+83 -10
View File
@@ -23,16 +23,16 @@
| Multi-Client/Inbound | :heavy_check_mark: | | Multi-Client/Inbound | :heavy_check_mark: |
| Advanced Traffic Routing Interface | :heavy_check_mark: | | Advanced Traffic Routing Interface | :heavy_check_mark: |
| Client & Traffic & System Status | :heavy_check_mark: | | Client & Traffic & System Status | :heavy_check_mark: |
| Subscription Service (link + info) | :heavy_check_mark: | | Subscription Service (link/json + info)| :heavy_check_mark: |
| Dark/Light Theme | :heavy_check_mark: | | Dark/Light Theme | :heavy_check_mark: |
## Default Installation Informarion ## Default Installation Information
- Panel Port: 2095 - Panel Port: 2095
- Panel Path: /app/ - Panel Path: /app/
- Subscription Port: 2096 - Subscription Port: 2096
- Subscription Path: /sub/ - Subscription Path: /sub/
- User/Passowrd: admin - User/Password: admin
## Install & Upgrade to Latest Version ## Install & Upgrade to Latest Version
@@ -40,17 +40,29 @@
bash <(curl -Ls https://raw.githubusercontent.com/alireza0/s-ui/master/install.sh) bash <(curl -Ls https://raw.githubusercontent.com/alireza0/s-ui/master/install.sh)
``` ```
## Install Custom Version ## Install legacy Version
**Step 1:** To install your desired version, add the version to the end of the installation command. e.g., ver `0.0.1`: **Step 1:** To install your desired legacy version, add the version to the end of the installation command. e.g., ver `1.0.0`:
```sh ```sh
bash <(curl -Ls https://raw.githubusercontent.com/alireza0/s-ui/master/install.sh) 0.0.1 VERSION=1.0.0 && bash <(curl -Ls https://raw.githubusercontent.com/alireza0/s-ui/$VERSION/install.sh) $VERSION
``` ```
## Manual installation
1. Get the latest version of S-UI based on your OS/Architecture from GitHub: [https://github.com/alireza0/s-ui/releases/latest](https://github.com/alireza0/s-ui/releases/latest)
2. **OPTIONAL** Get the latest version of `s-ui.sh` [https://raw.githubusercontent.com/alireza0/s-ui/master/s-ui.sh](https://raw.githubusercontent.com/alireza0/s-ui/master/s-ui.sh)
3. **OPTIONAL** Copy `s-ui.sh` to /usr/bin/ and run `chmod +x /usr/bin/s-ui`.
4. Extract s-ui tar.gz file to a directory of your choice and navigate to the directory where you extracted the tar.gz file.
5. Copy *.service files to /etc/systemd/system/ and run `systemctl daemon-reload`.
6. Enable autostart and start S-UI service using `systemctl enable s-ui --now`
7. Start sing-box service using `systemctl enable sing-box --now`
## Uninstall S-UI ## Uninstall S-UI
```sh ```sh
sudo -i
systemctl disable sing-box --now systemctl disable sing-box --now
systemctl disable s-ui --now systemctl disable s-ui --now
@@ -59,6 +71,8 @@ rm -f /etc/systemd/system/sing-box.service
systemctl daemon-reload systemctl daemon-reload
rm -fr /usr/local/s-ui rm -fr /usr/local/s-ui
rm /usr/bin/s-ui
``` ```
## Install using Docker ## Install using Docker
@@ -80,7 +94,7 @@ curl -fsSL https://get.docker.com | sh
```shell ```shell
mkdir s-ui && cd s-ui mkdir s-ui && cd s-ui
wget -q https://raw.githubusercontent.com/alireza0/s-ui/main/docker-compose.yml wget -q https://raw.githubusercontent.com/alireza0/s-ui/master/docker-compose.yml
docker compose up -d docker compose up -d
``` ```
@@ -104,6 +118,56 @@ docker build -t s-ui .
</details> </details>
## Manual run ( contribution )
<details>
<summary>Click for details</summary>
### Build and run whole project
```shell
./runSUI.sh
```
### - Frontend
Frontend codes are in `frontend` folder in the root of repository.
To run it locally for instant development you can use (apply automatic changes on file save):
```shell
cd frontend
npm run dev
```
> By this command it will run a `vite` web server on separate port `3000`, with backend proxy to `http://localhost:2095`. You can change it in `frontend/vite.config.mts`.
To build frontend:
```shell
cd frontend
npm run build
```
### - Backend
Backend codes are in `backend` folder in the root of repository.
> Please build frontend once before!
To build backend:
```shell
cd backend
# remove old frontend compiled files
rm -fr web/html/*
# apply new frontend compiled files
cp -R ../frontend/dist/ web/html/
# build
go build -o ../sui main.go
```
To run backend (from root folder of repository):
```shell
./sui
```
</details>
## Languages ## Languages
- English - English
@@ -111,6 +175,7 @@ docker build -t s-ui .
- Vietnamese - Vietnamese
- Chinese (Simplified) - Chinese (Simplified)
- Chinese (Traditional) - Chinese (Traditional)
- Russian
## Features ## Features
@@ -129,10 +194,18 @@ docker build -t s-ui .
## Recommended OS ## Recommended OS
- Ubuntu 20.04+
- Debian 11+
- CentOS 8+ - CentOS 8+
- Ubuntu 20+
- Debian 10+
- Fedora 36+ - Fedora 36+
- Arch Linux
- Parch Linux
- Manjaro
- Armbian
- AlmaLinux 9+
- Rocky Linux 9+
- Oracle Linux 8+
- OpenSUSE Tubleweed
## Environment Variables ## Environment Variables
@@ -169,4 +242,4 @@ certbot certonly --standalone --register-unsafely-without-email --non-interactiv
</details> </details>
## Stargazers over Time ## Stargazers over Time
[![Stargazers over time](https://starchart.cc/alireza0/s-ui.svg?variant=adaptive)](https://starchart.cc/alireza0/s-ui) [![Stargazers over time](https://starchart.cc/alireza0/s-ui.svg)](https://starchart.cc/alireza0/s-ui)
+149 -23
View File
@@ -1,9 +1,11 @@
package api package api
import ( import (
"fmt" "encoding/json"
"s-ui/logger" "s-ui/logger"
"s-ui/service" "s-ui/service"
"s-ui/util"
"s-ui/util/common"
"strconv" "strconv"
"strings" "strings"
@@ -15,6 +17,10 @@ type APIHandler struct {
service.UserService service.UserService
service.ConfigService service.ConfigService
service.ClientService service.ClientService
service.TlsService
service.InboundService
service.OutboundService
service.EndpointService
service.PanelService service.PanelService
service.StatsService service.StatsService
service.ServerService service.ServerService
@@ -41,6 +47,8 @@ func (a *APIHandler) postHandler(c *gin.Context) {
var err error var err error
action := c.Param("postAction") action := c.Param("postAction")
remoteIP := getRemoteIp(c) remoteIP := getRemoteIp(c)
loginUser := GetLoginUser(c)
hostname := getHostname(c)
switch action { switch action {
case "login": case "login":
@@ -55,14 +63,7 @@ func (a *APIHandler) postHandler(c *gin.Context) {
logger.Infof("Unable to get session's max age from DB") logger.Infof("Unable to get session's max age from DB")
} }
if sessionMaxAge > 0 { err = SetLoginUser(c, loginUser, sessionMaxAge)
err = SetMaxAge(c, sessionMaxAge*60)
if err != nil {
logger.Infof("Unable to set session's max age")
}
}
err = SetLoginUser(c, loginUser)
if err == nil { if err == nil {
logger.Info("user ", loginUser, " login success") logger.Info("user ", loginUser, " login success")
} else { } else {
@@ -84,18 +85,31 @@ func (a *APIHandler) postHandler(c *gin.Context) {
jsonMsg(c, "", err) jsonMsg(c, "", err)
} }
case "save": case "save":
loginUser := GetLoginUser(c) obj := c.Request.FormValue("object")
data := map[string]string{} act := c.Request.FormValue("action")
err = c.ShouldBind(&data) data := c.Request.FormValue("data")
if err == nil { objs, err := a.ConfigService.Save(obj, act, json.RawMessage(data), loginUser, hostname)
err = a.ConfigService.SaveChanges(data, loginUser) if err != nil {
jsonMsg(c, "save", err)
return
} }
jsonMsg(c, "save", err) err = a.loadPartialData(c, objs)
if err != nil {
jsonMsg(c, obj, err)
}
return
case "restartApp": case "restartApp":
err = a.PanelService.RestartPanel(3) err = a.PanelService.RestartPanel(3)
jsonMsg(c, "restartApp", err) jsonMsg(c, "restartApp", err)
case "restartSb":
err = a.ConfigService.RestartCore()
jsonMsg(c, "restartSb", err)
case "linkConvert":
link := c.Request.FormValue("link")
result, _, err := util.GetOutbound(link, 0)
jsonObj(c, result, err)
default: default:
jsonMsg(c, "API call", nil) jsonMsg(c, "failed", common.NewError("unknown action: ", action))
} }
} }
@@ -117,6 +131,12 @@ func (a *APIHandler) getHandler(c *gin.Context) {
return return
} }
jsonObj(c, data, nil) jsonObj(c, data, nil)
case "inbounds", "outbounds", "endpoints", "tls", "clients", "config":
err := a.loadPartialData(c, []string{action})
if err != nil {
jsonMsg(c, action, err)
}
return
case "users": case "users":
users, err := a.UserService.GetUsers() users, err := a.UserService.GetUsers()
if err != nil { if err != nil {
@@ -151,24 +171,49 @@ func (a *APIHandler) getHandler(c *gin.Context) {
case "onlines": case "onlines":
onlines, err := a.StatsService.GetOnlines() onlines, err := a.StatsService.GetOnlines()
jsonObj(c, onlines, err) jsonObj(c, onlines, err)
case "logs":
count := c.Query("c")
level := c.Query("l")
logs := a.ServerService.GetLogs(count, level)
jsonObj(c, logs, nil)
case "changes":
actor := c.Query("a")
chngKey := c.Query("k")
count := c.Query("c")
changes := a.ConfigService.GetChanges(actor, chngKey, count)
jsonObj(c, changes, nil)
case "keypairs":
kType := c.Query("k")
options := c.Query("o")
keypair := a.ServerService.GenKeypair(kType, options)
jsonObj(c, keypair, nil)
default: default:
jsonMsg(c, "API call", nil) jsonMsg(c, "failed", common.NewError("unknown action: ", action))
} }
} }
func (a *APIHandler) loadData(c *gin.Context) (string, error) { func (a *APIHandler) loadData(c *gin.Context) (interface{}, error) {
var data string data := make(map[string]interface{}, 0)
lu := c.Query("lu") lu := c.Query("lu")
isUpdated, err := a.ConfigService.CheckChnages(lu) isUpdated, err := a.ConfigService.CheckChanges(lu)
if err != nil { if err != nil {
return "", err return "", err
} }
onlines, err := a.StatsService.GetOnlines() onlines, err := a.StatsService.GetOnlines()
sysInfo := a.ServerService.GetSingboxInfo()
if sysInfo["running"] == false {
logs := a.ServerService.GetLogs("1", "debug")
if len(logs) > 0 {
data["lastLog"] = logs[0]
}
}
if err != nil { if err != nil {
return "", err return "", err
} }
if isUpdated { if isUpdated {
config, err := a.ConfigService.GetConfig() config, err := a.SettingService.GetConfig()
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -176,14 +221,95 @@ func (a *APIHandler) loadData(c *gin.Context) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
tlsConfigs, err := a.TlsService.GetAll()
if err != nil {
return "", err
}
inbounds, err := a.InboundService.GetAll()
if err != nil {
return "", err
}
outbounds, err := a.OutboundService.GetAll()
if err != nil {
return "", err
}
endpoints, err := a.EndpointService.GetAll()
if err != nil {
return "", err
}
subURI, err := a.SettingService.GetFinalSubURI(strings.Split(c.Request.Host, ":")[0]) subURI, err := a.SettingService.GetFinalSubURI(strings.Split(c.Request.Host, ":")[0])
if err != nil { if err != nil {
return "", err return "", err
} }
data = fmt.Sprintf(`{"config": %s,"clients": %s,"subURI": "%s", "onlines": %s}`, string(*config), clients, subURI, onlines) data["config"] = json.RawMessage(config)
data["clients"] = clients
data["tls"] = tlsConfigs
data["inbounds"] = inbounds
data["outbounds"] = outbounds
data["endpoints"] = endpoints
data["subURI"] = subURI
data["onlines"] = onlines
} else { } else {
data = fmt.Sprintf(`{"onlines": %s}`, onlines) data["onlines"] = onlines
} }
return data, nil return data, nil
} }
func (a *APIHandler) loadPartialData(c *gin.Context, objs []string) error {
data := make(map[string]interface{}, 0)
for _, obj := range objs {
switch obj {
case "inbounds":
id := c.Query("id")
inbounds, err := a.InboundService.Get(id)
if err != nil {
return err
}
data[obj] = inbounds
case "outbounds":
outbounds, err := a.OutboundService.GetAll()
if err != nil {
return err
}
data[obj] = outbounds
case "endpoints":
endpoints, err := a.EndpointService.GetAll()
if err != nil {
return err
}
data[obj] = endpoints
case "tls":
tlsConfigs, err := a.TlsService.GetAll()
if err != nil {
return err
}
data[obj] = tlsConfigs
case "clients":
clients, err := a.ClientService.GetAll()
if err != nil {
return err
}
data[obj] = clients
case "config":
config, err := a.SettingService.GetConfig()
if err != nil {
return err
}
data[obj] = json.RawMessage(config)
}
}
jsonObj(c, data, nil)
return nil
}
func (a *APIHandler) postActions(c *gin.Context) (string, json.RawMessage, error) {
var data map[string]json.RawMessage
err := c.ShouldBind(&data)
if err != nil {
return "", nil, err
}
return string(data["action"]), data["data"], nil
}
+14 -5
View File
@@ -4,7 +4,7 @@ import (
"encoding/gob" "encoding/gob"
"s-ui/database/model" "s-ui/database/model"
sessions "github.com/Calidity/gin-sessions" "github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -16,17 +16,26 @@ func init() {
gob.Register(model.User{}) gob.Register(model.User{})
} }
func SetLoginUser(c *gin.Context, userName string) error { func SetLoginUser(c *gin.Context, userName string, maxAge int) error {
options := sessions.Options{
Path: "/",
Secure: false,
}
if maxAge > 0 {
options.MaxAge = maxAge * 60
}
s := sessions.Default(c) s := sessions.Default(c)
s.Set(loginUser, userName) s.Set(loginUser, userName)
s.Options(options)
return s.Save() return s.Save()
} }
func SetMaxAge(c *gin.Context, maxAge int) error { func SetMaxAge(c *gin.Context) error {
s := sessions.Default(c) s := sessions.Default(c)
s.Options(sessions.Options{ s.Options(sessions.Options{
Path: "/", Path: "/",
MaxAge: maxAge,
}) })
return s.Save() return s.Save()
} }
+9 -1
View File
@@ -27,6 +27,14 @@ func getRemoteIp(c *gin.Context) string {
} }
} }
func getHostname(c *gin.Context) string {
host := c.Request.Host
if colonIndex := strings.LastIndex(host, ":"); colonIndex != -1 {
host, _, _ = net.SplitHostPort(c.Request.Host)
}
return host
}
func jsonMsg(c *gin.Context, msg string, err error) { func jsonMsg(c *gin.Context, msg string, err error) {
jsonMsgObj(c, msg, nil, err) jsonMsgObj(c, msg, nil, err)
} }
@@ -46,7 +54,7 @@ func jsonMsgObj(c *gin.Context, msg string, obj interface{}, err error) {
} }
} else { } else {
m.Success = false m.Success = false
m.Msg = msg + err.Error() m.Msg = msg + ": " + err.Error()
logger.Warning("failed :", err) logger.Warning("failed :", err)
} }
c.JSON(http.StatusOK, m) c.JSON(http.StatusOK, m)
+27 -8
View File
@@ -3,6 +3,7 @@ package app
import ( import (
"log" "log"
"s-ui/config" "s-ui/config"
"s-ui/core"
"s-ui/cronjob" "s-ui/cronjob"
"s-ui/database" "s-ui/database"
"s-ui/logger" "s-ui/logger"
@@ -15,9 +16,12 @@ import (
type APP struct { type APP struct {
service.SettingService service.SettingService
webServer *web.Server configService *service.ConfigService
subServer *sub.Server webServer *web.Server
cronJob *cronjob.CronJob subServer *sub.Server
cronJob *cronjob.CronJob
logger *logging.Logger
core *core.Core
} }
func NewApp() *APP { func NewApp() *APP {
@@ -34,15 +38,17 @@ func (a *APP) Init() error {
return err return err
} }
// Init Setting
a.SettingService.GetAllSetting()
a.core = core.NewCore()
a.cronJob = cronjob.NewCronJob() a.cronJob = cronjob.NewCronJob()
a.webServer = web.NewServer() a.webServer = web.NewServer()
a.subServer = sub.NewServer() a.subServer = sub.NewServer()
configService := service.NewConfigService() a.configService = service.NewConfigService(a.core)
err = configService.InitConfig()
if err != nil {
return err
}
return nil return nil
} }
@@ -72,6 +78,11 @@ func (a *APP) Start() error {
return err return err
} }
err = a.configService.StartCore("")
if err != nil {
logger.Error(err)
}
return nil return nil
} }
@@ -85,6 +96,10 @@ func (a *APP) Stop() {
if err != nil { if err != nil {
logger.Warning("stop Web Server err:", err) logger.Warning("stop Web Server err:", err)
} }
err = a.configService.StopCore()
if err != nil {
logger.Warning("stop Core err:", err)
}
} }
func (a *APP) initLog() { func (a *APP) initLog() {
@@ -106,3 +121,7 @@ func (a *APP) RestartApp() {
a.Stop() a.Stop()
a.Start() a.Start()
} }
func (a *APP) GetCore() *core.Core {
return a.core
}
+9
View File
@@ -4,6 +4,7 @@ import (
"flag" "flag"
"fmt" "fmt"
"os" "os"
"s-ui/cmd/migration"
"s-ui/config" "s-ui/config"
) )
@@ -40,6 +41,8 @@ func ParseCmd() {
fmt.Println() fmt.Println()
fmt.Println("Commands:") fmt.Println("Commands:")
fmt.Println(" admin set/reset/show first admin credentials") fmt.Println(" admin set/reset/show first admin credentials")
fmt.Println(" uri Show panel URI")
fmt.Println(" migrate migrate form older version")
fmt.Println(" setting set/reset/show settings") fmt.Println(" setting set/reset/show settings")
fmt.Println() fmt.Println()
adminCmd.Usage() adminCmd.Usage()
@@ -70,6 +73,12 @@ func ParseCmd() {
showAdmin() showAdmin()
} }
case "uri":
getPanelURI()
case "migrate":
migration.MigrateDb()
case "setting": case "setting":
err := settingCmd.Parse(os.Args[2:]) err := settingCmd.Parse(os.Args[2:])
if err != nil { if err != nil {
+79
View File
@@ -0,0 +1,79 @@
package migration
import (
"encoding/json"
"fmt"
"s-ui/database/model"
"strings"
"gorm.io/gorm"
)
func migrateClientSchema(db *gorm.DB) error {
rows, err := db.Raw("PRAGMA table_info(clients)").Rows()
if err != nil {
fmt.Println(err)
return err
}
defer rows.Close()
for rows.Next() {
var (
cid int
cname string
ctype string
notnull int
dfltValue interface{}
pk int
)
rows.Scan(&cid, &cname, &ctype, &notnull, &dfltValue, &pk)
if cname == "config" || cname == "inbounds" || cname == "links" {
if ctype == "text" {
fmt.Printf("Column %s has type TEXT\n", cname)
oldData := make([]struct {
Id uint
Data string
}, 0)
db.Model(model.Client{}).Select("id", cname+" as data").Scan(&oldData)
for _, data := range oldData {
var newData []byte
switch cname {
case "inbounds":
inbounds := strings.Split(data.Data, ",")
newData, _ = json.MarshalIndent(inbounds, "", " ")
case "config":
jsonData := map[string]interface{}{}
json.Unmarshal([]byte(data.Data), &jsonData)
newData, _ = json.MarshalIndent(jsonData, "", " ")
case "links":
jsonData := make([]interface{}, 0)
json.Unmarshal([]byte(data.Data), &jsonData)
newData, _ = json.MarshalIndent(jsonData, "", " ")
}
err = db.Model(model.Client{}).Where("id = ?", data.Id).UpdateColumn(cname, newData).Error
if err != nil {
return err
}
}
}
}
}
return nil
}
func changesObj(db *gorm.DB) error {
return db.Exec("UPDATE changes SET obj = CAST('\"' || CAST(obj AS TEXT) || '\"' AS BLOB) WHERE actor = ? and obj not like ?", "DepleteJob", "\"%\"").Error
}
func to1_1(db *gorm.DB) error {
err := migrateClientSchema(db)
if err != nil {
return err
}
err = changesObj(db)
if err != nil {
return err
}
return nil
}
+293
View File
@@ -0,0 +1,293 @@
package migration
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"s-ui/database/model"
"gorm.io/gorm"
)
type InboundData struct {
Id uint
Tag string
Addrs json.RawMessage
OutJson json.RawMessage
}
func moveJsonToDb(db *gorm.DB) error {
binFolderPath := os.Getenv("SUI_BIN_FOLDER")
if binFolderPath == "" {
binFolderPath = "bin"
}
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
if err != nil {
return err
}
configPath := dir + "/" + binFolderPath + "/config.json"
if _, err := os.Stat(configPath); errors.Is(err, os.ErrNotExist) {
return nil
}
data, err := os.ReadFile(configPath)
if err != nil {
return err
}
var oldConfig map[string]interface{}
err = json.Unmarshal(data, &oldConfig)
if err != nil {
return err
}
oldInbounds := oldConfig["inbounds"].([]interface{})
db.Migrator().DropTable(&model.Inbound{})
db.AutoMigrate(&model.Inbound{})
for _, inbound := range oldInbounds {
inbObj, _ := inbound.(map[string]interface{})
tag, _ := inbObj["tag"].(string)
if tlsObj, ok := inbObj["tls"]; ok {
var tls_id uint
err = db.Raw("SELECT id FROM tls WHERE inbounds like ?", `%"`+tag+`"%`).Find(&tls_id).Error
if err != nil {
return err
}
// Bind or Create tls_id
if tls_id > 0 {
inbObj["tls_id"] = tls_id
} else {
tls_server, _ := json.MarshalIndent(tlsObj, "", " ")
if len(tls_server) > 5 {
tlsObject := tlsObj.(map[string]interface{})
tlsClientObj := map[string]interface{}{}
if enabled, ok := tlsObject["enabled"]; ok {
tlsClientObj["enabled"] = enabled
}
if alpn, ok := tlsObject["alpn"]; ok {
tlsClientObj["alpn"] = alpn
}
if sni, ok := tlsObject["server_name"]; ok {
tlsClientObj["server_name"] = sni
}
tls_client, _ := json.MarshalIndent(tlsClientObj, "", " ")
newTls := &model.Tls{
Name: tag,
Server: tls_server,
Client: tls_client,
}
err = db.Create(newTls).Error
if err != nil {
return err
}
inbObj["tls_id"] = newTls.Id
}
}
}
var inbData InboundData
db.Raw("select id,addrs,out_json from inbound_data where tag = ?", tag).Find(&inbData)
if inbData.Id > 0 {
inbObj["out_json"] = inbData.OutJson
var addrs []map[string]interface{}
json.Unmarshal(inbData.Addrs, &addrs)
for index, addr := range addrs {
if tlsEnable, ok := addr["tls"].(bool); ok {
newTls := map[string]interface{}{
"enabled": tlsEnable,
}
if insecure, ok := addr["insecure"].(bool); ok {
newTls["insecure"] = insecure
delete(addrs[index], "insecure")
}
if sni, ok := addr["server_name"].(string); ok {
newTls["server_name"] = sni
delete(addrs[index], "server_name")
}
addrs[index]["tls"] = newTls
}
}
inbObj["addrs"] = addrs
} else {
inbObj["out_json"] = json.RawMessage("{}")
inbObj["addrs"] = json.RawMessage("[]")
}
// Delete deprecated fields
delete(inbObj, "sniff")
delete(inbObj, "sniff_override_destination")
delete(inbObj, "sniff_timeout")
delete(inbObj, "domain_strategy")
inbJson, _ := json.Marshal(inbObj)
var newInbound model.Inbound
err = newInbound.UnmarshalJSON(inbJson)
if err != nil {
return err
}
err = db.Create(&newInbound).Error
if err != nil {
return err
}
}
delete(oldConfig, "inbounds")
blockOutboundTags := []string{}
dnsOutboundTags := []string{}
oldOutbounds := oldConfig["outbounds"].([]interface{})
db.Migrator().DropTable(&model.Outbound{}, &model.Endpoint{})
db.AutoMigrate(&model.Outbound{}, &model.Endpoint{})
for _, outbound := range oldOutbounds {
outType, _ := outbound.(map[string]interface{})["type"].(string)
outboundRaw, _ := json.MarshalIndent(outbound, "", " ")
if outType == "wireguard" { // Check if it is Entrypoint
var newEntrypoint model.Endpoint
err = newEntrypoint.UnmarshalJSON(outboundRaw)
if err != nil {
return err
}
err = db.Create(&newEntrypoint).Error
if err != nil {
return err
}
} else { // It is Outbound
var newOutbound model.Outbound
err = newOutbound.UnmarshalJSON(outboundRaw)
if err != nil {
return err
}
// Delete deprecated fields
if newOutbound.Type == "direct" {
var options map[string]interface{}
json.Unmarshal(newOutbound.Options, &options)
delete(options, "override_address")
delete(options, "override_port")
newOutbound.Options, _ = json.Marshal(options)
}
switch newOutbound.Type {
case "dns":
dnsOutboundTags = append(dnsOutboundTags, newOutbound.Tag)
case "block":
blockOutboundTags = append(blockOutboundTags, newOutbound.Tag)
default:
err = db.Create(&newOutbound).Error
if err != nil {
return err
}
}
}
}
delete(oldConfig, "outbounds")
// Check routing rules
if routingRules, ok := oldConfig["route"].(map[string]interface{}); ok {
if rules, hasRules := routingRules["rules"].([]interface{}); hasRules {
hasDns := false
for index, rule := range rules {
ruleObj, _ := rule.(map[string]interface{})
isBlock := false
isDns := false
outboundTag, _ := ruleObj["outbound"].(string)
for _, tag := range blockOutboundTags {
if tag == outboundTag {
isBlock = true
delete(ruleObj, "outbound")
ruleObj["action"] = "reject"
break
}
}
for _, tag := range dnsOutboundTags {
if tag == outboundTag {
isDns = true
hasDns = true
delete(ruleObj, "outbound")
ruleObj["action"] = "hijack-dns"
break
}
}
if !isBlock && !isDns {
ruleObj["action"] = "route"
}
rules[index] = ruleObj
}
if hasDns {
rules = append(rules, map[string]interface{}{"action": "sniff"})
}
routingRules["rules"] = rules
}
oldConfig["route"] = routingRules
}
// Remove v2rayapi and clashapi from experimental config
experimental := oldConfig["experimental"].(map[string]interface{})
delete(experimental, "v2ray_api")
delete(experimental, "clash_api")
oldConfig["experimental"] = experimental
// Save the other configs
var otherConfigs json.RawMessage
otherConfigs, err = json.MarshalIndent(oldConfig, "", " ")
if err != nil {
return err
}
return db.Save(&model.Setting{
Key: "config",
Value: string(otherConfigs),
}).Error
}
func migrateTls(db *gorm.DB) error {
if !db.Migrator().HasColumn(&model.Tls{}, "inbounds") {
return nil
}
return db.Migrator().DropColumn(&model.Tls{}, "inbounds")
}
func dropInboundData(db *gorm.DB) error {
if !db.Migrator().HasTable(&InboundData{}) {
return nil
}
return db.Migrator().DropTable(&InboundData{})
}
func migrateClients(db *gorm.DB) error {
var oldClients []model.Client
err := db.Model(model.Client{}).Scan(&oldClients).Error
if err != nil {
return err
}
for index, oldClient := range oldClients {
var old_inbounds []string
err = json.Unmarshal(oldClient.Inbounds, &old_inbounds)
if err != nil {
return err
}
var inbound_ids []uint
err = db.Raw("SELECT id FROM inbounds WHERE tag in ?", old_inbounds).Find(&inbound_ids).Error
if err != nil {
return err
}
oldClients[index].Inbounds, _ = json.Marshal(inbound_ids)
}
return db.Save(oldClients).Error
}
func to1_2(db *gorm.DB) error {
err := moveJsonToDb(db)
if err != nil {
return err
}
err = migrateTls(db)
if err != nil {
return err
}
err = dropInboundData(db)
if err != nil {
return err
}
return migrateClients(db)
}
+68
View File
@@ -0,0 +1,68 @@
package migration
import (
"fmt"
"log"
"os"
"s-ui/config"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func MigrateDb() {
// void running on first install
path := config.GetDBPath()
_, err := os.Stat(path)
if err != nil {
println("Database not found")
return
}
db, err := gorm.Open(sqlite.Open(path))
if err != nil {
log.Fatal(err)
return
}
tx := db.Begin()
defer func() {
if err == nil {
tx.Commit()
} else {
tx.Rollback()
}
}()
currentVersion := config.GetVersion()
dbVersion := ""
tx.Raw("SELECT value FROM settings WHERE key = ?", "version").Find(&dbVersion)
fmt.Println("Current version:", currentVersion, "\nDatabase version:", dbVersion)
if currentVersion == dbVersion {
fmt.Println("Database is up to date, no need to migrate")
return
}
fmt.Println("Start migrating database...")
// Before 1.2
if dbVersion == "" {
err = to1_1(tx)
if err != nil {
log.Fatal("Migration to 1.1 failed: ", err)
return
}
err = to1_2(tx)
if err != nil {
log.Fatal("Migration to 1.2 failed: ", err)
return
}
}
// Set version
err = tx.Raw("UPDATE settings SET value = ? WHERE key = ?", currentVersion, "version").Error
if err != nil {
log.Fatal("Update version failed: ", err)
return
}
fmt.Println("Migration done!")
}
+66
View File
@@ -2,9 +2,14 @@ package cmd
import ( import (
"fmt" "fmt"
"io"
"net/http"
"s-ui/config" "s-ui/config"
"s-ui/database" "s-ui/database"
"s-ui/service" "s-ui/service"
"strings"
"github.com/shirou/gopsutil/v4/net"
) )
func resetSetting() { func resetSetting() {
@@ -103,3 +108,64 @@ func showSetting() {
fmt.Println("\tSub URI:\t", (*allSetting)["subURI"]) fmt.Println("\tSub URI:\t", (*allSetting)["subURI"])
} }
} }
func getPanelURI() {
err := database.InitDB(config.GetDBPath())
if err != nil {
fmt.Println(err)
return
}
settingService := service.SettingService{}
Port, _ := settingService.GetPort()
BasePath, _ := settingService.GetWebPath()
Listen, _ := settingService.GetListen()
Domain, _ := settingService.GetWebDomain()
KeyFile, _ := settingService.GetKeyFile()
CertFile, _ := settingService.GetCertFile()
TLS := false
if KeyFile != "" && CertFile != "" {
TLS = true
}
Proto := ""
if TLS {
Proto = "https://"
} else {
Proto = "http://"
}
PortText := fmt.Sprintf(":%d", Port)
if (Port == 443 && TLS) || (Port == 80 && !TLS) {
PortText = ""
}
if len(Domain) > 0 {
fmt.Println(Proto + Domain + PortText + BasePath)
return
}
if len(Listen) > 0 {
fmt.Println(Proto + Listen + PortText + BasePath)
return
}
fmt.Println("Local address:")
// get ip address
netInterfaces, _ := net.Interfaces()
for i := 0; i < len(netInterfaces); i++ {
if len(netInterfaces[i].Flags) > 2 && netInterfaces[i].Flags[0] == "up" && netInterfaces[i].Flags[1] != "loopback" {
addrs := netInterfaces[i].Addrs
for _, address := range addrs {
IP := strings.Split(address.Addr, "/")[0]
if strings.Contains(address.Addr, ".") {
fmt.Println(Proto + IP + PortText + BasePath)
} else if address.Addr[0:6] != "fe80::" {
fmt.Println(Proto + "[" + IP + "]" + PortText + BasePath)
}
}
}
}
resp, err := http.Get("https://api.ipify.org?format=text")
if err == nil {
defer resp.Body.Close()
ip, err := io.ReadAll(resp.Body)
if err == nil {
fmt.Printf("\nGlobal address:\n%s%s%s%s\n", Proto, ip, PortText, BasePath)
}
}
}
+6 -24
View File
@@ -4,6 +4,7 @@ import (
_ "embed" _ "embed"
"fmt" "fmt"
"os" "os"
"path/filepath"
"strings" "strings"
) )
@@ -13,9 +14,6 @@ var version string
//go:embed name //go:embed name
var name string var name string
//go:embed config.json
var defaultConfig string
type LogLevel string type LogLevel string
const ( const (
@@ -48,18 +46,14 @@ func IsDebug() bool {
return os.Getenv("SUI_DEBUG") == "true" return os.Getenv("SUI_DEBUG") == "true"
} }
func GetBinFolderPath() string {
binFolderPath := os.Getenv("SUI_BIN_FOLDER")
if binFolderPath == "" {
binFolderPath = "bin"
}
return binFolderPath
}
func GetDBFolderPath() string { func GetDBFolderPath() string {
dbFolderPath := os.Getenv("SUI_DB_FOLDER") dbFolderPath := os.Getenv("SUI_DB_FOLDER")
if dbFolderPath == "" { if dbFolderPath == "" {
dbFolderPath = "/usr/local/s-ui/db" dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
if err != nil {
dbFolderPath = "/usr/local/s-ui/db"
}
dbFolderPath = dir + "/db"
} }
return dbFolderPath return dbFolderPath
} }
@@ -67,15 +61,3 @@ func GetDBFolderPath() string {
func GetDBPath() string { func GetDBPath() string {
return fmt.Sprintf("%s/%s.db", GetDBFolderPath(), GetName()) return fmt.Sprintf("%s/%s.db", GetDBFolderPath(), GetName())
} }
func GetDefaultConfig() string {
apiEnv := GetEnvApi()
if len(apiEnv) > 0 {
return strings.Replace(defaultConfig, "127.0.0.1:1080", apiEnv, 1)
}
return defaultConfig
}
func GetEnvApi() string {
return os.Getenv("SINGBOX_API")
}
-38
View File
@@ -1,38 +0,0 @@
{
"log": {
"level": "info"
},
"dns": {},
"inbounds": [],
"outbounds": [
{
"tag": "direct",
"type": "direct"
},
{
"type": "dns",
"tag": "dns-out"
}
],
"route": {
"rules": [
{
"protocol": "dns",
"outbound": "dns-out"
}
]
},
"experimental": {
"v2ray_api": {
"listen": "127.0.0.1:1080",
"stats": {
"enabled": true,
"inbounds": [],
"outbounds": [
"direct"
],
"users": []
}
}
}
}
+1 -1
View File
@@ -1 +1 @@
0.0.3 1.2.0-beta.1
+408
View File
@@ -0,0 +1,408 @@
package core
import (
"context"
"fmt"
"io"
"os"
"s-ui/util/common"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/adapter/inbound"
"github.com/sagernet/sing-box/adapter/outbound"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/taskmonitor"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/cachefile"
"github.com/sagernet/sing-box/experimental/libbox/platform"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/protocol/direct"
"github.com/sagernet/sing-box/route"
sbCommon "github.com/sagernet/sing/common"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/ntp"
"github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/pause"
)
var _ adapter.Service = (*Box)(nil)
type Box struct {
createdAt time.Time
logFactory log.Factory
logger log.ContextLogger
network *route.NetworkManager
endpoint *endpoint.Manager
inbound *inbound.Manager
outbound *outbound.Manager
connection *route.ConnectionManager
router *route.Router
services []adapter.LifecycleService
connTracker *ConnTracker
done chan struct{}
}
type Options struct {
option.Options
Context context.Context
}
func Context(
ctx context.Context,
inboundRegistry adapter.InboundRegistry,
outboundRegistry adapter.OutboundRegistry,
endpointRegistry adapter.EndpointRegistry,
) context.Context {
if service.FromContext[option.InboundOptionsRegistry](ctx) == nil ||
service.FromContext[adapter.InboundRegistry](ctx) == nil {
ctx = service.ContextWith[option.InboundOptionsRegistry](ctx, inboundRegistry)
ctx = service.ContextWith[adapter.InboundRegistry](ctx, inboundRegistry)
}
if service.FromContext[option.OutboundOptionsRegistry](ctx) == nil ||
service.FromContext[adapter.OutboundRegistry](ctx) == nil {
ctx = service.ContextWith[option.OutboundOptionsRegistry](ctx, outboundRegistry)
ctx = service.ContextWith[adapter.OutboundRegistry](ctx, outboundRegistry)
}
if service.FromContext[option.EndpointOptionsRegistry](ctx) == nil ||
service.FromContext[adapter.EndpointRegistry](ctx) == nil {
ctx = service.ContextWith[option.EndpointOptionsRegistry](ctx, endpointRegistry)
ctx = service.ContextWith[adapter.EndpointRegistry](ctx, endpointRegistry)
}
return ctx
}
func NewBox(options Options) (*Box, error) {
var err error
createdAt := time.Now()
ctx := options.Context
if ctx == nil {
ctx = context.Background()
}
ctx = service.ContextWithDefaultRegistry(ctx)
endpointRegistry := service.FromContext[adapter.EndpointRegistry](ctx)
inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
if endpointRegistry == nil {
return nil, common.NewError("missing endpoint registry in context")
}
if inboundRegistry == nil {
return nil, common.NewError("missing inbound registry in context")
}
if outboundRegistry == nil {
return nil, common.NewError("missing outbound registry in context")
}
ctx = pause.WithDefaultManager(ctx)
experimentalOptions := sbCommon.PtrValueOrDefault(options.Experimental)
var needCacheFile bool
if experimentalOptions.CacheFile != nil && experimentalOptions.CacheFile.Enabled {
needCacheFile = true
}
platformInterface := service.FromContext[platform.Interface](ctx)
var defaultLogWriter io.Writer
if platformInterface != nil {
defaultLogWriter = io.Discard
}
var logFactory log.Factory
logFactory, err = NewFactory(log.Options{
Context: ctx,
Options: sbCommon.PtrValueOrDefault(options.Log),
DefaultWriter: defaultLogWriter,
BaseTime: createdAt,
})
if err != nil {
return nil, common.NewError("create log factory", err)
}
factory = logFactory
routeOptions := sbCommon.PtrValueOrDefault(options.Route)
endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry)
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final)
service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
service.MustRegister[adapter.InboundManager](ctx, inboundManager)
service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions)
if err != nil {
return nil, common.NewError("initialize network manager", err)
}
service.MustRegister[adapter.NetworkManager](ctx, networkManager)
connectionManager := route.NewConnectionManager(logFactory.NewLogger("connection"))
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)
router, err := route.NewRouter(ctx, logFactory, routeOptions, sbCommon.PtrValueOrDefault(options.DNS))
if err != nil {
return nil, common.NewError("initialize router", err)
}
for i, endpointOptions := range options.Endpoints {
var tag string
if endpointOptions.Tag != "" {
tag = endpointOptions.Tag
} else {
tag = F.ToString(i)
}
err = endpointManager.Create(ctx,
router,
logFactory.NewLogger(F.ToString("endpoint/", endpointOptions.Type, "[", tag, "]")),
tag,
endpointOptions.Type,
endpointOptions.Options,
)
if err != nil {
return nil, common.NewError("initialize endpoint["+F.ToString(i)+"] "+tag, err)
}
}
for i, inboundOptions := range options.Inbounds {
var tag string
if inboundOptions.Tag != "" {
tag = inboundOptions.Tag
} else {
tag = F.ToString(i)
}
err = inboundManager.Create(ctx,
router,
logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")),
tag,
inboundOptions.Type,
inboundOptions.Options,
)
if err != nil {
return nil, common.NewError("initialize inbound[", i, "] ", tag, err)
}
}
for i, outboundOptions := range options.Outbounds {
var tag string
if outboundOptions.Tag != "" {
tag = outboundOptions.Tag
} else {
tag = F.ToString(i)
}
outboundCtx := ctx
if tag != "" {
// TODO: remove this
outboundCtx = adapter.WithContext(outboundCtx, &adapter.InboundContext{
Outbound: tag,
})
}
err = outboundManager.Create(
outboundCtx,
router,
logFactory.NewLogger(F.ToString("outbound/", outboundOptions.Type, "[", tag, "]")),
tag,
outboundOptions.Type,
outboundOptions.Options,
)
if err != nil {
return nil, common.NewError("initialize outbound["+F.ToString(i)+"] "+tag, err)
}
}
outboundManager.Initialize(sbCommon.Must1(
direct.NewOutbound(
ctx,
router,
logFactory.NewLogger("outbound/direct"),
"direct",
option.DirectOutboundOptions{},
),
))
if platformInterface != nil {
err = platformInterface.Initialize(networkManager)
if err != nil {
return nil, common.NewError("initialize platform interface", err)
}
}
if connTracker == nil {
connTracker = NewConnTracker()
}
router.SetTracker(connTracker)
var services []adapter.LifecycleService
if needCacheFile {
cacheFile := cachefile.New(ctx, sbCommon.PtrValueOrDefault(experimentalOptions.CacheFile))
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
services = append(services, cacheFile)
}
ntpOptions := sbCommon.PtrValueOrDefault(options.NTP)
if ntpOptions.Enabled {
ntpDialer, err := dialer.New(ctx, ntpOptions.DialerOptions)
if err != nil {
return nil, common.NewError(err, "create NTP service")
}
timeService := ntp.NewService(ntp.Options{
Context: ctx,
Dialer: ntpDialer,
Logger: logFactory.NewLogger("ntp"),
Server: ntpOptions.ServerOptions.Build(),
Interval: time.Duration(ntpOptions.Interval),
WriteToSystem: ntpOptions.WriteToSystem,
})
service.MustRegister[ntp.TimeService](ctx, timeService)
services = append(services, adapter.NewLifecycleService(timeService, "ntp service"))
}
return &Box{
network: networkManager,
endpoint: endpointManager,
inbound: inboundManager,
outbound: outboundManager,
connection: connectionManager,
router: router,
createdAt: createdAt,
logFactory: logFactory,
logger: logFactory.Logger(),
services: services,
connTracker: connTracker,
done: make(chan struct{}),
}, nil
}
func (s *Box) PreStart() error {
err := s.preStart()
if err != nil {
// TODO: remove catch error
defer func() {
v := recover()
if v != nil {
s.logger.Error(err.Error())
s.logger.Error("panic on early close: " + fmt.Sprint(v))
}
}()
s.Close()
return err
}
s.logger.Info("sing-box pre-started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
return nil
}
func (s *Box) Start() error {
err := s.start()
if err != nil {
// TODO: remove catch error
defer func() {
v := recover()
if v != nil {
s.logger.Debug(err.Error())
s.logger.Error("panic on early start: " + fmt.Sprint(v))
}
}()
s.Close()
return err
}
s.logger.Info("sing-box started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
return nil
}
func (s *Box) preStart() error {
monitor := taskmonitor.New(s.logger, C.StartTimeout)
monitor.Start("start logger")
err := s.logFactory.Start()
monitor.Finish()
if err != nil {
return common.NewError(err, "start logger")
}
err = adapter.StartNamed(adapter.StartStateInitialize, s.services) // cache-file
if err != nil {
return err
}
err = adapter.Start(adapter.StartStateInitialize, s.network, s.connection, s.router, s.outbound, s.inbound, s.endpoint)
if err != nil {
return err
}
err = adapter.Start(adapter.StartStateStart, s.outbound, s.network, s.connection, s.router)
if err != nil {
return err
}
return nil
}
func (s *Box) start() error {
err := s.preStart()
if err != nil {
return err
}
err = adapter.StartNamed(adapter.StartStateStart, s.services)
if err != nil {
return err
}
err = s.inbound.Start(adapter.StartStateStart)
if err != nil {
return err
}
err = adapter.Start(adapter.StartStateStart, s.endpoint)
if err != nil {
return err
}
err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.connection, s.router, s.inbound, s.endpoint)
if err != nil {
return err
}
err = adapter.StartNamed(adapter.StartStatePostStart, s.services)
if err != nil {
return err
}
err = adapter.Start(adapter.StartStateStarted, s.network, s.connection, s.router, s.outbound, s.inbound, s.endpoint)
if err != nil {
return err
}
err = adapter.StartNamed(adapter.StartStateStarted, s.services)
if err != nil {
return err
}
return nil
}
func (s *Box) Close() error {
select {
case <-s.done:
return os.ErrClosed
default:
close(s.done)
}
err := sbCommon.Close(
s.endpoint, s.inbound, s.outbound, s.router, s.connection, s.network,
)
for _, lifecycleService := range s.services {
err1 := lifecycleService.Close()
if err1 != nil {
s.logger.Debug(lifecycleService.Name(), " close error: ", err1)
}
}
err1 := s.logFactory.Close()
if err1 != nil {
s.logger.Debug("logger close error: ", err1)
}
return err
}
func (s *Box) Uptime() uint32 {
return uint32(time.Now().Sub(s.createdAt).Seconds())
}
func (s *Box) Network() adapter.NetworkManager {
return s.network
}
func (s *Box) Router() adapter.Router {
return s.router
}
func (s *Box) Inbound() adapter.InboundManager {
return s.inbound
}
func (s *Box) Outbound() adapter.OutboundManager {
return s.outbound
}
func (s *Box) Endpoint() adapter.EndpointManager {
return s.endpoint
}
func (s *Box) ConnTracker() *ConnTracker {
return s.connTracker
}
+145
View File
@@ -0,0 +1,145 @@
package core
import (
"context"
"net"
"s-ui/database/model"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common/atomic"
"github.com/sagernet/sing/common/bufio"
"github.com/sagernet/sing/common/network"
)
type Counter struct {
read *atomic.Int64
write *atomic.Int64
}
type ConnTracker struct {
access sync.Mutex
createdAt time.Time
inbounds map[string]Counter
outbounds map[string]Counter
users map[string]Counter
}
func NewConnTracker() *ConnTracker {
return &ConnTracker{
createdAt: time.Now(),
inbounds: make(map[string]Counter),
outbounds: make(map[string]Counter),
users: make(map[string]Counter),
}
}
func (c *ConnTracker) getReadCounters(inbound string, outbound string, user string) ([]*atomic.Int64, []*atomic.Int64) {
var readCounter []*atomic.Int64
var writeCounter []*atomic.Int64
c.access.Lock()
if inbound != "" {
readCounter = append(readCounter, c.loadOrCreateCounter(&c.inbounds, inbound).read)
writeCounter = append(writeCounter, c.inbounds[inbound].write)
}
if outbound != "" {
readCounter = append(readCounter, c.loadOrCreateCounter(&c.outbounds, outbound).read)
writeCounter = append(writeCounter, c.outbounds[outbound].write)
}
if user != "" {
readCounter = append(readCounter, c.loadOrCreateCounter(&c.users, user).read)
writeCounter = append(writeCounter, c.users[user].write)
}
c.access.Unlock()
return readCounter, writeCounter
}
func (c *ConnTracker) loadOrCreateCounter(obj *map[string]Counter, name string) Counter {
counter, loaded := (*obj)[name]
if loaded {
return counter
}
counter = Counter{read: &atomic.Int64{}, write: &atomic.Int64{}}
(*obj)[name] = counter
return counter
}
func (c *ConnTracker) RoutedConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) net.Conn {
readCounter, writeCounter := c.getReadCounters(metadata.Inbound, matchOutbound.Tag(), metadata.User)
return bufio.NewInt64CounterConn(conn, readCounter, writeCounter)
}
func (c *ConnTracker) RoutedPacketConnection(ctx context.Context, conn network.PacketConn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) network.PacketConn {
readCounter, writeCounter := c.getReadCounters(metadata.Inbound, matchOutbound.Tag(), metadata.User)
return bufio.NewInt64CounterPacketConn(conn, readCounter, writeCounter)
}
func (c *ConnTracker) GetStats() *[]model.Stats {
c.access.Lock()
defer c.access.Unlock()
dt := time.Now().Unix()
s := []model.Stats{}
for inbound, counter := range c.inbounds {
down := counter.write.Swap(0)
up := counter.read.Swap(0)
if down > 0 || up > 0 {
s = append(s, model.Stats{
DateTime: dt,
Resource: "inbound",
Tag: inbound,
Direction: false,
Traffic: down,
}, model.Stats{
DateTime: dt,
Resource: "inbound",
Tag: inbound,
Direction: true,
Traffic: up,
})
}
}
for outbound, counter := range c.outbounds {
down := counter.write.Swap(0)
up := counter.read.Swap(0)
if down > 0 || up > 0 {
s = append(s, model.Stats{
DateTime: dt,
Resource: "outbound",
Tag: outbound,
Direction: false,
Traffic: down,
}, model.Stats{
DateTime: dt,
Resource: "outbound",
Tag: outbound,
Direction: true,
Traffic: up,
})
}
}
for user, counter := range c.users {
down := counter.write.Swap(0)
up := counter.read.Swap(0)
if down > 0 || up > 0 {
s = append(s, model.Stats{
DateTime: dt,
Resource: "user",
Tag: user,
Direction: false,
Traffic: down,
}, model.Stats{
DateTime: dt,
Resource: "user",
Tag: user,
Direction: true,
Traffic: up,
})
}
}
return &s
}
+109
View File
@@ -0,0 +1,109 @@
package core
import (
"s-ui/logger"
"s-ui/util/common"
"github.com/sagernet/sing-box/option"
)
func (c *Core) AddInbound(config []byte) error {
if !c.isRunning {
return common.NewError("sing-box is not running")
}
var err error
var inbound_config option.Inbound
err = inbound_config.UnmarshalJSONContext(globalCtx, config)
if err != nil {
return err
}
err = inbound_manager.Create(
globalCtx,
router,
factory.NewLogger("inbound/"+inbound_config.Type+"["+inbound_config.Tag+"]"),
inbound_config.Tag,
inbound_config.Type,
inbound_config.Options)
if err != nil {
return err
}
return nil
}
func (c *Core) RemoveInbound(tag string) error {
if !c.isRunning {
return common.NewError("sing-box is not running")
}
logger.Info("remove inbound: ", tag)
return inbound_manager.Remove(tag)
}
func (c *Core) AddOutbound(config []byte) error {
if !c.isRunning {
return common.NewError("sing-box is not running")
}
var err error
var outbound_config option.Outbound
err = outbound_config.UnmarshalJSONContext(globalCtx, config)
if err != nil {
return err
}
err = outbound_manager.Create(
globalCtx,
router,
factory.NewLogger("outbound/"+outbound_config.Type+"["+outbound_config.Tag+"]"),
outbound_config.Tag,
outbound_config.Type,
outbound_config.Options)
if err != nil {
return err
}
return nil
}
func (c *Core) RemoveOutbound(tag string) error {
if !c.isRunning {
return common.NewError("sing-box is not running")
}
logger.Info("remove outbound: ", tag)
return outbound_manager.Remove(tag)
}
func (c *Core) AddEndpoint(config []byte) error {
if !c.isRunning {
return common.NewError("sing-box is not running")
}
var err error
var endpoint_config option.Endpoint
err = endpoint_config.UnmarshalJSONContext(globalCtx, config)
if err != nil {
return err
}
err = endpoint_manager.Create(
globalCtx,
router,
factory.NewLogger("endpoint/"+endpoint_config.Type+"["+endpoint_config.Tag+"]"),
endpoint_config.Tag,
endpoint_config.Type,
endpoint_config.Options)
if err != nil {
return err
}
return nil
}
func (c *Core) RemoveEndpoint(tag string) error {
if !c.isRunning {
return common.NewError("sing-box is not running")
}
logger.Info("remove endpoint: ", tag)
return endpoint_manager.Remove(tag)
}
+236
View File
@@ -0,0 +1,236 @@
package core
import (
"context"
"io"
"os"
suiLog "s-ui/logger"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/observable"
"github.com/sagernet/sing/service/filemanager"
)
type PlatformWriter struct{}
func (p PlatformWriter) DisableColors() bool {
return true
}
func (p PlatformWriter) WriteMessage(level log.Level, message string) {
switch level {
case log.LevelInfo:
suiLog.Info(message)
case log.LevelWarn:
suiLog.Warning(message)
case log.LevelPanic:
case log.LevelFatal:
case log.LevelError:
suiLog.Error(message)
default:
suiLog.Debug(message)
}
}
func NewFactory(options log.Options) (log.Factory, error) {
logOptions := options.Options
if logOptions.Disabled {
return log.NewNOPFactory(), nil
}
var logWriter io.Writer
var logFilePath string
switch logOptions.Output {
case "":
logWriter = options.DefaultWriter
if logWriter == nil {
logWriter = os.Stderr
}
case "stderr":
logWriter = os.Stderr
case "stdout":
logWriter = os.Stdout
default:
logFilePath = logOptions.Output
}
logFormatter := log.Formatter{
BaseTime: options.BaseTime,
DisableColors: logOptions.DisableColor || logFilePath != "",
DisableTimestamp: !logOptions.Timestamp && logFilePath != "",
FullTimestamp: logOptions.Timestamp,
TimestampFormat: "-0700 2006-01-02 15:04:05",
}
factory := NewDefaultFactory(
options.Context,
logFormatter,
logWriter,
logFilePath,
)
if logOptions.Level != "" {
logLevel, err := log.ParseLevel(logOptions.Level)
if err != nil {
return nil, common.Error("parse log level", err)
}
factory.SetLevel(logLevel)
} else {
factory.SetLevel(log.LevelTrace)
}
return factory, nil
}
var _ log.Factory = (*defaultFactory)(nil)
type defaultFactory struct {
ctx context.Context
formatter log.Formatter
writer io.Writer
file *os.File
filePath string
level log.Level
subscriber *observable.Subscriber[log.Entry]
observer *observable.Observer[log.Entry]
}
func NewDefaultFactory(
ctx context.Context,
formatter log.Formatter,
writer io.Writer,
filePath string,
) log.ObservableFactory {
factory := &defaultFactory{
ctx: ctx,
formatter: formatter,
writer: writer,
filePath: filePath,
level: log.LevelTrace,
subscriber: observable.NewSubscriber[log.Entry](128),
}
return factory
}
func (f *defaultFactory) Start() error {
if f.filePath != "" {
logFile, err := filemanager.OpenFile(f.ctx, f.filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return err
}
f.writer = logFile
f.file = logFile
}
return nil
}
func (f *defaultFactory) Close() error {
return common.Close(
common.PtrOrNil(f.file),
f.subscriber,
)
}
func (f *defaultFactory) Level() log.Level {
return f.level
}
func (f *defaultFactory) SetLevel(level log.Level) {
f.level = level
}
func (f *defaultFactory) Logger() log.ContextLogger {
return f.NewLogger("")
}
func (f *defaultFactory) NewLogger(tag string) log.ContextLogger {
return &observableLogger{f, tag}
}
func (f *defaultFactory) Subscribe() (subscription observable.Subscription[log.Entry], done <-chan struct{}, err error) {
return f.observer.Subscribe()
}
func (f *defaultFactory) UnSubscribe(sub observable.Subscription[log.Entry]) {
f.observer.UnSubscribe(sub)
}
type observableLogger struct {
*defaultFactory
tag string
}
func (l *observableLogger) Log(ctx context.Context, level log.Level, args []any) {
level = log.OverrideLevelFromContext(level, ctx)
if level > l.level {
return
}
msg := F.ToString(args...)
switch level {
case log.LevelInfo:
suiLog.Info(l.tag, msg)
case log.LevelWarn:
suiLog.Warning(l.tag, msg)
case log.LevelPanic:
case log.LevelFatal:
case log.LevelError:
suiLog.Error(l.tag, msg)
default:
suiLog.Debug(l.tag, msg)
}
}
func (l *observableLogger) Trace(args ...any) {
l.TraceContext(context.Background(), args...)
}
func (l *observableLogger) Debug(args ...any) {
l.DebugContext(context.Background(), args...)
}
func (l *observableLogger) Info(args ...any) {
l.InfoContext(context.Background(), args...)
}
func (l *observableLogger) Warn(args ...any) {
l.WarnContext(context.Background(), args...)
}
func (l *observableLogger) Error(args ...any) {
l.ErrorContext(context.Background(), args...)
}
func (l *observableLogger) Fatal(args ...any) {
l.FatalContext(context.Background(), args...)
}
func (l *observableLogger) Panic(args ...any) {
l.PanicContext(context.Background(), args...)
}
func (l *observableLogger) TraceContext(ctx context.Context, args ...any) {
l.Log(ctx, log.LevelTrace, args)
}
func (l *observableLogger) DebugContext(ctx context.Context, args ...any) {
l.Log(ctx, log.LevelDebug, args)
}
func (l *observableLogger) InfoContext(ctx context.Context, args ...any) {
l.Log(ctx, log.LevelInfo, args)
}
func (l *observableLogger) WarnContext(ctx context.Context, args ...any) {
l.Log(ctx, log.LevelWarn, args)
}
func (l *observableLogger) ErrorContext(ctx context.Context, args ...any) {
l.Log(ctx, log.LevelError, args)
}
func (l *observableLogger) FatalContext(ctx context.Context, args ...any) {
l.Log(ctx, log.LevelFatal, args)
}
func (l *observableLogger) PanicContext(ctx context.Context, args ...any) {
l.Log(ctx, log.LevelPanic, args)
}
+90
View File
@@ -0,0 +1,90 @@
package core
import (
"context"
"s-ui/logger"
sb "github.com/sagernet/sing-box"
"github.com/sagernet/sing-box/adapter"
_ "github.com/sagernet/sing-box/experimental/clashapi"
_ "github.com/sagernet/sing-box/experimental/v2rayapi"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
_ "github.com/sagernet/sing-box/transport/v2rayquic"
_ "github.com/sagernet/sing-dns/quic"
"github.com/sagernet/sing/service"
)
var (
globalCtx context.Context
inbound_manager adapter.InboundManager
outbound_manager adapter.OutboundManager
endpoint_manager adapter.EndpointManager
router adapter.Router
connTracker *ConnTracker
factory log.Factory
)
type Core struct {
isRunning bool
instance *Box
}
func NewCore() *Core {
globalCtx = context.Background()
globalCtx = sb.Context(globalCtx, inboundRegistry(), outboundRegistry(), EndpointRegistry())
return &Core{
isRunning: false,
instance: nil,
}
}
func (c *Core) GetCtx() context.Context {
return globalCtx
}
func (c *Core) GetInstance() *Box {
return c.instance
}
func (c *Core) Start(sbConfig []byte) error {
var opt option.Options
err := opt.UnmarshalJSONContext(globalCtx, sbConfig)
if err != nil {
logger.Error("Unmarshal config err:", err.Error())
}
c.instance, err = NewBox(Options{
Context: globalCtx,
Options: opt,
})
if err != nil {
return err
}
err = c.instance.Start()
if err != nil {
return err
}
globalCtx = service.ContextWith(globalCtx, c)
inbound_manager = service.FromContext[adapter.InboundManager](globalCtx)
outbound_manager = service.FromContext[adapter.OutboundManager](globalCtx)
endpoint_manager = service.FromContext[adapter.EndpointManager](globalCtx)
router = service.FromContext[adapter.Router](globalCtx)
c.isRunning = true
return nil
}
func (c *Core) Stop() error {
if c.isRunning {
c.isRunning = false
return c.instance.Close()
}
return nil
}
func (c *Core) IsRunning() bool {
return c.isRunning
}
+94
View File
@@ -0,0 +1,94 @@
package core
import (
"github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/adapter/inbound"
"github.com/sagernet/sing-box/adapter/outbound"
"github.com/sagernet/sing-box/protocol/block"
"github.com/sagernet/sing-box/protocol/direct"
"github.com/sagernet/sing-box/protocol/dns"
"github.com/sagernet/sing-box/protocol/group"
"github.com/sagernet/sing-box/protocol/http"
"github.com/sagernet/sing-box/protocol/hysteria"
"github.com/sagernet/sing-box/protocol/hysteria2"
"github.com/sagernet/sing-box/protocol/mixed"
"github.com/sagernet/sing-box/protocol/naive"
_ "github.com/sagernet/sing-box/protocol/naive/quic"
"github.com/sagernet/sing-box/protocol/redirect"
"github.com/sagernet/sing-box/protocol/shadowsocks"
"github.com/sagernet/sing-box/protocol/shadowtls"
"github.com/sagernet/sing-box/protocol/socks"
"github.com/sagernet/sing-box/protocol/ssh"
"github.com/sagernet/sing-box/protocol/tor"
"github.com/sagernet/sing-box/protocol/trojan"
"github.com/sagernet/sing-box/protocol/tuic"
"github.com/sagernet/sing-box/protocol/tun"
"github.com/sagernet/sing-box/protocol/vless"
"github.com/sagernet/sing-box/protocol/vmess"
"github.com/sagernet/sing-box/protocol/wireguard"
_ "github.com/sagernet/sing-box/transport/v2rayquic"
_ "github.com/sagernet/sing-dns/quic"
)
func inboundRegistry() *inbound.Registry {
registry := inbound.NewRegistry()
tun.RegisterInbound(registry)
redirect.RegisterRedirect(registry)
redirect.RegisterTProxy(registry)
direct.RegisterInbound(registry)
socks.RegisterInbound(registry)
http.RegisterInbound(registry)
mixed.RegisterInbound(registry)
shadowsocks.RegisterInbound(registry)
vmess.RegisterInbound(registry)
trojan.RegisterInbound(registry)
naive.RegisterInbound(registry)
shadowtls.RegisterInbound(registry)
vless.RegisterInbound(registry)
hysteria.RegisterInbound(registry)
tuic.RegisterInbound(registry)
hysteria2.RegisterInbound(registry)
return registry
}
func outboundRegistry() *outbound.Registry {
registry := outbound.NewRegistry()
direct.RegisterOutbound(registry)
block.RegisterOutbound(registry)
dns.RegisterOutbound(registry)
group.RegisterSelector(registry)
group.RegisterURLTest(registry)
socks.RegisterOutbound(registry)
http.RegisterOutbound(registry)
shadowsocks.RegisterOutbound(registry)
vmess.RegisterOutbound(registry)
trojan.RegisterOutbound(registry)
tor.RegisterOutbound(registry)
ssh.RegisterOutbound(registry)
shadowtls.RegisterOutbound(registry)
vless.RegisterOutbound(registry)
hysteria.RegisterOutbound(registry)
tuic.RegisterOutbound(registry)
hysteria2.RegisterOutbound(registry)
wireguard.RegisterOutbound(registry)
return registry
}
func EndpointRegistry() *endpoint.Registry {
registry := endpoint.NewRegistry()
wireguard.RegisterEndpoint(registry)
return registry
}
+17
View File
@@ -0,0 +1,17 @@
package cronjob
import (
"s-ui/service"
)
type CheckCoreJob struct {
service.ConfigService
}
func NewCheckCoreJob() *CheckCoreJob {
return &CheckCoreJob{}
}
func (s *CheckCoreJob) Run() {
s.ConfigService.StartCore("")
}
+2
View File
@@ -25,6 +25,8 @@ func (c *CronJob) Start(loc *time.Location, trafficAge int) error {
c.cron.AddJob("@every 1m", NewDepleteJob()) c.cron.AddJob("@every 1m", NewDepleteJob())
// Start deleting old stats // Start deleting old stats
c.cron.AddJob("@daily", NewDelStatsJob(trafficAge)) c.cron.AddJob("@daily", NewDelStatsJob(trafficAge))
// Start core if it is not running
c.cron.AddJob("@every 5s", NewCheckCoreJob())
}() }()
return nil return nil
+1
View File
@@ -22,4 +22,5 @@ func (s *DelStatsJob) Run() {
logger.Warning("Deleting old statistics failed: ", err) logger.Warning("Deleting old statistics failed: ", err)
return return
} }
logger.Debug("Stats older than ", s.trafficAge, " days were deleted")
} }
+2 -2
View File
@@ -6,7 +6,7 @@ import (
) )
type DepleteJob struct { type DepleteJob struct {
service.ConfigService service.ClientService
} }
func NewDepleteJob() *DepleteJob { func NewDepleteJob() *DepleteJob {
@@ -14,7 +14,7 @@ func NewDepleteJob() *DepleteJob {
} }
func (s *DepleteJob) Run() { func (s *DepleteJob) Run() {
err := s.ConfigService.DepleteClients() err := s.ClientService.DepleteClients()
if err != nil { if err != nil {
logger.Warning("Disable depleted users failed: ", err) logger.Warning("Disable depleted users failed: ", err)
return return
+3 -3
View File
@@ -6,15 +6,15 @@ import (
) )
type StatsJob struct { type StatsJob struct {
service.SingBoxService service.StatsService
} }
func NewStatsJob() *StatsJob { func NewStatsJob() *StatsJob {
return new(StatsJob) return &StatsJob{}
} }
func (s *StatsJob) Run() { func (s *StatsJob) Run() {
err := s.SingBoxService.GetStats() err := s.StatsService.SaveStats()
if err != nil { if err != nil {
logger.Warning("Get stats failed: ", err) logger.Warning("Get stats failed: ", err)
return return
+25 -1
View File
@@ -1,6 +1,7 @@
package database package database
import ( import (
"encoding/json"
"os" "os"
"path" "path"
"s-ui/config" "s-ui/config"
@@ -29,7 +30,7 @@ func initUser() error {
return nil return nil
} }
func InitDB(dbPath string) error { func OpenDB(dbPath string) error {
dir := path.Dir(dbPath) dir := path.Dir(dbPath)
err := os.MkdirAll(dir, 01740) err := os.MkdirAll(dir, 01740)
if err != nil { if err != nil {
@@ -48,12 +49,35 @@ func InitDB(dbPath string) error {
Logger: gormLogger, Logger: gormLogger,
} }
db, err = gorm.Open(sqlite.Open(dbPath), c) db, err = gorm.Open(sqlite.Open(dbPath), c)
if config.IsDebug() {
db = db.Debug()
}
return err
}
func InitDB(dbPath string) error {
err := OpenDB(dbPath)
if err != nil { if err != nil {
return err return err
} }
// Default Outbounds
if !db.Migrator().HasTable(&model.Outbound{}) {
db.Migrator().CreateTable(&model.Outbound{})
defaultOutbound := []model.Outbound{
{Type: "direct", Tag: "direct", Options: json.RawMessage(`{}`)},
{Type: "dns", Tag: "dns-out", Options: json.RawMessage(`{}`)},
}
db.Create(&defaultOutbound)
}
err = db.AutoMigrate( err = db.AutoMigrate(
&model.Setting{}, &model.Setting{},
&model.Tls{},
&model.Inbound{},
&model.Outbound{},
&model.Endpoint{},
&model.User{}, &model.User{},
&model.Stats{}, &model.Stats{},
&model.Client{}, &model.Client{},
+55
View File
@@ -0,0 +1,55 @@
package model
import (
"encoding/json"
)
type Endpoint struct {
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
Type string `json:"type" form:"type"`
Tag string `json:"tag" form:"tag" gorm:"unique"`
Options json.RawMessage `json:"-" form:"-"`
}
func (o *Endpoint) UnmarshalJSON(data []byte) error {
var err error
var raw map[string]interface{}
if err = json.Unmarshal(data, &raw); err != nil {
return err
}
// Extract fixed fields and store the rest in Options
if val, exists := raw["id"].(float64); exists {
o.Id = uint(val)
}
delete(raw, "id")
o.Type, _ = raw["type"].(string)
delete(raw, "type")
o.Tag = raw["tag"].(string)
delete(raw, "tag")
// Remaining fields
o.Options, err = json.Marshal(raw)
return err
}
// MarshalJSON customizes marshalling
func (o Endpoint) MarshalJSON() ([]byte, error) {
// Combine fixed fields and dynamic fields into one map
combined := make(map[string]interface{})
combined["type"] = o.Type
combined["tag"] = o.Tag
if o.Options != nil {
var restFields map[string]json.RawMessage
if err := json.Unmarshal(o.Options, &restFields); err != nil {
return nil, err
}
for k, v := range restFields {
combined[k] = v
}
}
return json.Marshal(combined)
}
+103
View File
@@ -0,0 +1,103 @@
package model
import (
"encoding/json"
)
type Inbound struct {
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
Type string `json:"type" form:"type"`
Tag string `json:"tag" form:"tag" gorm:"unique"`
// Foreign key to tls table
TlsId uint `json:"tls_id" form:"tls_id"`
Tls *Tls `json:"tls" form:"tls" gorm:"foreignKey:TlsId;references:Id"`
Addrs json.RawMessage `json:"addrs" form:"addrs"`
OutJson json.RawMessage `json:"out_json" form:"out_json"`
Options json.RawMessage `json:"-" form:"-"`
}
func (i *Inbound) UnmarshalJSON(data []byte) error {
var err error
var raw map[string]interface{}
if err = json.Unmarshal(data, &raw); err != nil {
return err
}
// Extract fixed fields and store the rest in Options
if val, exists := raw["id"].(float64); exists {
i.Id = uint(val)
}
delete(raw, "id")
i.Type, _ = raw["type"].(string)
delete(raw, "type")
i.Tag, _ = raw["tag"].(string)
delete(raw, "tag")
// TlsId
if val, exists := raw["tls_id"].(float64); exists {
i.TlsId = uint(val)
}
delete(raw, "tls_id")
delete(raw, "tls")
delete(raw, "users")
// Addrs
i.Addrs, _ = json.MarshalIndent(raw["addrs"], "", " ")
delete(raw, "addrs")
// OutJson
i.OutJson, _ = json.MarshalIndent(raw["out_json"], "", " ")
delete(raw, "out_json")
// Remaining fields
i.Options, err = json.MarshalIndent(raw, "", " ")
return err
}
// MarshalJSON customizes marshalling
func (i Inbound) MarshalJSON() ([]byte, error) {
// Combine fixed fields and dynamic fields into one map
combined := make(map[string]interface{})
combined["type"] = i.Type
combined["tag"] = i.Tag
if i.Tls != nil {
combined["tls"] = i.Tls.Server
}
if i.Options != nil {
var restFields map[string]json.RawMessage
if err := json.Unmarshal(i.Options, &restFields); err != nil {
return nil, err
}
for k, v := range restFields {
combined[k] = v
}
}
return json.Marshal(combined)
}
func (i Inbound) MarshalFull() (*map[string]interface{}, error) {
combined := make(map[string]interface{})
combined["id"] = i.Id
combined["type"] = i.Type
combined["tag"] = i.Tag
combined["tls_id"] = i.TlsId
combined["addrs"] = i.Addrs
combined["out_json"] = i.OutJson
if i.Options != nil {
var restFields map[string]interface{}
if err := json.Unmarshal(i.Options, &restFields); err != nil {
return nil, err
}
for k, v := range restFields {
combined[k] = v
}
}
return &combined, nil
}
+19 -11
View File
@@ -8,6 +8,13 @@ type Setting struct {
Value string `json:"value" form:"value"` Value string `json:"value" form:"value"`
} }
type Tls struct {
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
Name string `json:"name" form:"name"`
Server json.RawMessage `json:"server" form:"server"`
Client json.RawMessage `json:"client" form:"client"`
}
type User struct { type User struct {
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
Username string `json:"username" form:"username"` Username string `json:"username" form:"username"`
@@ -16,17 +23,18 @@ type User struct {
} }
type Client struct { type Client struct {
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
Enable bool `json:"enable" form:"enable"` Enable bool `json:"enable" form:"enable"`
Name string `json:"name" form:"name"` Name string `json:"name" form:"name"`
Config string `json:"config" form:"config"` Config json.RawMessage `json:"config" form:"config"`
Inbounds string `json:"inbounds" form:"inbounds"` Inbounds json.RawMessage `json:"inbounds" form:"inbounds"`
Links string `json:"links" form:"links"` Links json.RawMessage `json:"links" form:"links"`
Volume int64 `json:"volume" form:"volume"` Volume int64 `json:"volume" form:"volume"`
Expiry int64 `json:"expiry" form:"expiry"` Expiry int64 `json:"expiry" form:"expiry"`
Down int64 `json:"down" form:"down"` Down int64 `json:"down" form:"down"`
Up int64 `json:"up" form:"up"` Up int64 `json:"up" form:"up"`
Desc string `json:"desc" from:"desc"` Desc string `json:"desc" form:"desc"`
Group string `json:"group" form:"group"`
} }
type Stats struct { type Stats struct {
+53
View File
@@ -0,0 +1,53 @@
package model
import "encoding/json"
type Outbound struct {
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
Type string `json:"type" form:"type"`
Tag string `json:"tag" form:"tag" gorm:"unique"`
Options json.RawMessage `json:"-" form:"-"`
}
func (o *Outbound) UnmarshalJSON(data []byte) error {
var err error
var raw map[string]interface{}
if err = json.Unmarshal(data, &raw); err != nil {
return err
}
// Extract fixed fields and store the rest in Options
if val, exists := raw["id"].(float64); exists {
o.Id = uint(val)
}
delete(raw, "id")
o.Type, _ = raw["type"].(string)
delete(raw, "type")
o.Tag = raw["tag"].(string)
delete(raw, "tag")
// Remaining fields
o.Options, err = json.Marshal(raw)
return err
}
// MarshalJSON customizes marshalling
func (o Outbound) MarshalJSON() ([]byte, error) {
// Combine fixed fields and dynamic fields into one map
combined := make(map[string]interface{})
combined["type"] = o.Type
combined["tag"] = o.Tag
if o.Options != nil {
var restFields map[string]json.RawMessage
if err := json.Unmarshal(o.Options, &restFields); err != nil {
return nil, err
}
for k, v := range restFields {
combined[k] = v
}
}
return json.Marshal(combined)
}
+115 -48
View File
@@ -1,64 +1,131 @@
module s-ui module s-ui
go 1.22.0 go 1.23.2
require ( require (
github.com/gin-contrib/gzip v0.0.6 github.com/gin-contrib/gzip v1.0.1
github.com/gin-gonic/gin v1.9.1 github.com/gin-gonic/gin v1.10.0
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/v2fly/v2ray-core/v5 v5.13.0 github.com/robfig/cron/v3 v3.0.1
gorm.io/driver/sqlite v1.5.5 github.com/sagernet/sing v0.6.0-beta.9
gorm.io/gorm v1.25.7 github.com/sagernet/sing-box v1.11.0-beta.19
github.com/sagernet/sing-dns v0.4.0-beta.1
github.com/shirou/gopsutil/v3 v3.24.5
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
gorm.io/driver/sqlite v1.5.7
gorm.io/gorm v1.25.12
) )
require ( require (
github.com/adrg/xdg v0.4.0 // indirect github.com/ebitengine/purego v0.8.1 // indirect
github.com/bytedance/sonic v1.11.1 // indirect google.golang.org/grpc v1.67.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect )
github.com/chenzhuoyu/iasm v0.9.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect require (
github.com/ajg/form v1.5.1 // indirect
github.com/andybalholm/brotli v1.0.6 // indirect
github.com/bytedance/sonic v1.12.3 // indirect
github.com/bytedance/sonic/loader v0.2.1 // indirect
github.com/caddyserver/certmagic v0.20.0 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/cretz/bine v0.2.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.6 // indirect
github.com/gin-contrib/sessions v1.0.1
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-chi/chi/v5 v5.1.0 // indirect
github.com/go-chi/render v1.0.3 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/go-playground/validator/v10 v10.22.1 // indirect
github.com/golang/protobuf v1.5.3 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/gofrs/uuid/v5 v5.3.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect
github.com/gorilla/context v1.1.2 // indirect github.com/gorilla/context v1.1.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/sessions v1.2.2 // indirect github.com/gorilla/sessions v1.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/hashicorp/yamux v0.1.2 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lufia/plan9stats v0.0.0-20240226150601-1dcf7310316a // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pires/go-proxyproto v0.7.0 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/tklauser/go-sysconf v0.3.13 // indirect
github.com/tklauser/numcpus v0.7.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/arch v0.7.0 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/net v0.23.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
require (
github.com/Calidity/gin-sessions v1.3.1
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/validator/v10 v10.18.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect github.com/josharian/native v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/robfig/cron/v3 v3.0.1 github.com/klauspost/compress v1.17.7 // indirect
github.com/shirou/gopsutil/v3 v3.24.1 github.com/klauspost/cpuid/v2 v2.2.8 // indirect
google.golang.org/grpc v1.62.0 github.com/leodido/go-urn v1.4.0 // indirect
github.com/libdns/alidns v1.0.3 // indirect
github.com/libdns/cloudflare v0.1.1 // indirect
github.com/libdns/libdns v0.2.2 // indirect; indiresct
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.24 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.4.1 // indirect
github.com/metacubex/tfo-go v0.0.0-20241006021335-daedaf0ca7aa // indirect
github.com/mholt/acmez v1.2.0 // indirect
github.com/miekg/dns v1.1.62 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/onsi/ginkgo/v2 v2.10.0 // indirect
github.com/oschwald/maxminddb-golang v1.12.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a // indirect
github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1 // indirect
github.com/sagernet/cors v1.2.1 // indirect
github.com/sagernet/fswatch v0.1.1 // indirect
github.com/sagernet/gvisor v0.0.0-20241123041152-536d05261cff // indirect
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
github.com/sagernet/nftables v0.3.0-beta.4 // indirect
github.com/sagernet/quic-go v0.48.2-beta.1 // indirect
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 // indirect
github.com/sagernet/sing-mux v0.3.0-alpha.1 // indirect
github.com/sagernet/sing-quic v0.4.0-beta.2 // indirect
github.com/sagernet/sing-shadowsocks v0.2.7 // indirect
github.com/sagernet/sing-shadowsocks2 v0.2.0 // indirect
github.com/sagernet/sing-shadowtls v0.2.0-alpha.2 // indirect
github.com/sagernet/sing-tun v0.6.0-beta.7.0.20241229131914-aa9d9c62966f // indirect
github.com/sagernet/sing-vmess v0.2.0-beta.1 // indirect
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect
github.com/sagernet/utls v1.6.7 // indirect
github.com/sagernet/wireguard-go v0.0.1-beta.5 // indirect
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 // indirect
github.com/shirou/gopsutil/v4 v4.24.12
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/tklauser/go-sysconf v0.3.14 // indirect
github.com/tklauser/numcpus v0.9.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/vishvananda/netns v0.0.4 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
github.com/zeebo/blake3 v0.2.3 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/arch v0.11.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/mod v0.20.0 // indirect
golang.org/x/net v0.31.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.7.0 // indirect
golang.org/x/tools v0.24.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect
google.golang.org/protobuf v1.35.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.3.0 // indirect
) )
+241 -214
View File
@@ -1,194 +1,229 @@
github.com/Calidity/gin-sessions v1.3.1 h1:nF3dCBWa7TZ4j26iYLwGRmzZy9YODhWoOS3fmi+snyE=
github.com/Calidity/gin-sessions v1.3.1/go.mod h1:I0+QE6qkO50TeN/n6If6novvxHk4Isvr23U8EdvPdns=
github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
github.com/aead/cmac v0.0.0-20160719120800-7af84192f0b1 h1:+JkXLHME8vLJafGhOH4aoV2Iu8bR55nU6iKMVfYVLjY=
github.com/aead/cmac v0.0.0-20160719120800-7af84192f0b1/go.mod h1:nuudZmJhzWtx2212z+pkuy7B6nkBqa+xwNXZHL1j8cg=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/boljen/go-bitmap v0.0.0-20151001105940-23cd2fb0ce7d h1:zsO4lp+bjv5XvPTF58Vq+qgmZEYZttJK+CWtSZhKenI= github.com/bytedance/sonic v1.12.3 h1:W2MGa7RCU1QTeYRTPE3+88mVC0yXmsRQRChiyVocVjU=
github.com/boljen/go-bitmap v0.0.0-20151001105940-23cd2fb0ce7d/go.mod h1:f1iKL6ZhUWvbk7PdWVmOaak10o86cqMUYEmn1CZNGEI= github.com/bytedance/sonic v1.12.3/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
github.com/bufbuild/protocompile v0.6.0 h1:Uu7WiSQ6Yj9DbkdnOe7U4mNKp58y9WDMKDn28/ZlunY= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bufbuild/protocompile v0.6.0/go.mod h1:YNP35qEYoYGme7QMtz5SBCoN4kL4g12jTtjuzRNdjpE= github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= github.com/caddyserver/certmagic v0.20.0 h1:bTw7LcEZAh9ucYCRXyCpIrSAGplplI0vGYJ4BpCQ/Fc=
github.com/bytedance/sonic v1.11.1 h1:JC0+6c9FoWYYxakaoa+c5QTtJeiSZNeByOBhXtAFSn4= github.com/caddyserver/certmagic v0.20.0/go.mod h1:N4sXgpICQUskEWpj7zVzvWD41p3NYacrNoZYiRM2jTg=
github.com/bytedance/sonic v1.11.1/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0= github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo=
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI=
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 h1:y7y0Oa6UawqTFPCDw9JG6pdKt4F9pAhHv0B7FMGaGD0= github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE=
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/ebfe/bcrypt_pbkdf v0.0.0-20140212075826-3c8d2dcb253a h1:YtdtTUN1iH97s+6PUjLnaiKSQj4oG1/EZ3N9bx6g4kU= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/ebfe/bcrypt_pbkdf v0.0.0-20140212075826-3c8d2dcb253a/go.mod h1:/CZpbhAusDOobpcb9yubw46kdYjq0zRC0Wpg9a9zFQM= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc=
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk= github.com/gin-contrib/gzip v1.0.1 h1:HQ8ENHODeLY7a4g1Au/46Z92bdGFl74OhxcZble9WJE=
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI= github.com/gin-contrib/gzip v1.0.1/go.mod h1:njt428fdUNRvjuJf16tZMYZ2Yl+WQB53X5wmhDwXvC4=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= github.com/gin-contrib/sessions v1.0.1 h1:3hsJyNs7v7N8OtelFmYXFrulAf6zSR7nW/putcPEHxI=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= github.com/gin-contrib/sessions v1.0.1/go.mod h1:ouxSFM24/OgIud5MJYQJLpy6AwxQ5EYO9yLhbtObGkM=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
github.com/go-playground/validator/v10 v10.18.0 h1:BvolUXjp4zuvkZ5YN5t7ebzbhlUtPsPm2S9NAZ5nl9U= github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-playground/validator/v10 v10.18.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/gofrs/uuid/v5 v5.3.0 h1:m0mUMr+oVYUdxpMLgSYCZiXe7PuVPnI94+OMeVBNedk=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/gofrs/uuid/v5 v5.3.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a h1:fEBsGL/sjAuJrgah5XqmmYsTLzJp/TO9Lhy39gkverk=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 h1:hR7/MlvK23p6+lIw9SN1TigNLn9ZnF3W4SYRKq2gAHs=
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA=
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
github.com/jhump/protoreflect v1.15.3 h1:6SFRuqU45u9hIZPJAoZ8c28T3nK64BNdp9w6jFonzls=
github.com/jhump/protoreflect v1.15.3/go.mod h1:4ORHmSBmlCW8fh3xHmJMGyul1zNqZK4Elxc8qKP+p1k=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
github.com/klauspost/reedsolomon v1.11.7 h1:9uaHU0slncktTEEg4+7Vl7q7XUNMBUOK4R9gnKhMjAU= github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/reedsolomon v1.11.7/go.mod h1:4bXRN+cVzMdml6ti7qLouuYi32KHJ5MGv0Qd8a47h6A=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/libdns/alidns v1.0.3 h1:LFHuGnbseq5+HCeGa1aW8awyX/4M2psB9962fdD2+yQ=
github.com/lufia/plan9stats v0.0.0-20240226150601-1dcf7310316a h1:3Bm7EwfUQUvhNeKIkUct/gl9eod1TcXuj8stxvi/GoI= github.com/libdns/alidns v1.0.3/go.mod h1:e18uAG6GanfRhcJj6/tps2rCMzQJaYVcGKT+ELjdjGE=
github.com/lufia/plan9stats v0.0.0-20240226150601-1dcf7310316a/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= github.com/libdns/cloudflare v0.1.1 h1:FVPfWwP8zZCqj268LZjmkDleXlHPlFU9KC4OJ3yn054=
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc= github.com/libdns/cloudflare v0.1.1/go.mod h1:9VK91idpOjg6v7/WbjkEW49bSCxj00ALesIFDhJ8PBU=
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg= github.com/libdns/libdns v0.2.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0=
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/metacubex/tfo-go v0.0.0-20241006021335-daedaf0ca7aa h1:9mcjV+RGZVC3reJBNDjjNPyS8PmFG97zq56X7WNaFO4=
github.com/metacubex/tfo-go v0.0.0-20241006021335-daedaf0ca7aa/go.mod h1:4tLB5c8U0CxpkFM+AJJB77jEaVDbLH5XQvy42vAGsWw=
github.com/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30=
github.com/mholt/acmez v1.2.0/go.mod h1:VT9YwH1xgNX1kmYY89gY8xPJC84BFAisjo8Egigt4kE=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mustafaturan/bus v1.0.2 h1:2x3ErwZ0uUPwwZ5ZZoknEQprdaxr68Yl3mY8jDye1Ws= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/mustafaturan/bus v1.0.2/go.mod h1:h7gfehm8TThv4Dcaa+wDQG7r7j6p74v+7ftr0Rq9i1Q= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/mustafaturan/monoton v1.0.0 h1:8SCej+JiNn0lyps7V+Jzc1CRAkDR4EZPWrTupQ61YCQ=
github.com/mustafaturan/monoton v1.0.0/go.mod h1:FOnE7NV3s3EWPXb8/7+/OSdiMBbdlkV0Lz8p1dc+vy8=
github.com/onsi/ginkgo/v2 v2.10.0 h1:sfUl4qgLdvkChZrWCYndY2EAu9BRIw1YphNAzy1VNWs= github.com/onsi/ginkgo/v2 v2.10.0 h1:sfUl4qgLdvkChZrWCYndY2EAu9BRIw1YphNAzy1VNWs=
github.com/onsi/ginkgo/v2 v2.10.0/go.mod h1:UDQOh5wbQUlMnkLfVaIUMtQ1Vus92oM+P2JX1aulgcE= github.com/onsi/ginkgo/v2 v2.10.0/go.mod h1:UDQOh5wbQUlMnkLfVaIUMtQ1Vus92oM+P2JX1aulgcE=
github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU=
github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/oschwald/maxminddb-golang v1.12.0 h1:9FnTOD0YOhP7DGxGsq4glzpGy5+w7pq50AS6wALUMYs=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/oschwald/maxminddb-golang v1.12.0/go.mod h1:q0Nob5lTCqyQ8WT6FYgS1L7PXKVVbgiymefNwIjPzgY=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/sctp v1.8.7 h1:JnABvFakZueGAn4KU/4PSKg+GWbF6QWbKTWZOSGJjXw=
github.com/pion/sctp v1.8.7/go.mod h1:g1Ul+ARqZq5JEmoFy87Q/4CePtKnTJ1QCL9dBBdN6AU=
github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QAlo=
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs= github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
github.com/quic-go/quic-go v0.40.0 h1:GYd1iznlKm7dpHD7pOVpUvItgMPo/jrMgDWZhMCecqw=
github.com/quic-go/quic-go v0.40.0/go.mod h1:PeN7kuVJ4xZbxSv/4OX6S1USOX8MJvydwpTx31vx60c=
github.com/refraction-networking/utls v1.5.4 h1:9k6EO2b8TaOGsQ7Pl7p9w6PUhx18/ZCeT0WNTZ7Uw4o=
github.com/refraction-networking/utls v1.5.4/go.mod h1:SPuDbBmgLGp8s+HLNc83FuavwZCFoMmExj+ltUHiHUw=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkkD2QgdTuzQG263YZ+2emfpeyGqW0=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1 h1:YbmpqPQEMdlk9oFSKYWRqVuu9qzNiOayIonKmv1gCXY=
github.com/secure-io/siv-go v0.0.0-20180922214919-5ff40651e2c4 h1:zOjq+1/uLzn/Xo40stbvjIY/yehG0+mfmlsiEmc0xmQ= github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1/go.mod h1:J2yAxTFPDjrDPhuAi9aWFz2L3ox9it4qAluBBbN0H5k=
github.com/secure-io/siv-go v0.0.0-20180922214919-5ff40651e2c4/go.mod h1:aI+8yClBW+1uovkHw6HM01YXnYB8vohtB9C83wzx34E= github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ=
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U= github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI=
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg= github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs=
github.com/shirou/gopsutil/v3 v3.24.1 h1:R3t6ondCEvmARp3wxODhXMTLC/klMa87h2PHUw5m7QI= github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o=
github.com/shirou/gopsutil/v3 v3.24.1/go.mod h1:UU7a2MSBQa+kW1uuDq8DeEBS8kmrnQwsv2b5O513rwU= github.com/sagernet/gvisor v0.0.0-20241123041152-536d05261cff h1:mlohw3360Wg1BNGook/UHnISXhUx4Gd/3tVLs5T0nSs=
github.com/sagernet/gvisor v0.0.0-20241123041152-536d05261cff/go.mod h1:ehZwnT2UpmOWAHFL48XdBhnd4Qu4hN2O3Ji0us3ZHMw=
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
github.com/sagernet/quic-go v0.48.2-beta.1 h1:W0plrLWa1XtOWDTdX3CJwxmQuxkya12nN5BRGZ87kEg=
github.com/sagernet/quic-go v0.48.2-beta.1/go.mod h1:1WgdDIVD1Gybp40JTWketeSfKA/+or9YMLaG5VeTk4k=
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc=
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
github.com/sagernet/sing v0.6.0-beta.5 h1:RD2j8WmJsvAbbBkAlJWaiYmnd+v/JohBiweoew7kMwo=
github.com/sagernet/sing v0.6.0-beta.5/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing v0.6.0-beta.8 h1:PoxDdN7y8D4oImT3cQ05Sq1ZYnYsJberkUkIEHIGwWE=
github.com/sagernet/sing v0.6.0-beta.8/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing v0.6.0-beta.9 h1:P8lKa5hN53fRNAVCIKy5cWd6/kLO5c4slhdsfehSmHs=
github.com/sagernet/sing v0.6.0-beta.9/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-box v1.11.0-beta.6 h1:MPdL2Yem/xM0RhejCO7krYvl1Zbd1zkSjKluKpHnHPQ=
github.com/sagernet/sing-box v1.11.0-beta.6/go.mod h1:6dO5V0A37cLlhvKnxCmZinSpZXz7ZSk11x3rgI+xH1I=
github.com/sagernet/sing-box v1.11.0-beta.11 h1:bVR0n3oQ3hGcuc/CSS7axsOeRNCRlCGkYVOKl0wxbsw=
github.com/sagernet/sing-box v1.11.0-beta.11/go.mod h1:GZnZUzUHZ6Bgm7D/i8unNORv3537u1s03tLXFdxCRpg=
github.com/sagernet/sing-box v1.11.0-beta.15 h1:oWcs/PHgKaeWKbTfgz/020KEVvDqQv/tQWe7zpyktkc=
github.com/sagernet/sing-box v1.11.0-beta.15/go.mod h1:+QZDsF4HkdiGcMfz+JNOfONLh9CnZjIwJJQNWEzhiaQ=
github.com/sagernet/sing-box v1.11.0-beta.19 h1:uL2xlXpz4t7BduLbXiLe5QqpyiMhvNNRThBzhTJ4p00=
github.com/sagernet/sing-box v1.11.0-beta.19/go.mod h1:UXUN/lwRT9mAM8PK7upPOwgqooOV2vU+CcjBfwT1rYg=
github.com/sagernet/sing-dns v0.4.0-beta.1 h1:W1XkdhigwxDOMgMDVB+9kdomCpb7ExsZfB4acPcTZFY=
github.com/sagernet/sing-dns v0.4.0-beta.1/go.mod h1:8wuFcoFkWM4vJuQyg8e97LyvDwe0/Vl7G839WLcKDs8=
github.com/sagernet/sing-mux v0.3.0-alpha.1 h1:IgNX5bJBpL41gGbp05pdDOvh/b5eUQ6cv9240+Ngipg=
github.com/sagernet/sing-mux v0.3.0-alpha.1/go.mod h1:FTcImmdfW38Lz7b+HQ+mxxOth1lz4ao8uEnz+MwIJQE=
github.com/sagernet/sing-quic v0.4.0-alpha.4 h1:P9xAx3nIfcqb9M8jfgs0uLm+VxCcaY++FCqaBfHY3dQ=
github.com/sagernet/sing-quic v0.4.0-alpha.4/go.mod h1:h5RkKTmUhudJKzK7c87FPXD5w1bJjVyxMN9+opZcctA=
github.com/sagernet/sing-quic v0.4.0-beta.2 h1:ikoQ7zTR0o/2rlI5H5FeNC0j5bQJJHb1uoyXFRu3yGk=
github.com/sagernet/sing-quic v0.4.0-beta.2/go.mod h1:1UNObFodd8CnS3aCT53x9cigjPSCl3P//8dfBMCwBDM=
github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8=
github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE=
github.com/sagernet/sing-shadowsocks2 v0.2.0 h1:wpZNs6wKnR7mh1wV9OHwOyUr21VkS3wKFHi+8XwgADg=
github.com/sagernet/sing-shadowsocks2 v0.2.0/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
github.com/sagernet/sing-shadowtls v0.2.0-alpha.2 h1:RPrpgAdkP5td0vLfS5ldvYosFjSsZtRPxiyLV6jyKg0=
github.com/sagernet/sing-shadowtls v0.2.0-alpha.2/go.mod h1:0j5XlzKxaWRIEjc1uiSKmVoWb0k+L9QgZVb876+thZA=
github.com/sagernet/sing-tun v0.6.0-beta.2 h1:GK7r2jWKm7RhlJGTq4QadgFcebQia1c3BO3OlYMcQJ0=
github.com/sagernet/sing-tun v0.6.0-beta.2/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE=
github.com/sagernet/sing-tun v0.6.0-beta.6 h1:xaIHoH78MqTSvZqQ4SQto8pC1A+X4qXReDRNaC8DQeI=
github.com/sagernet/sing-tun v0.6.0-beta.6/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE=
github.com/sagernet/sing-tun v0.6.0-beta.7 h1:FCSX8oGBqb0H57AAvfGeeH/jMGYWCOg6XWkN/oeES+0=
github.com/sagernet/sing-tun v0.6.0-beta.7/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE=
github.com/sagernet/sing-tun v0.6.0-beta.7.0.20241229131914-aa9d9c62966f h1:dTnXP0e3LbSa4EpUmuOGhllanKPei4vPKfzlLvk76Pc=
github.com/sagernet/sing-tun v0.6.0-beta.7.0.20241229131914-aa9d9c62966f/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE=
github.com/sagernet/sing-vmess v0.2.0-beta.1 h1:5sXQ23uwNlZuDvygzi0dFtnG0Csm/SNqTjAHXJkpuj4=
github.com/sagernet/sing-vmess v0.2.0-beta.1/go.mod h1:fLyE1emIcvQ5DV8reFWnufquZ7MkCSYM5ThodsR9NrQ=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
github.com/sagernet/utls v1.6.7 h1:Ep3+aJ8FUGGta+II2IEVNUc3EDhaRCZINWkj/LloIA8=
github.com/sagernet/utls v1.6.7/go.mod h1:Uua1TKO/FFuAhLr9rkaVnnrTmmiItzDjv1BUb2+ERwM=
github.com/sagernet/wireguard-go v0.0.1-beta.4 h1:8uyM5fxfEXdu4RH05uOK+v25i3lTNdCYMPSAUJ14FnI=
github.com/sagernet/wireguard-go v0.0.1-beta.4/go.mod h1:jGXij2Gn2wbrWuYNUmmNhf1dwcZtvyAvQoe8Xd8MbUo=
github.com/sagernet/wireguard-go v0.0.1-beta.5 h1:aBEsxJUMEONwOZqKPIkuAcv4zJV5p6XlzEN04CF0FXc=
github.com/sagernet/wireguard-go v0.0.1-beta.5/go.mod h1:jGXij2Gn2wbrWuYNUmmNhf1dwcZtvyAvQoe8Xd8MbUo=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
github.com/shirou/gopsutil/v4 v4.24.12 h1:qvePBOk20e0IKA1QXrIIU+jmk+zEiYVVx06WjBRlZo4=
github.com/shirou/gopsutil/v4 v4.24.12/go.mod h1:DCtMPAad2XceTeIAbGyVfycbYQNBGk2P8cvDi7/VN9o=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
@@ -202,110 +237,102 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4= github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0= github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI=
github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4=
github.com/tklauser/numcpus v0.7.0/go.mod h1:bb6dMVcj8A42tSE7i32fsIUCbQNllK5iDguyOZRUzAY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/v2fly/BrowserBridge v0.0.0-20210430233438-0570fc1d7d08 h1:4Yh46CVE3k/lPq6hUbEdbB1u1anRBXLewm3k+L0iOMc= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/v2fly/BrowserBridge v0.0.0-20210430233438-0570fc1d7d08/go.mod h1:KAuQNm+LWQCOFqdBcUgihPzRpVXRKzGbTNhfEfRZ4wY= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/v2fly/VSign v0.0.0-20201108000810-e2adc24bf848 h1:p1UzXK6VAutXFFQMnre66h7g1BjRKUnLv0HfmmRoz7w=
github.com/v2fly/VSign v0.0.0-20201108000810-e2adc24bf848/go.mod h1:p80Bv154ZtrGpXMN15slDCqc9UGmfBuUzheDFBYaW/M=
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI=
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU=
github.com/v2fly/v2ray-core/v5 v5.13.0 h1:BDJfi3Ftx1NpQlZZPpeWJe3RDqRNyIVBs+YGG4RRMDU=
github.com/v2fly/v2ray-core/v5 v5.13.0/go.mod h1:Bc3gmQWLr8UR7xBSCYI9FbfKuVvqA9lbkeBTWNRRAS4=
github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI=
github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
github.com/xiaokangwang/VLite v0.0.0-20220418190619-cff95160a432 h1:I/ATawgO2RerCq9ACwL0wBB8xNXZdE3J+93MCEHReRs=
github.com/xiaokangwang/VLite v0.0.0-20220418190619-cff95160a432/go.mod h1:QN7Go2ftTVfx0aCTh9RXHV8pkpi0FtmbwQw40dy61wQ=
github.com/xtaci/smux v1.5.24 h1:77emW9dtnOxxOQ5ltR+8BbsX1kzcOxQ5gB+aaV9hXOY=
github.com/xtaci/smux v1.5.24/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.starlark.net v0.0.0-20230612165344-9532f5667272 h1:2/wtqS591wZyD2OsClsVBKRPEvBsQt/Js+fsCiYhwu8= github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
go.starlark.net v0.0.0-20230612165344-9532f5667272/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo= github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=
go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ=
go4.org/netipx v0.0.0-20230303233057-f1b76eb4bb35 h1:nJAwRlGWZZDOD+6wni9KVUNHMpHko/OnRwsrCYeAzPo= github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
go4.org/netipx v0.0.0-20230303233057-f1b76eb4bb35/go.mod h1:TQvodOM+hJTioNQJilmLXu08JNb8i+ccq418+KWu1/Y= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4=
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c h1:NUsgEN92SQQqzfA+YtqYNqYmB3DMMYLlIwUZAQFVFbo= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE=
google.golang.org/grpc v1.62.0 h1:HQKZ/fa1bXkX1oFOvSjmZEUL8wLSaZTjCcLAlmZRtdk= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E= gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE=
gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE= gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
gorm.io/gorm v1.25.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A= gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
gvisor.dev/gvisor v0.0.0-20231020174304-b8a429915ff1 h1:qDCwdCWECGnwQSQC01Dpnp09fRHxJs9PbktotUqG+hs= gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gvisor.dev/gvisor v0.0.0-20231020174304-b8a429915ff1/go.mod h1:8hmigyCdYtw5xJGfQDJzSH5Ju8XEIDBnpyi8+O6GRt8= gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
+14 -13
View File
@@ -8,30 +8,27 @@ import (
"github.com/op/go-logging" "github.com/op/go-logging"
) )
var logger *logging.Logger var (
var logBuffer []struct { logger *logging.Logger
time string logBuffer []struct {
level logging.Level time string
log string level logging.Level
} log string
}
func init() { )
InitLogger(logging.INFO)
}
func InitLogger(level logging.Level) { func InitLogger(level logging.Level) {
newLogger := logging.MustGetLogger("s-ui") newLogger := logging.MustGetLogger("s-ui")
var err error var err error
var backend logging.Backend var backend logging.Backend
var format logging.Formatter var format logging.Formatter
ppid := os.Getppid()
backend, err = logging.NewSyslogBackend("") backend, err = logging.NewSyslogBackend("")
if err != nil { if err != nil {
println(err) fmt.Println("Unable to use syslog: " + err.Error())
backend = logging.NewLogBackend(os.Stderr, "", 0) backend = logging.NewLogBackend(os.Stderr, "", 0)
} }
if ppid > 0 && err != nil { if err != nil {
format = logging.MustStringFormatter(`%{time:2006/01/02 15:04:05} %{level} - %{message}`) format = logging.MustStringFormatter(`%{time:2006/01/02 15:04:05} %{level} - %{message}`)
} else { } else {
format = logging.MustStringFormatter(`%{level} - %{message}`) format = logging.MustStringFormatter(`%{level} - %{message}`)
@@ -45,6 +42,10 @@ func InitLogger(level logging.Level) {
logger = newLogger logger = newLogger
} }
func GetLogger() *logging.Logger {
return logger
}
func Debug(args ...interface{}) { func Debug(args ...interface{}) {
logger.Debug(args...) logger.Debug(args...)
addToBuffer("DEBUG", fmt.Sprint(args...)) addToBuffer("DEBUG", fmt.Sprint(args...))
+5 -1
View File
@@ -1,6 +1,7 @@
package middleware package middleware
import ( import (
"net"
"net/http" "net/http"
"strings" "strings"
@@ -9,7 +10,10 @@ import (
func DomainValidator(domain string) gin.HandlerFunc { func DomainValidator(domain string) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
host := strings.Split(c.Request.Host, ":")[0] host := c.Request.Host
if colonIndex := strings.LastIndex(host, ":"); colonIndex != -1 {
host, _, _ = net.SplitHostPort(c.Request.Host)
}
if host != domain { if host != domain {
c.AbortWithStatus(http.StatusForbidden) c.AbortWithStatus(http.StatusForbidden)
+243 -34
View File
@@ -5,89 +5,298 @@ import (
"s-ui/database" "s-ui/database"
"s-ui/database/model" "s-ui/database/model"
"s-ui/logger" "s-ui/logger"
"strings" "s-ui/util"
"s-ui/util/common"
"time" "time"
"gorm.io/gorm" "gorm.io/gorm"
) )
type ClientService struct { type ClientService struct {
InboundService
} }
func (s *ClientService) GetAll() (string, error) { func (s *ClientService) GetAll() ([]model.Client, error) {
db := database.GetDB() db := database.GetDB()
clients := []model.Client{} clients := []model.Client{}
err := db.Model(model.Client{}).Scan(&clients).Error err := db.Model(model.Client{}).Scan(&clients).Error
if err != nil { if err != nil {
return "", err return nil, err
} }
data, err := json.Marshal(clients) return clients, nil
if err != nil {
return "", err
}
return string(data), nil
} }
func (s *ClientService) Save(tx *gorm.DB, changes []model.Changes) error { func (s *ClientService) Save(tx *gorm.DB, act string, data json.RawMessage, hostname string) ([]uint, error) {
var err error var err error
for _, change := range changes { var inboundIds []uint
client := model.Client{}
err = json.Unmarshal(change.Obj, &client) switch act {
case "new", "edit":
var client model.Client
err = json.Unmarshal(data, &client)
if err != nil { if err != nil {
return err return nil, err
} }
switch change.Action { err = json.Unmarshal(client.Inbounds, &inboundIds)
case "new": if err != nil {
err = tx.Create(&client).Error return nil, err
case "del":
err = tx.Where("id = ?", change.Index).Delete(model.Client{}).Error
default:
err = tx.Save(client).Error
} }
err = s.updateLinksWithFixedInbounds(tx, []*model.Client{&client}, inboundIds, hostname)
if err != nil {
return nil, err
}
err = tx.Save(&client).Error
if err != nil {
return nil, err
}
case "addbulk":
var clients []*model.Client
err = json.Unmarshal(data, &clients)
if err != nil {
return nil, err
}
err = json.Unmarshal(clients[0].Inbounds, &inboundIds)
if err != nil {
return nil, err
}
err = s.updateLinksWithFixedInbounds(tx, clients, inboundIds, hostname)
if err != nil {
return nil, err
}
err = tx.Save(clients).Error
if err != nil {
return nil, err
}
case "del":
var id uint
err = json.Unmarshal(data, &id)
if err != nil {
return nil, err
}
var client model.Client
err = tx.Where("id = ?", id).First(&client).Error
if err != nil {
return nil, err
}
err = json.Unmarshal(client.Inbounds, &inboundIds)
if err != nil {
return nil, err
}
err = tx.Where("id = ?", id).Delete(model.Client{}).Error
if err != nil {
return nil, err
}
default:
return nil, common.NewErrorf("unknown action: %s", act)
}
return inboundIds, nil
}
func (s *ClientService) updateLinksWithFixedInbounds(tx *gorm.DB, clients []*model.Client, inbounIds []uint, hostname string) error {
var err error
var inbounds []model.Inbound
// Zero inbounds means removing local links only
if len(inbounIds) > 0 {
err = tx.Model(model.Inbound{}).Preload("Tls").Where("id in ? and type in ?", inbounIds, util.InboundTypeWithLink).Find(&inbounds).Error
if err != nil { if err != nil {
return err return err
} }
} }
return err for index, client := range clients {
var clientLinks []map[string]string
err = json.Unmarshal(client.Links, &clientLinks)
if err != nil {
return err
}
newClientLinks := []map[string]string{}
for _, inbound := range inbounds {
newLinks := util.LinkGenerator(client.Config, &inbound, hostname)
for _, newLink := range newLinks {
newClientLinks = append(newClientLinks, map[string]string{
"remark": inbound.Tag,
"type": "local",
"uri": newLink,
})
}
}
// Add no local links
for _, clientLink := range clientLinks {
if clientLink["type"] != "local" {
newClientLinks = append(newClientLinks, clientLink)
}
}
clients[index].Links, err = json.MarshalIndent(newClientLinks, "", " ")
if err != nil {
return err
}
}
return nil
} }
func (s *ClientService) DepleteClients() ([]string, []string, error) { func (s *ClientService) UpdateClientsOnInboundDelete(tx *gorm.DB, id uint, tag string) error {
var clients []model.Client
err := tx.Table("clients").
Where("EXISTS (SELECT 1 FROM json_each(clients.inbounds) WHERE json_each.value = ?)", id).
Find(&clients).Error
if err != nil {
return err
}
for _, client := range clients {
// Delete inbounds
var clientInbounds, newClientInbounds []uint
json.Unmarshal(client.Inbounds, &clientInbounds)
for _, clientInbound := range clientInbounds {
if clientInbound != id {
newClientInbounds = append(newClientInbounds, clientInbound)
}
}
client.Inbounds, err = json.MarshalIndent(newClientInbounds, "", " ")
if err != nil {
return err
}
// Delete links
var clientLinks, newClientLinks []map[string]string
json.Unmarshal(client.Links, &clientLinks)
for _, clientLink := range clientLinks {
if clientLink["remark"] != tag {
newClientLinks = append(newClientLinks, clientLink)
}
}
client.Links, err = json.MarshalIndent(newClientLinks, "", " ")
if err != nil {
return err
}
err = tx.Save(&client).Error
if err != nil {
return err
}
}
return nil
}
func (s *ClientService) UpdateLinksByInboundChange(tx *gorm.DB, inbounIds []uint, hostname string) error {
var inbounds []model.Inbound
err := tx.Model(model.Inbound{}).Preload("Tls").Where("id in ? and type in ?", inbounIds, util.InboundTypeWithLink).Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound {
return err
}
for _, inbound := range inbounds {
var clients []model.Client
err = tx.Table("clients").
Where("EXISTS (SELECT 1 FROM json_each(clients.inbounds) WHERE json_each.value = ?)", inbound.Id).
Find(&clients).Error
if err != nil {
return err
}
for _, client := range clients {
var clientLinks, newClientLinks []map[string]string
json.Unmarshal(client.Links, &clientLinks)
newLinks := util.LinkGenerator(client.Config, &inbound, hostname)
for _, newLink := range newLinks {
newClientLinks = append(newClientLinks, map[string]string{
"remark": inbound.Tag,
"type": "local",
"uri": newLink,
})
}
for _, clientLink := range clientLinks {
if clientLink["remark"] != inbound.Tag {
newClientLinks = append(newClientLinks, clientLink)
}
}
client.Links, err = json.MarshalIndent(newClientLinks, "", " ")
if err != nil {
return err
}
err = tx.Save(&client).Error
if err != nil {
return err
}
}
}
return nil
}
func (s *ClientService) DepleteClients() error {
var err error var err error
var clients []model.Client var clients []model.Client
var changes []model.Changes var changes []model.Changes
var users []string
var inboundIds []uint
now := time.Now().Unix() now := time.Now().Unix()
db := database.GetDB() db := database.GetDB()
err = db.Model(model.Client{}).Where("enable = true AND ((volume >0 AND up+down > volume) OR (expiry > 0 AND expiry < ?))", now).Scan(&clients).Error
tx := db.Begin()
defer func() {
if err == nil {
tx.Commit()
if len(inboundIds) > 0 && corePtr.IsRunning() {
err1 := s.InboundService.RestartInbounds(tx, inboundIds)
if err1 != nil {
logger.Error("unable to restart inbounds: ", err1)
}
}
} else {
tx.Rollback()
}
}()
err = tx.Model(model.Client{}).Where("enable = true AND ((volume >0 AND up+down > volume) OR (expiry > 0 AND expiry < ?))", now).Scan(&clients).Error
if err != nil { if err != nil {
return nil, nil, err return err
} }
var users, inbounds []string dt := time.Now().Unix()
for _, client := range clients { for _, client := range clients {
logger.Debug("Client ", client.Name, " is going to be disabled") logger.Debug("Client ", client.Name, " is going to be disabled")
users = append(users, client.Name) users = append(users, client.Name)
userInbounds := strings.Split(client.Inbounds, ",") var userInbounds []uint
inbounds = append(inbounds, userInbounds...) json.Unmarshal(client.Inbounds, &userInbounds)
inboundIds = s.uniqueAppendInboundIds(inboundIds, userInbounds)
changes = append(changes, model.Changes{ changes = append(changes, model.Changes{
DateTime: time.Now().Unix(), DateTime: dt,
Actor: "DepleteJob", Actor: "DepleteJob",
Key: "clients", Key: "clients",
Action: "disable", Action: "disable",
Obj: json.RawMessage(client.Name), Obj: json.RawMessage("\"" + client.Name + "\""),
}) })
} }
// Save changes // Save changes
if len(changes) > 0 { if len(changes) > 0 {
err = db.Model(model.Client{}).Where("enable = true AND ((volume >0 AND up+down > volume) OR (expiry > 0 AND expiry < ?))", now).Update("enable", false).Error err = tx.Model(model.Client{}).Where("enable = true AND ((volume >0 AND up+down > volume) OR (expiry > 0 AND expiry < ?))", now).Update("enable", false).Error
if err != nil { if err != nil {
return nil, nil, err return err
} }
err = db.Model(model.Changes{}).Create(&changes).Error err = tx.Model(model.Changes{}).Create(&changes).Error
if err != nil { if err != nil {
return nil, nil, err return err
} }
LastUpdate = dt
} }
return users, inbounds, nil return nil
}
// avoid duplicate inboundIds
func (s *ClientService) uniqueAppendInboundIds(a []uint, b []uint) []uint {
m := make(map[uint]bool)
for _, v := range a {
m[v] = true
}
for _, v := range b {
m[v] = true
}
var res []uint
for k := range m {
res = append(res, k)
}
return res
} }
+193 -257
View File
@@ -2,20 +2,27 @@ package service
import ( import (
"encoding/json" "encoding/json"
"os" "s-ui/core"
"s-ui/config"
"s-ui/database" "s-ui/database"
"s-ui/database/model" "s-ui/database/model"
"s-ui/singbox" "s-ui/logger"
"s-ui/util/common"
"strconv"
"time" "time"
) )
var ApiAddr string var (
LastUpdate int64
corePtr *core.Core
)
type ConfigService struct { type ConfigService struct {
ClientService ClientService
singbox.Controller TlsService
SettingService SettingService
InboundService
OutboundService
EndpointService
} }
type SingBoxConfig struct { type SingBoxConfig struct {
@@ -24,66 +31,95 @@ type SingBoxConfig struct {
Ntp json.RawMessage `json:"ntp"` Ntp json.RawMessage `json:"ntp"`
Inbounds []json.RawMessage `json:"inbounds"` Inbounds []json.RawMessage `json:"inbounds"`
Outbounds []json.RawMessage `json:"outbounds"` Outbounds []json.RawMessage `json:"outbounds"`
Endpoints []json.RawMessage `json:"endpoints"`
Route json.RawMessage `json:"route"` Route json.RawMessage `json:"route"`
Experimental json.RawMessage `json:"experimental"` Experimental json.RawMessage `json:"experimental"`
} }
func NewConfigService() *ConfigService { func NewConfigService(core *core.Core) *ConfigService {
corePtr = core
return &ConfigService{} return &ConfigService{}
} }
func (s *ConfigService) InitConfig() error { func (s *ConfigService) GetConfig(data string) (*SingBoxConfig, error) {
configPath := config.GetBinFolderPath() var err error
data, err := os.ReadFile(configPath + "/config.json") if len(data) == 0 {
if err != nil { data, err = s.SettingService.GetConfig()
if os.IsNotExist(err) { if err != nil {
defaultConfig := []byte(config.GetDefaultConfig()) return nil, err
err = os.MkdirAll(configPath, 01764)
if err != nil {
return err
}
err = os.WriteFile(configPath+"/config.json", defaultConfig, 0764)
if err != nil {
return err
}
data = defaultConfig
} else {
return err
} }
} }
return s.RefreshApiAddr(&data) singboxConfig := SingBoxConfig{}
} err = json.Unmarshal([]byte(data), &singboxConfig)
func (s *ConfigService) GetConfig() (*[]byte, error) {
configPath := config.GetBinFolderPath()
data, err := os.ReadFile(configPath + "/config.json")
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &data, nil
singboxConfig.Inbounds, err = s.InboundService.GetAllConfig(database.GetDB())
if err != nil {
return nil, err
}
singboxConfig.Outbounds, err = s.OutboundService.GetAllConfig(database.GetDB())
if err != nil {
return nil, err
}
singboxConfig.Endpoints, err = s.EndpointService.GetAllConfig(database.GetDB())
if err != nil {
return nil, err
}
return &singboxConfig, nil
} }
func (s *ConfigService) SaveChanges(changes map[string]string, loginUser string) error { func (s *ConfigService) StartCore(defaultConfig string) error {
if corePtr.IsRunning() {
return nil
}
singboxConfig, err := s.GetConfig(defaultConfig)
if err != nil {
return err
}
rawConfig, err := json.MarshalIndent(singboxConfig, "", " ")
if err != nil {
return err
}
err = corePtr.Start(rawConfig)
if err != nil {
logger.Error("start sing-box err:", err.Error())
return err
}
logger.Info("sing-box started")
return nil
}
func (s *ConfigService) RestartCore() error {
err := s.StopCore()
if err != nil {
return err
}
return s.StartCore("")
}
func (s *ConfigService) restartCoreWithConfig(config json.RawMessage) error {
err := s.StopCore()
if err != nil {
return err
}
return s.StartCore(string(config))
}
func (s *ConfigService) StopCore() error {
err := corePtr.Stop()
if err != nil {
return err
}
logger.Info("sing-box stopped")
return nil
}
func (s *ConfigService) Save(obj string, act string, data json.RawMessage, loginUser string, hostname string) ([]string, error) {
var err error var err error
var clientChanges, settingChanges, configChanges []model.Changes var inboundIds []uint
if _, ok := changes["clients"]; ok { var inboundId uint
err = json.Unmarshal([]byte(changes["clients"]), &clientChanges)
if err != nil {
return err
}
}
if _, ok := changes["settings"]; ok {
err = json.Unmarshal([]byte(changes["settings"]), &settingChanges)
if err != nil {
return err
}
}
if _, ok := changes["config"]; ok {
err = json.Unmarshal([]byte(changes["config"]), &configChanges)
if err != nil {
return err
}
}
db := database.GetDB() db := database.GetDB()
tx := db.Begin() tx := db.Begin()
@@ -95,230 +131,130 @@ func (s *ConfigService) SaveChanges(changes map[string]string, loginUser string)
} }
}() }()
if len(clientChanges) > 0 { switch obj {
err = s.ClientService.Save(tx, clientChanges) case "clients":
inboundIds, err = s.ClientService.Save(tx, act, data, hostname)
case "tls":
inboundIds, err = s.TlsService.Save(tx, act, data)
case "inbounds":
inboundId, err = s.InboundService.Save(tx, act, data, hostname)
case "outbounds":
err = s.OutboundService.Save(tx, act, data)
case "endpoints":
err = s.EndpointService.Save(tx, act, data)
case "config":
err = s.SettingService.SaveConfig(tx, data)
if err != nil { if err != nil {
return err return nil, err
} }
err = s.restartCoreWithConfig(data)
default:
return nil, common.NewError("unknown object: ", obj)
} }
if len(settingChanges) > 0 {
err = s.SettingService.Save(tx, settingChanges)
if err != nil {
return err
}
}
if len(configChanges) > 0 {
singboxConfig, err := s.GetConfig()
if err != nil {
return err
}
newConfig := SingBoxConfig{}
err = json.Unmarshal(*singboxConfig, &newConfig)
if err != nil {
return err
}
for _, change := range configChanges {
rawObject := change.Obj
switch change.Key {
case "all":
err = json.Unmarshal(rawObject, &newConfig)
if err != nil {
return err
}
case "log":
newConfig.Log = rawObject
case "dns":
newConfig.Dns = rawObject
case "ntp":
newConfig.Ntp = rawObject
case "route":
newConfig.Route = rawObject
case "experimental":
newConfig.Experimental = rawObject
case "inbounds":
if change.Action == "edit" {
newConfig.Inbounds[change.Index] = rawObject
} else if change.Action == "del" {
newConfig.Inbounds = append(newConfig.Inbounds[:change.Index], newConfig.Inbounds[change.Index+1:]...)
} else {
newConfig.Inbounds = append(newConfig.Inbounds, rawObject)
}
case "outbounds":
if change.Action == "edit" {
newConfig.Outbounds[change.Index] = rawObject
} else if change.Action == "del" {
newConfig.Outbounds = append(newConfig.Outbounds[:change.Index], newConfig.Outbounds[change.Index+1:]...)
} else {
newConfig.Outbounds = append(newConfig.Outbounds, rawObject)
}
}
}
// Save to config.json
data, err := json.MarshalIndent(newConfig, "", " ")
if err != nil {
return err
}
err = s.Save(&data)
if err != nil {
return err
}
}
// Log changes
dt := time.Now().Unix()
allChanges := append(append(clientChanges, settingChanges...), configChanges...)
for index := range allChanges {
allChanges[index].DateTime = dt
allChanges[index].Actor = loginUser
}
err = tx.Model(model.Changes{}).Create(&allChanges).Error
if err != nil { if err != nil {
return err return nil, err
} }
return nil dt := time.Now().Unix()
err = tx.Create(&model.Changes{
DateTime: dt,
Actor: loginUser,
Key: obj,
Action: act,
Obj: data,
}).Error
if err != nil {
return nil, err
}
// Commit changes so far
tx.Commit()
LastUpdate = time.Now().Unix()
var objs []string = []string{obj}
tx = db.Begin()
// Update side changes
// Update client links
if obj == "tls" && len(inboundIds) > 0 {
err = s.ClientService.UpdateLinksByInboundChange(tx, inboundIds, hostname)
if err != nil {
return nil, err
}
objs = append(objs, "clients")
}
if obj == "inbounds" && act != "add" {
switch act {
case "edit":
err = s.ClientService.UpdateLinksByInboundChange(tx, []uint{inboundId}, hostname)
case "del":
var tag string
err = json.Unmarshal(data, &tag)
if err != nil {
return nil, err
}
err = s.ClientService.UpdateClientsOnInboundDelete(tx, inboundId, tag)
}
if err != nil {
return nil, err
}
objs = append(objs, "clients")
}
// Update out_json of inbounds when tls is changed
if obj == "tls" && len(inboundIds) > 0 {
err = s.InboundService.UpdateOutJsons(tx, inboundIds, hostname)
if err != nil {
return nil, common.NewError("unable to update out_json of inbounds: ", err.Error())
}
objs = append(objs, "inbounds")
}
if len(inboundIds) > 0 && corePtr.IsRunning() {
err1 := s.InboundService.RestartInbounds(tx, inboundIds)
if err1 != nil {
logger.Error("unable to restart inbounds: ", err1)
}
}
// Try to start core if it is not running
if !corePtr.IsRunning() {
s.StartCore("")
}
return objs, nil
} }
func (s *ConfigService) CheckChnages(lu string) (bool, error) { func (s *ConfigService) CheckChanges(lu string) (bool, error) {
if lu == "" { if lu == "" {
return true, nil return true, nil
} }
db := database.GetDB() if LastUpdate == 0 {
var count int64 db := database.GetDB()
err := db.Model(model.Changes{}).Where("date_time > " + lu).Count(&count).Error var count int64
return count > 0, err err := db.Model(model.Changes{}).Where("date_time > " + lu).Count(&count).Error
} if err == nil {
LastUpdate = time.Now().Unix()
func (s *ConfigService) Save(data *[]byte) error {
configPath := config.GetBinFolderPath()
_, err := os.Stat(configPath + "/config.json")
if os.IsNotExist(err) {
err = os.MkdirAll(configPath, 01764)
if err != nil {
return err
} }
} else if err != nil { return count > 0, err
return err
}
err = os.WriteFile(configPath+"/config.json", *data, 0764)
if err != nil {
return err
}
s.RefreshApiAddr(data)
s.Controller.Restart()
return nil
}
func (s *ConfigService) RefreshApiAddr(data *[]byte) error {
Env_API := config.GetEnvApi()
if len(Env_API) > 0 {
ApiAddr = Env_API
} else { } else {
var err error intLu, err := strconv.ParseInt(lu, 10, 64)
if data == nil { return LastUpdate > intLu, err
data, err = s.GetConfig()
if err != nil {
return err
}
}
singboxConfig := SingBoxConfig{}
err = json.Unmarshal(*data, &singboxConfig)
if err != nil {
return err
}
var experimental struct {
V2rayApi struct {
Listen string `json:"listen"`
Stats interface{} `jaon:"stats"`
} `json:"v2ray_api"`
}
err = json.Unmarshal(singboxConfig.Experimental, &experimental)
if err != nil {
return err
}
ApiAddr = experimental.V2rayApi.Listen
} }
return nil
} }
func (s *ConfigService) DepleteClients() error { func (s *ConfigService) GetChanges(actor string, chngKey string, count string) []model.Changes {
users, inbounds, err := s.ClientService.DepleteClients() c, _ := strconv.Atoi(count)
if err != nil || len(users) == 0 || len(inbounds) == 0 { whereString := "`id`>0"
return err if len(actor) > 0 {
whereString += " and `actor`='" + actor + "'"
} }
if len(chngKey) > 0 {
singboxConfig, err := s.GetConfig() whereString += " and `key`='" + chngKey + "'"
}
db := database.GetDB()
var chngs []model.Changes
err := db.Model(model.Changes{}).Where(whereString).Order("`id` desc").Limit(c).Scan(&chngs).Error
if err != nil { if err != nil {
return err logger.Warning(err)
} }
newConfig := SingBoxConfig{} return chngs
err = json.Unmarshal(*singboxConfig, &newConfig)
if err != nil {
return err
}
for inbound_index, inbound := range newConfig.Inbounds {
var inboundJson map[string]interface{}
json.Unmarshal(inbound, &inboundJson)
if s.contains(inbounds, inboundJson["tag"].(string)) {
inbound_users, ok := inboundJson["users"].([]interface{})
if ok {
var updatedUsers []interface{}
for _, user := range inbound_users {
userMap, ok := user.(map[string]interface{})
if ok {
name, exists := userMap["name"].(string)
if exists && s.contains(users, name) {
// Skip the user exists
continue
}
username, exists := userMap["username"].(string)
if exists && s.contains(users, username) {
// Skip the username exists
continue
}
}
updatedUsers = append(updatedUsers, user)
}
// Exception for Naive and ShadowTLSv3
if len(updatedUsers) == 0 {
if inboundJson["type"].(string) == "naive" ||
(inboundJson["type"].(string) == "shadowtls" &&
inboundJson["version"].(float64) == 3) {
updatedUsers = append(updatedUsers, make(map[string]interface{}))
}
}
inboundJson["users"] = updatedUsers
}
}
modifiedInbound, err := json.MarshalIndent(inboundJson, "", " ")
if err != nil {
return err
}
newConfig.Inbounds[inbound_index] = modifiedInbound
}
modifiedConfig, err := json.MarshalIndent(newConfig, "", " ")
if err != nil {
return err
}
err = s.Save(&modifiedConfig)
if err != nil {
return err
}
return nil
}
func (s *ConfigService) contains(slice []string, item string) bool {
for _, str := range slice {
if str == item {
return true
}
}
return false
} }
+117
View File
@@ -0,0 +1,117 @@
package service
import (
"encoding/json"
"os"
"s-ui/database"
"s-ui/database/model"
"s-ui/util/common"
"gorm.io/gorm"
)
type EndpointService struct{}
func (o *EndpointService) GetAll() (*[]map[string]interface{}, error) {
db := database.GetDB()
endpoints := []*model.Endpoint{}
err := db.Model(model.Endpoint{}).Scan(&endpoints).Error
if err != nil {
return nil, err
}
var data []map[string]interface{}
for _, endpoint := range endpoints {
epData := map[string]interface{}{
"id": endpoint.Id,
"type": endpoint.Type,
"tag": endpoint.Tag,
}
if endpoint.Options != nil {
var restFields map[string]json.RawMessage
if err := json.Unmarshal(endpoint.Options, &restFields); err != nil {
return nil, err
}
for k, v := range restFields {
epData[k] = v
}
}
data = append(data, epData)
}
return &data, nil
}
func (o *EndpointService) GetAllConfig(db *gorm.DB) ([]json.RawMessage, error) {
var endpointsJson []json.RawMessage
var endpoints []*model.Endpoint
err := db.Model(model.Endpoint{}).Scan(&endpoints).Error
if err != nil {
return nil, err
}
for _, endpoint := range endpoints {
endpointJson, err := endpoint.MarshalJSON()
if err != nil {
return nil, err
}
endpointsJson = append(endpointsJson, endpointJson)
}
return endpointsJson, nil
}
func (s *EndpointService) Save(tx *gorm.DB, act string, data json.RawMessage) error {
var err error
switch act {
case "new", "edit":
var endpoint model.Endpoint
err = endpoint.UnmarshalJSON(data)
if err != nil {
return err
}
if corePtr.IsRunning() {
configData, err := endpoint.MarshalJSON()
if err != nil {
return err
}
if act == "edit" {
var oldTag string
err = tx.Model(model.Endpoint{}).Select("tag").Where("id = ?", endpoint.Id).Find(&oldTag).Error
if err != nil {
return err
}
err = corePtr.RemoveEndpoint(oldTag)
if err != nil && err != os.ErrInvalid {
return err
}
}
err = corePtr.AddEndpoint(configData)
if err != nil {
return err
}
}
err = tx.Save(&endpoint).Error
if err != nil {
return err
}
case "del":
var tag string
err = json.Unmarshal(data, &tag)
if err != nil {
return err
}
if corePtr.IsRunning() {
err = corePtr.RemoveEndpoint(tag)
if err != nil && err != os.ErrInvalid {
return err
}
}
err = tx.Where("tag = ?", tag).Delete(model.Endpoint{}).Error
if err != nil {
return err
}
default:
return common.NewErrorf("unknown action: %s", act)
}
return nil
}
+262
View File
@@ -0,0 +1,262 @@
package service
import (
"encoding/json"
"os"
"s-ui/database"
"s-ui/database/model"
"s-ui/util"
"s-ui/util/common"
"strings"
"gorm.io/gorm"
)
type InboundService struct{}
func (s *InboundService) Get(ids string) (*[]map[string]interface{}, error) {
if ids == "" {
return s.GetAll()
}
return s.getById(ids)
}
func (s *InboundService) getById(ids string) (*[]map[string]interface{}, error) {
var inbound []model.Inbound
var result []map[string]interface{}
db := database.GetDB()
err := db.Model(model.Inbound{}).Where("id in ?", strings.Split(ids, ",")).Scan(&inbound).Error
if err != nil {
return nil, err
}
for _, inb := range inbound {
inbData, err := inb.MarshalFull()
if err != nil {
return nil, err
}
result = append(result, *inbData)
}
return &result, nil
}
func (s *InboundService) GetAll() (*[]map[string]interface{}, error) {
db := database.GetDB()
inbounds := []model.Inbound{}
err := db.Model(model.Inbound{}).Scan(&inbounds).Error
if err != nil {
return nil, err
}
var data []map[string]interface{}
for _, inbound := range inbounds {
inbData := map[string]interface{}{
"id": inbound.Id,
"type": inbound.Type,
"tag": inbound.Tag,
"tls_id": inbound.TlsId,
}
if inbound.Options != nil {
var restFields map[string]json.RawMessage
if err := json.Unmarshal(inbound.Options, &restFields); err != nil {
return nil, err
}
inbData["listen"] = restFields["listen"]
inbData["listen_port"] = restFields["listen_port"]
}
data = append(data, inbData)
}
return &data, nil
}
func (s *InboundService) FromIds(ids []uint) ([]*model.Inbound, error) {
db := database.GetDB()
inbounds := []*model.Inbound{}
err := db.Model(model.Inbound{}).Where("id in ?", ids).Scan(&inbounds).Error
if err != nil {
return nil, err
}
return inbounds, nil
}
func (s *InboundService) Save(tx *gorm.DB, act string, data json.RawMessage, hostname string) (uint, error) {
var err error
var id uint
switch act {
case "new", "edit":
var inbound model.Inbound
err = inbound.UnmarshalJSON(data)
if err != nil {
return 0, err
}
id = inbound.Id
if inbound.TlsId > 0 {
err = tx.Model(model.Tls{}).Where("id = ?", inbound.TlsId).Find(&inbound.Tls).Error
if err != nil {
return 0, err
}
}
if corePtr.IsRunning() {
if act == "edit" {
var oldTag string
err = tx.Model(model.Inbound{}).Select("tag").Where("id = ?", inbound.Id).Find(&oldTag).Error
if err != nil {
return 0, err
}
err = corePtr.RemoveInbound(oldTag)
if err != nil && err != os.ErrInvalid {
return 0, err
}
}
inboundConfig, err := inbound.MarshalJSON()
if err != nil {
return 0, err
}
inboundConfig, err = s.addUsers(tx, inboundConfig, inbound.Id, inbound.Type)
if err != nil {
return 0, err
}
err = corePtr.AddInbound(inboundConfig)
if err != nil {
return 0, err
}
}
err = util.FillOutJson(&inbound, hostname)
if err != nil {
return 0, err
}
err = tx.Save(&inbound).Error
if err != nil {
return 0, err
}
case "del":
var tag string
err = json.Unmarshal(data, &tag)
if err != nil {
return 0, err
}
if corePtr.IsRunning() {
err = corePtr.RemoveInbound(tag)
if err != nil && err != os.ErrInvalid {
return 0, err
}
}
err = tx.Model(model.Inbound{}).Select("id").Where("tag = ?", tag).Scan(&id).Error
if err != nil {
return 0, err
}
err = tx.Where("tag = ?", tag).Delete(model.Inbound{}).Error
if err != nil {
return 0, err
}
default:
return 0, common.NewErrorf("unknown action: %s", act)
}
return id, nil
}
func (s *InboundService) UpdateOutJsons(tx *gorm.DB, inboundIds []uint, hostname string) error {
var inbounds []model.Inbound
err := tx.Model(model.Inbound{}).Preload("Tls").Where("id in ?", inboundIds).Find(&inbounds).Error
if err != nil {
return err
}
for _, inbound := range inbounds {
err = util.FillOutJson(&inbound, hostname)
if err != nil {
return err
}
err = tx.Model(model.Inbound{}).Where("tag = ?", inbound.Tag).Update("out_json", inbound.OutJson).Error
if err != nil {
return err
}
}
return nil
}
func (s *InboundService) GetAllConfig(db *gorm.DB) ([]json.RawMessage, error) {
var inboundsJson []json.RawMessage
var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Preload("Tls").Find(&inbounds).Error
if err != nil {
return nil, err
}
for _, inbound := range inbounds {
inboundJson, err := inbound.MarshalJSON()
if err != nil {
return nil, err
}
inboundJson, err = s.addUsers(db, inboundJson, inbound.Id, inbound.Type)
if err != nil {
return nil, err
}
inboundsJson = append(inboundsJson, inboundJson)
}
return inboundsJson, nil
}
func (s *InboundService) addUsers(db *gorm.DB, inboundJson []byte, inboundId uint, inboundType string) ([]byte, error) {
switch inboundType {
case "mixed", "socks", "http", "shadowsocks", "vmess", "trojan", "naive", "hysteria", "shadowtls", "tuic", "hysteria2", "vless":
break
default:
return inboundJson, nil
}
var inbound map[string]interface{}
err := json.Unmarshal(inboundJson, &inbound)
if err != nil {
return nil, err
}
if inboundType == "shadowsocks" {
method, _ := inbound["method"].(string)
if method == "2022-blake3-aes-128-gcm" {
inboundType = "shadowsocks16"
}
}
var users []string
err = db.Raw(`SELECT json_extract(clients.config, ?)
FROM clients, json_each(clients.inbounds) as je
WHERE clients.enable = true AND je.value = ?;`,
"$."+inboundType, inboundId).Scan(&users).Error
if err != nil {
return nil, err
}
var usersJson []json.RawMessage
for _, user := range users {
usersJson = append(usersJson, json.RawMessage(user))
}
inbound["users"] = usersJson
return json.Marshal(inbound)
}
func (s *InboundService) RestartInbounds(tx *gorm.DB, ids []uint) error {
var inbounds []*model.Inbound
err := tx.Model(model.Inbound{}).Preload("Tls").Where("id in ?", ids).Find(&inbounds).Error
if err != nil {
return err
}
for _, inbound := range inbounds {
err = corePtr.RemoveInbound(inbound.Tag)
if err != nil && err != os.ErrInvalid {
return err
}
inboundConfig, err := inbound.MarshalJSON()
if err != nil {
return err
}
inboundConfig, err = s.addUsers(tx, inboundConfig, inbound.Id, inbound.Type)
err = corePtr.AddInbound(inboundConfig)
if err != nil {
return err
}
}
return nil
}
+117
View File
@@ -0,0 +1,117 @@
package service
import (
"encoding/json"
"os"
"s-ui/database"
"s-ui/database/model"
"s-ui/util/common"
"gorm.io/gorm"
)
type OutboundService struct{}
func (o *OutboundService) GetAll() (*[]map[string]interface{}, error) {
db := database.GetDB()
outbounds := []*model.Outbound{}
err := db.Model(model.Outbound{}).Scan(&outbounds).Error
if err != nil {
return nil, err
}
var data []map[string]interface{}
for _, outbound := range outbounds {
outData := map[string]interface{}{
"id": outbound.Id,
"type": outbound.Type,
"tag": outbound.Tag,
}
if outbound.Options != nil {
var restFields map[string]json.RawMessage
if err := json.Unmarshal(outbound.Options, &restFields); err != nil {
return nil, err
}
for k, v := range restFields {
outData[k] = v
}
}
data = append(data, outData)
}
return &data, nil
}
func (o *OutboundService) GetAllConfig(db *gorm.DB) ([]json.RawMessage, error) {
var outboundsJson []json.RawMessage
var outbounds []*model.Outbound
err := db.Model(model.Outbound{}).Scan(&outbounds).Error
if err != nil {
return nil, err
}
for _, outbound := range outbounds {
outboundJson, err := outbound.MarshalJSON()
if err != nil {
return nil, err
}
outboundsJson = append(outboundsJson, outboundJson)
}
return outboundsJson, nil
}
func (s *OutboundService) Save(tx *gorm.DB, act string, data json.RawMessage) error {
var err error
switch act {
case "new", "edit":
var outbound model.Outbound
err = outbound.UnmarshalJSON(data)
if err != nil {
return err
}
if corePtr.IsRunning() {
configData, err := outbound.MarshalJSON()
if err != nil {
return err
}
if act == "edit" {
var oldTag string
err = tx.Model(model.Outbound{}).Select("tag").Where("id = ?", outbound.Id).Find(&oldTag).Error
if err != nil {
return err
}
err = corePtr.RemoveOutbound(oldTag)
if err != nil && err != os.ErrInvalid {
return err
}
}
err = corePtr.AddOutbound(configData)
if err != nil {
return err
}
}
err = tx.Save(&outbound).Error
if err != nil {
return err
}
case "del":
var tag string
err = json.Unmarshal(data, &tag)
if err != nil {
return err
}
if corePtr.IsRunning() {
err = corePtr.RemoveOutbound(tag)
if err != nil && err != os.ErrInvalid {
return err
}
}
err = tx.Where("tag = ?", tag).Delete(model.Outbound{}).Error
if err != nil {
return err
}
default:
return common.NewErrorf("unknown action: %s", act)
}
return nil
}
+82 -15
View File
@@ -1,21 +1,24 @@
package service package service
import ( import (
"encoding/base64"
"os" "os"
"runtime" "runtime"
"s-ui/config" "s-ui/config"
"s-ui/logger" "s-ui/logger"
"strconv"
"strings" "strings"
"time"
"github.com/shirou/gopsutil/v3/cpu" "github.com/sagernet/sing-box/common/tls"
"github.com/shirou/gopsutil/v3/host" "github.com/shirou/gopsutil/v4/cpu"
"github.com/shirou/gopsutil/v3/mem" "github.com/shirou/gopsutil/v4/host"
"github.com/shirou/gopsutil/v3/net" "github.com/shirou/gopsutil/v4/mem"
"github.com/shirou/gopsutil/v4/net"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
) )
type ServerService struct { type ServerService struct{}
SingBoxService
}
func (s *ServerService) GetStatus(request string) *map[string]interface{} { func (s *ServerService) GetStatus(request string) *map[string]interface{} {
status := make(map[string]interface{}, 0) status := make(map[string]interface{}, 0)
@@ -88,15 +91,21 @@ func (s *ServerService) GetNetInfo() map[string]interface{} {
} }
func (s *ServerService) GetSingboxInfo() map[string]interface{} { func (s *ServerService) GetSingboxInfo() map[string]interface{} {
info := make(map[string]interface{}, 0) var rtm runtime.MemStats
sysStats, err := s.SingBoxService.GetSysStats() runtime.ReadMemStats(&rtm)
if err == nil { isRunning := corePtr.IsRunning()
info["running"] = true uptime := uint32(0)
info["stats"] = sysStats if isRunning {
} else { uptime = corePtr.GetInstance().Uptime()
info["running"] = s.SingBoxService.IsRunning() }
return map[string]interface{}{
"running": isRunning,
"stats": map[string]interface{}{
"NumGoroutine": uint32(runtime.NumGoroutine()),
"Alloc": rtm.Alloc,
"Uptime": uptime,
},
} }
return info
} }
func (s *ServerService) GetSystemInfo() map[string]interface{} { func (s *ServerService) GetSystemInfo() map[string]interface{} {
@@ -135,3 +144,61 @@ func (s *ServerService) GetSystemInfo() map[string]interface{} {
return info return info
} }
func (s *ServerService) GetLogs(count string, level string) []string {
c, _ := strconv.Atoi(count)
return logger.GetLogs(c, level)
}
func (s *ServerService) GenKeypair(keyType string, options string) []string {
if len(keyType) == 0 {
return []string{"No keypair to generate"}
}
switch keyType {
case "ech":
return s.generateECHKeyPair(options)
case "tls":
return s.generateTLSKeyPair(options)
case "reality":
return s.generateRealityKeyPair()
case "wireguard":
return generateWireGuardKey()
}
return []string{"Failed to generate keypair"}
}
func (s *ServerService) generateECHKeyPair(options string) []string {
parts := strings.Split(options, ",")
configPem, keyPem, err := tls.ECHKeygenDefault(parts[0], parts[1] == "true")
if err != nil {
return []string{"Failed to generate ECH keypair: ", err.Error()}
}
return append(strings.Split(configPem, "\n"), strings.Split(keyPem, "\n")...)
}
func (s *ServerService) generateTLSKeyPair(serverName string) []string {
privateKeyPem, publicKeyPem, err := tls.GenerateKeyPair(time.Now, serverName, time.Now().AddDate(0, 12, 0))
if err != nil {
return []string{"Failed to generate TLS keypair: ", err.Error()}
}
return append(strings.Split(string(privateKeyPem), "\n"), strings.Split(string(publicKeyPem), "\n")...)
}
func (s *ServerService) generateRealityKeyPair() []string {
privateKey, err := wgtypes.GeneratePrivateKey()
if err != nil {
return []string{"Failed to generate Reality keypair: ", err.Error()}
}
publicKey := privateKey.PublicKey()
return []string{"PrivateKey: " + base64.RawURLEncoding.EncodeToString(privateKey[:]), "PublicKey: " + base64.RawURLEncoding.EncodeToString(publicKey[:])}
}
func generateWireGuardKey() []string {
privateKey, err := wgtypes.GeneratePrivateKey()
if err != nil {
return []string{"Failed to generate wireguard keypair: ", err.Error()}
}
return []string{"PrivateKey: " + privateKey.String(), "PublicKey: " + privateKey.PublicKey().String()}
}
+56 -11
View File
@@ -3,6 +3,7 @@ package service
import ( import (
"encoding/json" "encoding/json"
"os" "os"
"s-ui/config"
"s-ui/database" "s-ui/database"
"s-ui/database/model" "s-ui/database/model"
"s-ui/logger" "s-ui/logger"
@@ -14,11 +15,30 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
var defaultConfig = `{
"log": {
"level": "info"
},
"dns": {},
"route": {
"rules": [
{
"protocol": [
"dns"
],
"outbound": "dns-out",
"action": "route"
}
]
},
"experimental": {}
}`
var defaultValueMap = map[string]string{ var defaultValueMap = map[string]string{
"webListen": "", "webListen": "",
"webDomain": "", "webDomain": "",
"webPort": "2095", "webPort": "2095",
"webSecret": common.Random(32), "secret": common.Random(32),
"webCertFile": "", "webCertFile": "",
"webKeyFile": "", "webKeyFile": "",
"webPath": "/app/", "webPath": "/app/",
@@ -36,6 +56,9 @@ var defaultValueMap = map[string]string{
"subEncode": "true", "subEncode": "true",
"subShowInfo": "false", "subShowInfo": "false",
"subURI": "", "subURI": "",
"subJsonExt": "",
"config": defaultConfig,
"version": config.GetVersion(),
} }
type SettingService struct { type SettingService struct {
@@ -65,7 +88,9 @@ func (s *SettingService) GetAllSetting() (*map[string]string, error) {
} }
// Due to security principles // Due to security principles
delete(allSetting, "webSecret") delete(allSetting, "secret")
delete(allSetting, "config")
delete(allSetting, "version")
return &allSetting, nil return &allSetting, nil
} }
@@ -127,9 +152,9 @@ func (s *SettingService) getBool(key string) (bool, error) {
return strconv.ParseBool(str) return strconv.ParseBool(str)
} }
func (s *SettingService) setBool(key string, value bool) error { // func (s *SettingService) setBool(key string, value bool) error {
return s.setString(key, strconv.FormatBool(value)) // return s.setString(key, strconv.FormatBool(value))
} // }
func (s *SettingService) getInt(key string) (int, error) { func (s *SettingService) getInt(key string) (int, error) {
str, err := s.getString(key) str, err := s.getString(key)
@@ -191,11 +216,11 @@ func (s *SettingService) SetWebPath(webPath string) error {
} }
func (s *SettingService) GetSecret() ([]byte, error) { func (s *SettingService) GetSecret() ([]byte, error) {
secret, err := s.getString("webSecret") secret, err := s.getString("secret")
if secret == defaultValueMap["webSecret"] { if secret == defaultValueMap["secret"] {
err := s.saveSetting("webSecret", secret) err := s.saveSetting("secret", secret)
if err != nil { if err != nil {
logger.Warning("save webSecret failed:", err) logger.Warning("save secret failed:", err)
} }
} }
return []byte(secret), err return []byte(secret), err
@@ -310,6 +335,22 @@ func (s *SettingService) GetFinalSubURI(host string) (string, error) {
return protocol + "://" + host + port + (*allSetting)["subPath"], nil return protocol + "://" + host + port + (*allSetting)["subPath"], nil
} }
func (s *SettingService) GetConfig() (string, error) {
return s.getString("config")
}
func (s *SettingService) SetConfig(config string) error {
return s.setString("config", config)
}
func (s *SettingService) SaveConfig(tx *gorm.DB, config json.RawMessage) error {
configs, err := json.MarshalIndent(config, "", " ")
if err != nil {
return err
}
return tx.Model(model.Setting{}).Where("key = ?", "config").Update("value", string(configs)).Error
}
func (s *SettingService) Save(tx *gorm.DB, changes []model.Changes) error { func (s *SettingService) Save(tx *gorm.DB, changes []model.Changes) error {
var err error var err error
for _, change := range changes { for _, change := range changes {
@@ -318,10 +359,10 @@ func (s *SettingService) Save(tx *gorm.DB, changes []model.Changes) error {
json.Unmarshal(change.Obj, &obj) json.Unmarshal(change.Obj, &obj)
// Secure file existance check // Secure file existance check
if key == "webCertFile" || if obj != "" && (key == "webCertFile" ||
key == "webKeyFile" || key == "webKeyFile" ||
key == "subCertFile" || key == "subCertFile" ||
key == "subKeyFile" { key == "subKeyFile") {
err = s.fileExists(obj) err = s.fileExists(obj)
if err != nil { if err != nil {
return common.NewError(" -> ", obj, " is not exists") return common.NewError(" -> ", obj, " is not exists")
@@ -347,6 +388,10 @@ func (s *SettingService) Save(tx *gorm.DB, changes []model.Changes) error {
return err return err
} }
func (s *SettingService) GetSubJsonExt() (string, error) {
return s.getString("subJsonExt")
}
func (s *SettingService) fileExists(path string) error { func (s *SettingService) fileExists(path string) error {
_, err := os.Stat(path) _, err := os.Stat(path)
return err return err
-45
View File
@@ -1,45 +0,0 @@
package service
import (
"s-ui/singbox"
)
type SingBoxService struct {
singbox.V2rayAPI
singbox.Controller
StatsService
}
func (s *SingBoxService) GetStats() error {
s.V2rayAPI.Init(ApiAddr)
defer s.V2rayAPI.Close()
stats, err := s.V2rayAPI.GetStats(true)
if err != nil {
return err
}
err = s.StatsService.SaveStats(stats)
if err != nil {
return err
}
return nil
}
func (s *SingBoxService) GetSysStats() (*map[string]interface{}, error) {
err := s.V2rayAPI.Init(ApiAddr)
if err != nil {
return nil, err
}
defer s.V2rayAPI.Close()
resp, err := s.V2rayAPI.GetSysStats()
if err != nil {
return nil, err
}
result := make(map[string]interface{})
result["NumGoroutine"] = resp.NumGoroutine
result["Alloc"] = resp.Alloc
result["Uptime"] = resp.Uptime
return &result, nil
}
+16 -13
View File
@@ -1,7 +1,6 @@
package service package service
import ( import (
"encoding/json"
"s-ui/database" "s-ui/database"
"s-ui/database/model" "s-ui/database/model"
"time" "time"
@@ -20,18 +19,22 @@ var onlineResources = &onlines{}
type StatsService struct { type StatsService struct {
} }
func (s *StatsService) SaveStats(stats []*model.Stats) error { func (s *StatsService) SaveStats() error {
var err error if !corePtr.IsRunning() {
return nil
}
stats := corePtr.GetInstance().ConnTracker().GetStats()
// Reset onlines // Reset onlines
onlineResources.Inbound = nil onlineResources.Inbound = nil
onlineResources.Outbound = nil onlineResources.Outbound = nil
onlineResources.User = nil onlineResources.User = nil
if len(stats) == 0 { if len(*stats) == 0 {
return nil return nil
} }
var err error
db := database.GetDB() db := database.GetDB()
tx := db.Begin() tx := db.Begin()
defer func() { defer func() {
@@ -42,7 +45,7 @@ func (s *StatsService) SaveStats(stats []*model.Stats) error {
} }
}() }()
for _, stat := range stats { for _, stat := range *stats {
if stat.Resource == "user" { if stat.Resource == "user" {
if stat.Direction { if stat.Direction {
err = tx.Model(model.Client{}).Where("name = ?", stat.Tag). err = tx.Model(model.Client{}).Where("name = ?", stat.Tag).
@@ -71,7 +74,7 @@ func (s *StatsService) SaveStats(stats []*model.Stats) error {
return err return err
} }
func (s *StatsService) GetStats(resorce string, tag string, limit int) ([]model.Stats, error) { func (s *StatsService) GetStats(resource string, tag string, limit int) ([]model.Stats, error) {
var err error var err error
var result []model.Stats var result []model.Stats
@@ -79,19 +82,19 @@ func (s *StatsService) GetStats(resorce string, tag string, limit int) ([]model.
timeDiff := currentTime - (int64(limit) * 3600) timeDiff := currentTime - (int64(limit) * 3600)
db := database.GetDB() db := database.GetDB()
err = db.Model(model.Stats{}).Where("resource = ? AND tag = ? AND date_time > ?", resorce, tag, timeDiff).Scan(&result).Error resources := []string{resource}
if resource == "endpoint" {
resources = []string{"inbound", "outbound"}
}
err = db.Model(model.Stats{}).Where("resource in ? AND tag = ? AND date_time > ?", resources, tag, timeDiff).Scan(&result).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
return result, nil return result, nil
} }
func (s *StatsService) GetOnlines() (string, error) { func (s *StatsService) GetOnlines() (onlines, error) {
onlines, err := json.Marshal(onlineResources) return *onlineResources, nil
if err != nil {
return "", err
}
return string(onlines), nil
} }
func (s *StatsService) DelOldStats(days int) error { func (s *StatsService) DelOldStats(days int) error {
oldTime := time.Now().AddDate(0, 0, -(days)).Unix() oldTime := time.Now().AddDate(0, 0, -(days)).Unix()
+68
View File
@@ -0,0 +1,68 @@
package service
import (
"encoding/json"
"s-ui/database"
"s-ui/database/model"
"s-ui/util/common"
"gorm.io/gorm"
)
type TlsService struct {
InboundService
}
func (s *TlsService) GetAll() ([]model.Tls, error) {
db := database.GetDB()
tlsConfig := []model.Tls{}
err := db.Model(model.Tls{}).Scan(&tlsConfig).Error
if err != nil {
return nil, err
}
return tlsConfig, nil
}
func (s *TlsService) Save(tx *gorm.DB, action string, data json.RawMessage) ([]uint, error) {
var err error
var inboundIds []uint
switch action {
case "new", "edit":
var tls model.Tls
err = json.Unmarshal(data, &tls)
if err != nil {
return nil, err
}
err = tx.Save(&tls).Error
if err != nil {
return nil, err
}
err = tx.Model(model.Inbound{}).Select("id").Where("tls_id = ?", tls.Id).Scan(&inboundIds).Error
if err != nil {
return nil, err
}
return inboundIds, nil
case "del":
var id uint
err = json.Unmarshal(data, &id)
if err != nil {
return nil, err
}
var inboundCount int64
err = tx.Model(model.Inbound{}).Where("tls_id = ?", id).Count(&inboundCount).Error
if err != nil {
return nil, err
}
if inboundCount > 0 {
return nil, common.NewError("tls in use")
}
err = tx.Where("id = ?", id).Delete(model.Tls{}).Error
if err != nil {
return nil, err
}
}
return nil, nil
}
-54
View File
@@ -1,54 +0,0 @@
package singbox
import (
"errors"
"io/fs"
"os"
"os/exec"
"s-ui/config"
"strings"
)
var serviceName = "sing-box"
type Controller struct {
}
func (s *Controller) GetBinaryName() string {
return "sing-box"
}
func (s *Controller) GetBinaryPath() string {
return config.GetBinFolderPath() + "/" + s.GetBinaryName()
}
func (s *Controller) GetConfigPath() string {
return config.GetBinFolderPath() + "/config.json"
}
func (s *Controller) IsRunning() bool {
cmd := exec.Command("pgrep", "sing-box")
output, err := cmd.Output()
if err != nil {
return false
}
// If pgrep found the Controller, its output will not be empty
return strings.TrimSpace(string(output)) != ""
}
func (s *Controller) signalSingbox(signal string) error {
return os.WriteFile(config.GetBinFolderPath()+"/signal", []byte(signal), fs.ModePerm)
}
func (s *Controller) Restart() error {
return s.signalSingbox("restart")
}
func (s *Controller) Stop() error {
if !s.IsRunning() {
return errors.New("Sing-Box is not running")
}
return s.signalSingbox("stop")
}
-95
View File
@@ -1,95 +0,0 @@
package singbox
import (
"context"
"regexp"
"s-ui/database/model"
"s-ui/util/common"
"time"
statsService "github.com/v2fly/v2ray-core/v5/app/stats/command"
"google.golang.org/grpc"
)
type V2rayAPI struct {
StatsServiceClient *statsService.StatsServiceClient
grpcClient *grpc.ClientConn
isConnected bool
}
func (v *V2rayAPI) Init(ApiAddr string) (err error) {
if len(ApiAddr) == 0 {
return common.NewError("The api address is wrong: ", ApiAddr)
}
v.grpcClient, err = grpc.Dial(ApiAddr, grpc.WithInsecure())
if err != nil {
return err
}
v.isConnected = true
ssClient := statsService.NewStatsServiceClient(v.grpcClient)
v.StatsServiceClient = &ssClient
return
}
func (v *V2rayAPI) Close() {
v.grpcClient.Close()
v.StatsServiceClient = nil
v.isConnected = false
}
func (v *V2rayAPI) GetStats(reset bool) ([]*model.Stats, error) {
if v.grpcClient == nil {
return nil, common.NewError("v2ray api is not initialized")
}
var trafficRegex = regexp.MustCompile("(inbound|outbound|user)>>>([^>]+)>>>traffic>>>(downlink|uplink)")
client := *v.StatsServiceClient
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
request := &statsService.QueryStatsRequest{
Reset_: reset,
}
resp, err := client.QueryStats(ctx, request)
if err != nil {
return nil, err
}
dt := time.Now().Unix()
stats := make([]*model.Stats, 0)
for _, stat := range resp.GetStat() {
if stat.Value > 0 {
matchs := trafficRegex.FindStringSubmatch(stat.Name)
if len(matchs) > 3 {
stat := model.Stats{
DateTime: dt,
Resource: matchs[1],
Tag: matchs[2],
Direction: matchs[3] == "uplink",
Traffic: stat.Value,
}
stats = append(stats, &stat)
}
}
}
return stats, nil
}
func (v *V2rayAPI) GetSysStats() (*statsService.SysStatsResponse, error) {
if v.grpcClient == nil {
return nil, common.NewError("v2ray api is not initialized")
}
client := *v.StatsServiceClient
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
request := &statsService.SysStatsRequest{}
resp, err := client.GetSysStats(ctx, request)
if err != nil {
return nil, err
}
return resp, nil
}
+287
View File
@@ -0,0 +1,287 @@
package sub
import (
"encoding/json"
"fmt"
"s-ui/database"
"s-ui/database/model"
"s-ui/service"
"s-ui/util"
)
const defaultJson = `
{
"inbounds": [
{
"type": "tun",
"address": [
"172.19.0.1/30",
"fdfe:dcba:9876::1/126"
],
"mtu": 9000,
"auto_route": true,
"strict_route": false,
"sniff": true,
"endpoint_independent_nat": false,
"stack": "system",
"platform": {
"http_proxy": {
"enabled": true,
"server": "127.0.0.1",
"server_port": 2080
}
}
},
{
"type": "mixed",
"listen": "127.0.0.1",
"listen_port": 2080,
"sniff": true,
"users": []
}
]
}
`
type JsonService struct {
service.SettingService
LinkService
}
func (j *JsonService) GetJson(subId string, format string) (*string, error) {
var jsonConfig map[string]interface{}
client, inDatas, err := j.getData(subId)
if err != nil {
return nil, err
}
outbounds, outTags, err := j.getOutbounds(client.Config, inDatas)
if err != nil {
return nil, err
}
links := j.LinkService.GetLinks(&client.Links, "external", "")
for index, link := range links {
json, tag, err := util.GetOutbound(link, index)
if err == nil && len(tag) > 0 {
*outbounds = append(*outbounds, *json)
*outTags = append(*outTags, tag)
}
}
j.addDefaultOutbounds(outbounds, outTags)
err = json.Unmarshal([]byte(defaultJson), &jsonConfig)
if err != nil {
return nil, err
}
jsonConfig["outbounds"] = outbounds
// Add other objects from settings
j.addOthers(&jsonConfig)
result, _ := json.MarshalIndent(jsonConfig, "", " ")
resultStr := string(result)
return &resultStr, nil
}
func (j *JsonService) getData(subId string) (*model.Client, []*model.Inbound, error) {
db := database.GetDB()
client := &model.Client{}
err := db.Model(model.Client{}).Where("enable = true and name = ?", subId).First(client).Error
if err != nil {
return nil, nil, err
}
var clientInbounds []uint
err = json.Unmarshal(client.Inbounds, &clientInbounds)
if err != nil {
return nil, nil, err
}
var inbounds []*model.Inbound
err = db.Model(model.Inbound{}).Where("id in ?", clientInbounds).Find(&inbounds).Error
if err != nil {
return nil, nil, err
}
return client, inbounds, nil
}
func (j *JsonService) getOutbounds(clientConfig json.RawMessage, inbounds []*model.Inbound) (*[]map[string]interface{}, *[]string, error) {
var outbounds []map[string]interface{}
var configs map[string]interface{}
var outTags []string
err := json.Unmarshal(clientConfig, &configs)
if err != nil {
return nil, nil, err
}
for _, inData := range inbounds {
if len(inData.OutJson) < 5 {
continue
}
var outbound map[string]interface{}
err = json.Unmarshal(inData.OutJson, &outbound)
if err != nil {
return nil, nil, err
}
protocol, _ := outbound["type"].(string)
config, _ := configs[protocol].(map[string]interface{})
for key, value := range config {
if key != "alterId" && key != "name" {
outbound[key] = value
}
}
var addrs []map[string]interface{}
err = json.Unmarshal(inData.Addrs, &addrs)
if err != nil {
return nil, nil, err
}
tag, _ := outbound["tag"].(string)
if len(addrs) == 0 {
// For mixed protocol, use separated socks and http
if protocol == "mixed" {
outbound["tag"] = tag
j.pushMixed(&outbounds, &outTags, outbound)
} else {
outTags = append(outTags, tag)
outbounds = append(outbounds, outbound)
}
} else {
for index, addr := range addrs {
// Copy original config
newOut := make(map[string]interface{}, len(outbound))
for key, value := range outbound {
newOut[key] = value
}
// Change and push copied config
newOut["server"], _ = addr["server"].(string)
port, _ := addr["server_port"].(float64)
newOut["server_port"] = int(port)
// Override TLS
outTls, _ := newOut["tls"].(map[string]interface{})
if addrTls, ok := addr["tls"].(map[string]interface{}); ok {
for key, value := range addrTls {
outTls[key] = value
}
}
newOut["tls"] = outTls
remark, _ := addr["remark"].(string)
newTag := fmt.Sprintf("%d.%s%s", index+1, tag, remark)
newOut["tag"] = newTag
// For mixed protocol, use separated socks and http
if protocol == "mixed" {
j.pushMixed(&outbounds, &outTags, newOut)
} else {
outTags = append(outTags, newTag)
outbounds = append(outbounds, newOut)
}
}
}
}
return &outbounds, &outTags, nil
}
func (j *JsonService) addDefaultOutbounds(outbounds *[]map[string]interface{}, outTags *[]string) {
outbound := []map[string]interface{}{
{
"outbounds": append([]string{"auto", "direct"}, *outTags...),
"tag": "proxy",
"type": "selector",
},
{
"tag": "auto",
"type": "urltest",
"outbounds": outTags,
"url": "http://www.gstatic.com/generate_204",
"interval": "10m",
"tolerance": 50,
},
{
"type": "direct",
"tag": "direct",
},
{
"type": "dns",
"tag": "dns-out",
},
{
"type": "block",
"tag": "block",
},
}
*outbounds = append(outbound, *outbounds...)
}
func (j *JsonService) addOthers(jsonConfig *map[string]interface{}) error {
rules := []interface{}{
map[string]interface{}{
"clash_mode": "Direct",
"outbound": "direct",
},
map[string]interface{}{
"clash_mode": "Global",
"outbound": "proxy",
},
}
route := map[string]interface{}{
"auto_detect_interface": true,
"final": "proxy",
"rules": rules,
}
othersStr, err := j.SettingService.GetSubJsonExt()
if err != nil {
return err
}
if len(othersStr) == 0 {
(*jsonConfig)["route"] = route
return nil
}
var othersJson map[string]interface{}
err = json.Unmarshal([]byte(othersStr), &othersJson)
if err != nil {
return err
}
if _, ok := othersJson["log"]; ok {
(*jsonConfig)["log"] = othersJson["log"]
}
if _, ok := othersJson["dns"]; ok {
(*jsonConfig)["dns"] = othersJson["dns"]
}
if _, ok := othersJson["inbounds"]; ok {
(*jsonConfig)["inbounds"] = othersJson["inbounds"]
}
if _, ok := othersJson["experimental"]; ok {
(*jsonConfig)["experimental"] = othersJson["experimental"]
}
if _, ok := othersJson["rule_set"]; ok {
route["rule_set"] = othersJson["rule_set"]
}
if settingRules, ok := othersJson["rules"].([]interface{}); ok {
route["rules"] = append(rules, settingRules...)
}
(*jsonConfig)["route"] = route
return nil
}
func (j *JsonService) pushMixed(outbounds *[]map[string]interface{}, outTags *[]string, out map[string]interface{}) {
socksOut := make(map[string]interface{}, 1)
httpOut := make(map[string]interface{}, 1)
for key, value := range out {
socksOut[key] = value
httpOut[key] = value
}
socksTag := fmt.Sprintf("%s-socks", out["tag"])
httpTag := fmt.Sprintf("%s-http", out["tag"])
socksOut["type"] = "socks"
httpOut["type"] = "http"
socksOut["tag"] = socksTag
httpOut["tag"] = httpTag
*outbounds = append(*outbounds, socksOut, httpOut)
*outTags = append(*outTags, socksTag, httpTag)
}
+103
View File
@@ -0,0 +1,103 @@
package sub
import (
"crypto/tls"
"encoding/json"
"io"
"net/http"
"s-ui/logger"
"s-ui/util"
"strings"
)
type Link struct {
Type string `json:"type"`
Remark string `json:"remark"`
Uri string `json:"uri"`
}
type LinkService struct {
}
func (s *LinkService) GetLinks(linkJson *json.RawMessage, types string, clientInfo string) []string {
links := []Link{}
var result []string
err := json.Unmarshal(*linkJson, &links)
if err != nil {
return nil
}
for _, link := range links {
switch link.Type {
case "external":
result = append(result, link.Uri)
case "sub":
result = append(result, s.getExternalSub(link.Uri)...)
case "local":
if types == "all" {
result = append(result, s.addClientInfo(link.Uri, clientInfo))
}
}
}
return result
}
func (s *LinkService) addClientInfo(uri string, clientInfo string) string {
if len(clientInfo) == 0 {
return uri
}
protocol := strings.Split(uri, "://")
if len(protocol) < 2 {
return uri
}
switch protocol[0] {
case "vmess":
var vmessJson map[string]interface{}
config, err := util.B64StrToByte(protocol[1])
if err != nil {
logger.Warning("sub: Error decoding vmess content:", err)
return uri
}
err = json.Unmarshal(config, &vmessJson)
if err != nil {
logger.Warning("sub: Error decoding vmess content:", err)
return uri
}
vmessJson["ps"] = vmessJson["ps"].(string) + clientInfo
result, err := json.MarshalIndent(vmessJson, "", " ")
if err != nil {
logger.Warning("sub: Error decoding vmess + clientInfo content:", err)
return uri
}
return "vmess://" + util.ByteToB64Str(result)
default:
return uri + clientInfo
}
}
func (s *LinkService) getExternalSub(url string) []string {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{Transport: tr}
// Make the HTTP request
response, err := client.Get(url)
if err != nil {
logger.Warning("sub: Error making HTTP request:", err)
return nil
}
defer response.Body.Close()
// Read the response body
body, err := io.ReadAll(response.Body)
if err != nil {
logger.Warning("sub: Error reading response body:", err)
return nil
}
// Convert if the content is Base64 encoded
links := util.StrOrBase64Encoded(string(body))
return strings.Split(links, "\n")
}
+21 -9
View File
@@ -10,6 +10,7 @@ import (
type SubHandler struct { type SubHandler struct {
service.SettingService service.SettingService
SubService SubService
JsonService
} }
func NewSubHandler(g *gin.RouterGroup) { func NewSubHandler(g *gin.RouterGroup) {
@@ -23,17 +24,28 @@ func (s *SubHandler) initRouter(g *gin.RouterGroup) {
func (s *SubHandler) subs(c *gin.Context) { func (s *SubHandler) subs(c *gin.Context) {
subId := c.Param("subid") subId := c.Param("subid")
result, headers, err := s.SubService.GetSubs(subId) format, isFormat := c.GetQuery("format")
if err != nil || result == nil { if isFormat {
logger.Error(err) result, err := s.JsonService.GetJson(subId, format)
c.String(400, "Error!") if err != nil || result == nil {
logger.Error(err)
c.String(400, "Error!")
} else {
c.String(200, *result)
}
} else { } else {
result, headers, err := s.SubService.GetSubs(subId)
if err != nil || result == nil {
logger.Error(err)
c.String(400, "Error!")
} else {
// Add headers // Add headers
c.Writer.Header().Set("Subscription-Userinfo", headers[0]) c.Writer.Header().Set("Subscription-Userinfo", headers[0])
c.Writer.Header().Set("Profile-Update-Interval", headers[1]) c.Writer.Header().Set("Profile-Update-Interval", headers[1])
c.Writer.Header().Set("Profile-Title", headers[2]) c.Writer.Header().Set("Profile-Title", headers[2])
c.String(200, *result) c.String(200, *result)
}
} }
} }
+3 -102
View File
@@ -1,15 +1,10 @@
package sub package sub
import ( import (
"crypto/tls"
"encoding/base64" "encoding/base64"
"encoding/json"
"fmt" "fmt"
"io"
"net/http"
"s-ui/database" "s-ui/database"
"s-ui/database/model" "s-ui/database/model"
"s-ui/logger"
"s-ui/service" "s-ui/service"
"strings" "strings"
"time" "time"
@@ -17,12 +12,7 @@ import (
type SubService struct { type SubService struct {
service.SettingService service.SettingService
} LinkService
type Link struct {
Type string `json:"type"`
Remark string `json:"remark"`
Uri string `json:"uri"`
} }
func (s *SubService) GetSubs(subId string) (*string, []string, error) { func (s *SubService) GetSubs(subId string) (*string, []string, error) {
@@ -35,29 +25,14 @@ func (s *SubService) GetSubs(subId string) (*string, []string, error) {
return nil, nil, err return nil, nil, err
} }
links := []Link{}
err = json.Unmarshal([]byte(client.Links), &links)
if err != nil {
return nil, nil, err
}
clientInfo := "" clientInfo := ""
subShowInfo, _ := s.SettingService.GetSubShowInfo() subShowInfo, _ := s.SettingService.GetSubShowInfo()
if subShowInfo { if subShowInfo {
clientInfo = s.getClientInfo(client) clientInfo = s.getClientInfo(client)
} }
var result string linksArray := s.LinkService.GetLinks(&client.Links, "all", clientInfo)
for _, link := range links { result := strings.Join(linksArray, "\n")
switch link.Type {
case "external":
result += fmt.Sprintln(link.Uri)
case "sub":
result += s.getExternalSub(link.Uri)
case "local":
result += fmt.Sprintln(s.addClientInfo(link.Uri, clientInfo))
}
}
var headers []string var headers []string
updateInterval, _ := s.SettingService.GetSubUpdates() updateInterval, _ := s.SettingService.GetSubUpdates()
@@ -90,80 +65,6 @@ func (s *SubService) getClientInfo(c *model.Client) string {
} }
} }
func (s *SubService) addClientInfo(uri string, clientInfo string) string {
protocol := strings.Split(uri, "://")
if len(protocol) < 2 {
return uri
}
switch protocol[0] {
case "vmess":
var vmessJson map[string]interface{}
config, err := base64.StdEncoding.DecodeString(protocol[1])
if err != nil {
logger.Warning("sub: Error decoding vmess content:", err)
return uri
}
err = json.Unmarshal(config, &vmessJson)
if err != nil {
logger.Warning("sub: Error decoding vmess content:", err)
return uri
}
vmessJson["ps"] = vmessJson["ps"].(string) + clientInfo
result, err := json.MarshalIndent(vmessJson, "", " ")
if err != nil {
logger.Warning("sub: Error decoding vmess + clientInfo content:", err)
return uri
}
return "vmess://" + base64.StdEncoding.EncodeToString(result)
default:
return uri + clientInfo
}
}
func (s *SubService) getExternalSub(url string) string {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{Transport: tr}
// Make the HTTP request
response, err := client.Get(url)
if err != nil {
logger.Warning("sub: Error making HTTP request:", err)
return ""
}
defer response.Body.Close()
// Read the response body
body, err := io.ReadAll(response.Body)
if err != nil {
logger.Warning("sub: Error reading response body:", err)
return ""
}
// Check if the content is Base64 encoded
isBase64 := s.isBase64Encoded(string(body))
if isBase64 {
// Decode Base64 content
decodedText, err := base64.StdEncoding.DecodeString(string(body))
if err != nil {
logger.Warning("sub: Error decoding Base64 content:", err)
return ""
}
return string(decodedText)
} else {
return string(body)
}
}
// Function to check if a string is Base64 encoded
func (s *SubService) isBase64Encoded(str string) bool {
_, err := base64.StdEncoding.DecodeString(str)
return err == nil
}
func (s *SubService) formatTraffic(trafficBytes int64) string { func (s *SubService) formatTraffic(trafficBytes int64) string {
if trafficBytes < 1024 { if trafficBytes < 1024 {
return fmt.Sprintf("%.2fB", float64(trafficBytes)/float64(1)) return fmt.Sprintf("%.2fB", float64(trafficBytes)/float64(1))
+20
View File
@@ -0,0 +1,20 @@
package util
import "encoding/base64"
// Function to return decoded bytes if a string is Base64 encoded
func StrOrBase64Encoded(str string) string {
decoded, err := base64.StdEncoding.DecodeString(str)
if err == nil {
return string(decoded)
}
return str
}
func B64StrToByte(str string) ([]byte, error) {
return base64.StdEncoding.DecodeString(str)
}
func ByteToB64Str(b []byte) string {
return base64.StdEncoding.EncodeToString(b)
}
+16 -3
View File
@@ -1,13 +1,26 @@
package common package common
import "math/rand" import (
"math/rand"
"time"
)
var allSeq [62]rune var (
allSeq []rune
rnd = rand.New(rand.NewSource(time.Now().UnixNano()))
)
func init() {
chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
for _, char := range chars {
allSeq = append(allSeq, char)
}
}
func Random(n int) string { func Random(n int) string {
runes := make([]rune, n) runes := make([]rune, n)
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
runes[i] = allSeq[rand.Intn(len(allSeq))] runes[i] = allSeq[rnd.Intn(len(allSeq))]
} }
return string(runes) return string(runes)
} }
+509
View File
@@ -0,0 +1,509 @@
package util
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/url"
"s-ui/database/model"
"strings"
)
var InboundTypeWithLink = []string{"shadowsocks", "naive", "hysteria", "hysteria2", "tuic", "vless", "trojan", "vmess"}
func LinkGenerator(clientConfig json.RawMessage, i *model.Inbound, hostname string) []string {
inbound, err := i.MarshalFull()
if err != nil {
return []string{}
}
var tls map[string]interface{}
if i.TlsId > 0 {
json.Unmarshal(i.Tls.Client, &tls)
}
var userConfig map[string]map[string]interface{}
if err := json.Unmarshal(clientConfig, &userConfig); err != nil {
return []string{}
}
var Addrs []map[string]interface{}
json.Unmarshal(i.Addrs, &Addrs)
if len(Addrs) == 0 {
Addrs = append(Addrs, map[string]interface{}{
"server": hostname,
"server_port": (*inbound)["listen_port"],
"remark": i.Tag,
})
if i.TlsId > 0 {
Addrs[0]["tls"] = tls
}
} else {
for index, addr := range Addrs {
addrRemark, _ := addr["remark"].(string)
Addrs[index]["remark"] = i.Tag + addrRemark
if addrTls, ok := addr["tls"].(map[string]interface{}); ok {
newTls := map[string]interface{}{}
if oldTls, hasOldTls := tls["tls"].(map[string]interface{}); hasOldTls {
for k, v := range oldTls {
newTls[k] = v
}
}
// Override tls
for k, v := range addrTls {
newTls[k] = v
}
Addrs[index]["tls"] = newTls
}
}
}
switch i.Type {
case "shadowsocks":
return shadowsocksLink(userConfig, *inbound, Addrs)
case "naive":
return naiveLink(userConfig["naive"], *inbound, Addrs)
case "hysteria":
return hysteriaLink(userConfig["hysteria"], *inbound, Addrs)
case "hysteria2":
return hysteria2Link(userConfig["hysteria2"], *inbound, Addrs)
case "tuic":
return tuicLink(userConfig["tuic"], *inbound, Addrs)
case "vless":
return vlessLink(userConfig["vless"], *inbound, Addrs)
case "trojan":
return trojanLink(userConfig["trojan"], *inbound, Addrs)
case "vmess":
return vmessLink(userConfig["vmess"], *inbound, Addrs)
}
return []string{}
}
func shadowsocksLink(
userConfig map[string]map[string]interface{},
inbound map[string]interface{},
addrs []map[string]interface{}) []string {
var userPass []string
method, _ := inbound["method"].(string)
var pass string
if method == "2022-blake3-aes-128-gcm" {
pass, _ = userConfig["shadowsocks16"]["password"].(string)
} else {
pass, _ = userConfig["shadowsocks"]["password"].(string)
}
userPass = append(userPass, pass)
if strings.HasPrefix(method, "2022") {
inbPass, _ := inbound["password"].(string)
userPass = append(userPass, inbPass)
}
uriBase := fmt.Sprintf("ss://%s", toBase64([]byte(fmt.Sprintf("%s:%s", method, strings.Join(userPass, ":")))))
var links []string
for _, addr := range addrs {
port, _ := addr["server_port"].(float64)
links = append(links, fmt.Sprintf("%s@%s:%d", uriBase, addr["server"].(string), uint(port)))
}
return links
}
func naiveLink(
userConfig map[string]interface{},
inbound map[string]interface{},
addrs []map[string]interface{}) []string {
password, _ := userConfig["password"].(string)
username, _ := userConfig["username"].(string)
baseUri := "http2://"
var links []string
for _, addr := range addrs {
params := map[string]string{}
params["padding"] = "1"
if tls, ok := addr["tls"].(map[string]interface{}); ok {
if sni, ok := tls["server_name"].(string); ok {
params["peer"] = sni
}
if alpn, ok := tls["alpn"].([]interface{}); ok {
alpnList := make([]string, len(alpn))
for i, v := range alpn {
alpnList[i] = v.(string)
}
params["alpn"] = strings.Join(alpnList, ",")
}
if insecure, ok := tls["insecure"].(bool); ok && insecure {
params["allowInsecure"] = "1"
}
}
if tfo, ok := inbound["tcp_fast_open"].(bool); ok && tfo {
params["tfo"] = "1"
} else {
params["tfo"] = "0"
}
port, _ := addr["server_port"].(float64)
uri := baseUri + toBase64([]byte(fmt.Sprintf("%s:%s@%s:%d", username, password, addr["server"].(string), uint(port))))
links = append(links, addParams(uri, params, addr["remark"].(string)))
}
return links
}
func hysteriaLink(
userConfig map[string]interface{},
inbound map[string]interface{},
addrs []map[string]interface{}) []string {
baseUri := "hysteria://"
var links []string
for _, addr := range addrs {
params := map[string]string{}
if upmbps, ok := inbound["up_mbps"].(string); ok {
params["up_mbps"] = upmbps
}
if downmbps, ok := inbound["down_mbps"].(string); ok {
params["down_mbps"] = downmbps
}
if auth, ok := userConfig["auth_str"].(string); ok {
params["auth"] = auth
}
if tls, ok := addr["tls"].(map[string]interface{}); ok {
if sni, ok := tls["server_name"].(string); ok {
params["peer"] = sni
}
if alpn, ok := tls["alpn"].([]interface{}); ok {
alpnList := make([]string, len(alpn))
for i, v := range alpn {
alpnList[i] = v.(string)
}
params["alpn"] = strings.Join(alpnList, ",")
}
if insecure, ok := tls["insecure"].(bool); ok && insecure {
params["allowInsecure"] = "1"
}
}
if obfs, ok := inbound["obfs"].(string); ok {
params["obfs"] = obfs
}
if tfo, ok := inbound["tcp_fast_open"].(bool); ok && tfo {
params["fastopen"] = "1"
} else {
params["fastopen"] = "0"
}
port, _ := addr["server_port"].(float64)
uri := fmt.Sprintf("%s%s:%d", baseUri, addr["server"].(string), uint(port))
links = append(links, addParams(uri, params, addr["remark"].(string)))
}
return links
}
func hysteria2Link(
userConfig map[string]interface{},
inbound map[string]interface{},
addrs []map[string]interface{}) []string {
password, _ := userConfig["password"].(string)
baseUri := fmt.Sprintf("%s%s@", "hysteria2://", password)
var links []string
for _, addr := range addrs {
params := map[string]string{}
if upmbps, ok := inbound["up_mbps"].(string); ok {
params["up_mbps"] = upmbps
}
if downmbps, ok := inbound["down_mbps"].(string); ok {
params["down_mbps"] = downmbps
}
if tls, ok := addr["tls"].(map[string]interface{}); ok {
if sni, ok := tls["server_name"].(string); ok {
params["sni"] = sni
}
if alpn, ok := tls["alpn"].([]interface{}); ok {
alpnList := make([]string, len(alpn))
for i, v := range alpn {
alpnList[i] = v.(string)
}
params["alpn"] = strings.Join(alpnList, ",")
}
if insecure, ok := tls["insecure"].(bool); ok && insecure {
params["allowInsecure"] = "1"
}
}
if obfs, ok := inbound["obfs"].(map[string]interface{}); ok {
if obfsType, ok := obfs["type"].(string); ok {
params["obfs"] = obfsType
}
if obfsPassword, ok := obfs["password"].(string); ok {
params["obfs-password"] = obfsPassword
}
}
if tfo, ok := inbound["tcp_fast_open"].(bool); ok && tfo {
params["fastopen"] = "1"
} else {
params["fastopen"] = "0"
}
port, _ := addr["server_port"].(float64)
uri := fmt.Sprintf("%s%s:%d", baseUri, addr["server"].(string), uint(port))
links = append(links, addParams(uri, params, addr["remark"].(string)))
}
return links
}
func tuicLink(
userConfig map[string]interface{},
inbound map[string]interface{},
addrs []map[string]interface{}) []string {
password, _ := userConfig["password"].(string)
uuid, _ := userConfig["uuid"].(string)
baseUri := fmt.Sprintf("%s%s:%s@", "tuic://", uuid, password)
var links []string
for _, addr := range addrs {
params := map[string]string{}
if tls, ok := addr["tls"].(map[string]interface{}); ok {
if sni, ok := tls["server_name"].(string); ok {
params["sni"] = sni
}
if alpn, ok := tls["alpn"].([]interface{}); ok {
alpnList := make([]string, len(alpn))
for i, v := range alpn {
alpnList[i] = v.(string)
}
params["alpn"] = strings.Join(alpnList, ",")
}
if insecure, ok := tls["insecure"].(bool); ok && insecure {
params["allowInsecure"] = "1"
}
if disableSni, ok := tls["disable_sni"].(bool); ok && disableSni {
params["disableSni"] = "1"
}
}
if congestionControl, ok := inbound["congestion_control"].(string); ok {
params["congestion_control"] = congestionControl
}
port, _ := addr["server_port"].(float64)
uri := fmt.Sprintf("%s%s:%d", baseUri, addr["server"].(string), uint(port))
links = append(links, addParams(uri, params, addr["remark"].(string)))
}
return links
}
func vlessLink(
userConfig map[string]interface{},
inbound map[string]interface{},
addrs []map[string]interface{}) []string {
uuid, _ := userConfig["uuid"].(string)
baseParams := getTransportParams(inbound["transport"])
var links []string
for _, addr := range addrs {
params := baseParams
if tls, ok := addr["tls"].(map[string]interface{}); ok && tls["enabled"].(bool) {
if reality, ok := tls["reality"].(map[string]interface{}); ok && reality["enabled"].(bool) {
params["security"] = "reality"
if pbk, ok := reality["public_key"].(string); ok {
params["pbk"] = pbk
}
if sid, ok := reality["short_id"].(string); ok {
params["sid"] = sid
}
} else {
params["security"] = "tls"
if insecure, ok := tls["insecure"].(bool); ok && insecure {
params["allowInsecure"] = "1"
}
if flow, ok := userConfig["flow"].(string); ok {
params["flow"] = flow
}
}
if sni, ok := tls["server_name"].(string); ok {
params["sni"] = sni
}
if alpn, ok := tls["alpn"].([]interface{}); ok {
alpnList := make([]string, len(alpn))
for i, v := range alpn {
alpnList[i] = v.(string)
}
params["alpn"] = strings.Join(alpnList, ",")
}
}
port, _ := addr["server_port"].(float64)
uri := fmt.Sprintf("vless://%s@%s:%d", uuid, addr["server"].(string), uint(port))
uri = addParams(uri, params, addr["remark"].(string))
links = append(links, uri)
}
return links
}
func trojanLink(
userConfig map[string]interface{},
inbound map[string]interface{},
addrs []map[string]interface{}) []string {
password, _ := userConfig["password"].(string)
baseParams := getTransportParams(inbound["transport"])
var links []string
for _, addr := range addrs {
params := baseParams
if tls, ok := addr["tls"].(map[string]interface{}); ok && tls["enabled"].(bool) {
if reality, ok := tls["reality"].(map[string]interface{}); ok && reality["enabled"].(bool) {
params["security"] = "reality"
if pbk, ok := reality["public_key"].(string); ok {
params["pbk"] = pbk
}
if sid, ok := reality["short_id"].(string); ok {
params["sid"] = sid
}
} else {
params["security"] = "tls"
if insecure, ok := tls["insecure"].(bool); ok && insecure {
params["allowInsecure"] = "1"
}
}
if sni, ok := tls["server_name"].(string); ok {
params["sni"] = sni
}
if alpn, ok := tls["alpn"].([]interface{}); ok {
alpnList := make([]string, len(alpn))
for i, v := range alpn {
alpnList[i] = v.(string)
}
params["alpn"] = strings.Join(alpnList, ",")
}
}
port, _ := addr["server_port"].(float64)
uri := fmt.Sprintf("trojan://%s@%s:%d", password, addr["server"].(string), uint(port))
uri = addParams(uri, params, addr["remark"].(string))
links = append(links, uri)
}
return links
}
func vmessLink(
userConfig map[string]interface{},
inbound map[string]interface{},
addrs []map[string]interface{}) []string {
uuid, _ := userConfig["uuid"].(string)
trasportParams := getTransportParams(inbound["transport"])
var links []string
baseParams := map[string]interface{}{
"v": 2,
"id": uuid,
"aid": 0,
}
if trasportParams["type"] == "http" || trasportParams["type"] == "tcp" {
baseParams["net"] = "tcp"
if trasportParams["type"] == "http" {
baseParams["type"] = "http"
}
} else {
baseParams["net"] = trasportParams["type"]
}
for _, addr := range addrs {
obj := baseParams
obj["addr"], _ = addr["server"].(string)
port, _ := addr["server_port"].(float64)
obj["port"] = uint(port)
obj["ps"], _ = addr["remark"].(string)
if trasportParams["host"] != "" {
obj["host"] = trasportParams["host"]
}
if trasportParams["path"] != "" {
obj["path"] = trasportParams["path"]
}
if tls, ok := addr["tls"].(map[string]interface{}); ok && tls["enabled"].(bool) {
obj["tls"] = "tls"
if insecure, ok := tls["insecure"].(bool); ok && insecure {
obj["allowInsecure"] = 1
}
if sni, ok := tls["server_name"].(string); ok {
obj["sni"] = sni
}
} else {
obj["tls"] = "none"
}
jsonStr, _ := json.MarshalIndent(obj, "", " ")
uri := fmt.Sprintf("vmess://%s", toBase64(jsonStr))
links = append(links, uri)
}
return links
}
func toBase64(d []byte) string {
return base64.StdEncoding.EncodeToString([]byte(d))
}
func addParams(uri string, params map[string]string, remark string) string {
URL, _ := url.Parse(uri)
q := URL.Query()
for k, v := range params {
q.Add(k, v)
}
URL.RawQuery = q.Encode()
URL.Fragment = remark
return URL.String()
}
func getTransportParams(t interface{}) map[string]string {
params := map[string]string{}
trasport, _ := t.(map[string]interface{})
if transportType, ok := trasport["type"].(string); ok {
params["type"] = transportType
} else {
params["type"] = "tcp"
return params
}
switch params["type"] {
case "http":
if host, ok := trasport["host"].([]interface{}); ok {
var hosts []string
for _, v := range host {
hosts = append(hosts, v.(string))
}
params["host"] = strings.Join(hosts, ",")
}
if path, ok := trasport["path"].(string); ok {
params["path"] = path
}
case "ws":
if path, ok := trasport["path"].(string); ok {
params["path"] = path
}
if headers, ok := trasport["headers"].(map[string]interface{}); ok {
if host, ok := headers["Host"].(string); ok {
params["peer"] = host
}
}
case "grpc":
if serviceName, ok := trasport["service_name"].(string); ok {
params["serviceName"] = serviceName
}
case "httpupgrade":
if host, ok := trasport["host"].(string); ok {
params["peer"] = host
}
if path, ok := trasport["path"].(string); ok {
params["path"] = path
}
}
return params
}
+473
View File
@@ -0,0 +1,473 @@
package util
import (
"encoding/json"
"fmt"
"net"
"net/url"
"s-ui/util/common"
"strconv"
"strings"
)
func GetOutbound(uri string, i int) (*map[string]interface{}, string, error) {
u, err := url.Parse(uri)
if err == nil {
switch u.Scheme {
case "vmess":
return vmess(u.Host, i)
case "vless":
return vless(u, i)
case "trojan":
return trojan(u, i)
case "hy", "hysteria":
return hy(u, i)
case "hy2", "hysteria2":
return hy2(u, i)
case "tuic":
return tuic(u, i)
case "ss", "shadowsocks":
return ss(u, i)
}
}
return nil, "", common.NewError("Unsupported link format")
}
func vmess(data string, i int) (*map[string]interface{}, string, error) {
dataByte, err := B64StrToByte(data)
if err != nil {
return nil, "", err
}
var dataJson map[string]interface{}
err = json.Unmarshal(dataByte, &dataJson)
if err != nil {
return nil, "", err
}
transport := map[string]interface{}{}
tp_net, _ := dataJson["net"].(string)
tp_type, _ := dataJson["type"].(string)
tp_host, _ := dataJson["host"].(string)
tp_path, _ := dataJson["path"].(string)
switch strings.ToLower(tp_net) {
case "tcp", "":
if tp_type == "http" {
transport["type"] = tp_type
if len(tp_host) > 0 {
transport["host"] = strings.Split(tp_host, ",")
}
transport["path"] = tp_path
}
case "http", "h2":
transport["type"] = "http"
if len(tp_host) > 0 {
transport["host"] = strings.Split(tp_host, ",")
}
transport["path"] = tp_path
case "ws":
transport["type"] = tp_net
transport["path"] = tp_path
transport["early_data_header_name"] = "Sec-WebSocket-Protocol"
if len(tp_host) > 0 {
transport["headers"] = map[string]interface{}{
"Host": tp_host,
}
}
case "quic":
transport["type"] = tp_net
case "grpc":
transport["type"] = tp_net
transport["service_name"] = tp_path
case "httpupgrade":
transport["type"] = tp_net
transport["path"] = tp_path
transport["host"] = tp_host
default:
return nil, "", common.NewError("Invalid vmess")
}
tls := map[string]interface{}{}
vmess_tls, _ := dataJson["tls"].(string)
if vmess_tls == "tls" {
tls["enabled"] = true
tls_sni, _ := dataJson["sni"].(string)
tls_alpn, _ := dataJson["alpn"].(string)
_, tls_insecure := dataJson["allowInsecure"]
tls_fp, _ := dataJson["fp"].(string)
if len(tls_sni) > 0 {
tls["server_name"] = tls_sni
}
if len(tls_alpn) > 0 {
tls["alpn"] = strings.Split(tls_alpn, ",")
}
if tls_insecure {
tls["insecure"] = true
}
if len(tls_fp) > 0 {
tls["utls"] = map[string]interface{}{
"enabled": true,
"fingerprint": tls_fp,
}
}
}
tag, _ := dataJson["ps"].(string)
if i > 0 {
tag = fmt.Sprintf("%d.%s", i, tag)
}
alter_id, ok := dataJson["aid"].(int)
if !ok {
alter_id = 0
}
vmess := map[string]interface{}{
"type": "vmess",
"tag": tag,
"server": dataJson["add"],
"server_port": dataJson["port"],
"uuid": dataJson["id"],
"security": "auto",
"alter_id": alter_id,
"tls": tls,
"transport": transport,
}
return &vmess, tag, err
}
func vless(u *url.URL, i int) (*map[string]interface{}, string, error) {
query, _ := url.ParseQuery(u.RawQuery)
security := query.Get("security")
host, portStr, _ := net.SplitHostPort(u.Host)
port := 80
if len(portStr) > 0 {
port, _ = strconv.Atoi(portStr)
} else {
if security == "tls" || security == "reality" {
port = 443
}
}
tp_type := query.Get("type")
tag := u.Fragment
if i > 0 {
tag = fmt.Sprintf("%d.%s", i, u.Fragment)
}
vless := map[string]interface{}{
"type": "vless",
"tag": tag,
"server": host,
"server_port": port,
"uuid": u.User.Username(),
"flow": query.Get("flow"),
"tls": getTls(security, &query),
"transport": getTransport(tp_type, &query),
}
return &vless, tag, nil
}
func trojan(u *url.URL, i int) (*map[string]interface{}, string, error) {
query, _ := url.ParseQuery(u.RawQuery)
security := query.Get("security")
host, portStr, _ := net.SplitHostPort(u.Host)
port := 80
if len(portStr) > 0 {
port, _ = strconv.Atoi(portStr)
} else {
if security == "tls" || security == "reality" {
port = 443
}
}
tp_type := query.Get("type")
tag := u.Fragment
if i > 0 {
tag = fmt.Sprintf("%d.%s", i, u.Fragment)
}
trojan := map[string]interface{}{
"type": "trojan",
"tag": tag,
"server": host,
"server_port": port,
"password": u.User.Username(),
"tls": getTls(security, &query),
"transport": getTransport(tp_type, &query),
}
return &trojan, tag, nil
}
func hy(u *url.URL, i int) (*map[string]interface{}, string, error) {
query, _ := url.ParseQuery(u.RawQuery)
host, portStr, _ := net.SplitHostPort(u.Host)
port := 443
if len(portStr) > 0 {
port, _ = strconv.Atoi(portStr)
}
tls := map[string]interface{}{
"enabled": true,
"server_name": query.Get("peer"),
}
alpn := query.Get("alpn")
insecure := query.Get("insecure")
if len(alpn) > 0 {
tls["alpn"] = strings.Split(alpn, ",")
}
if insecure == "1" || insecure == "true" {
tls["insecure"] = true
}
tag := u.Fragment
if i > 0 {
tag = fmt.Sprintf("%d.%s", i, u.Fragment)
}
hy := map[string]interface{}{
"type": "hysteria",
"tag": tag,
"server": host,
"server_port": port,
"obfs": query.Get("obfsParam"),
"auth_str": query.Get("auth"),
"tls": tls,
}
down, _ := strconv.Atoi(query.Get("downmbps"))
up, _ := strconv.Atoi(query.Get("upmbps"))
recv_window_conn, _ := strconv.Atoi(query.Get("recv_window_conn"))
recv_window, _ := strconv.Atoi(query.Get("recv_window"))
if down > 0 {
hy["down_mbps"] = down
}
if up > 0 {
hy["up_mbps"] = up
}
if recv_window_conn > 0 {
hy["recv_window_conn"] = recv_window_conn
}
if recv_window > 0 {
hy["recv_window"] = recv_window
}
return &hy, tag, nil
}
func hy2(u *url.URL, i int) (*map[string]interface{}, string, error) {
query, _ := url.ParseQuery(u.RawQuery)
host, portStr, _ := net.SplitHostPort(u.Host)
port := 443
if len(portStr) > 0 {
port, _ = strconv.Atoi(portStr)
}
tls := map[string]interface{}{
"enabled": true,
"server_name": query.Get("sni"),
}
alpn := query.Get("alpn")
insecure := query.Get("insecure")
if len(alpn) > 0 {
tls["alpn"] = strings.Split(alpn, ",")
}
if insecure == "1" || insecure == "true" {
tls["insecure"] = true
}
tag := u.Fragment
if i > 0 {
tag = fmt.Sprintf("%d.%s", i, u.Fragment)
}
hy2 := map[string]interface{}{
"type": "hysteria2",
"tag": tag,
"server": host,
"server_port": port,
"password": u.User.Username(),
"tls": tls,
}
down, _ := strconv.Atoi(query.Get("downmbps"))
up, _ := strconv.Atoi(query.Get("upmbps"))
obfs := query.Get("obfs")
if down > 0 {
hy2["down_mbps"] = down
}
if up > 0 {
hy2["up_mbps"] = up
}
if obfs == "salamander" {
hy2["obfs"] = map[string]interface{}{
"type": "salamander",
"password": query.Get("obfs-password"),
}
}
return &hy2, tag, nil
}
func tuic(u *url.URL, i int) (*map[string]interface{}, string, error) {
query, _ := url.ParseQuery(u.RawQuery)
host, portStr, _ := net.SplitHostPort(u.Host)
port := 443
if len(portStr) > 0 {
port, _ = strconv.Atoi(portStr)
}
tls := map[string]interface{}{
"enabled": true,
"server_name": query.Get("sni"),
}
alpn := query.Get("alpn")
insecure := query.Get("allow_insecure")
disable_sni := query.Get("disable_sni")
if len(alpn) > 0 {
tls["alpn"] = strings.Split(alpn, ",")
}
if insecure == "1" || insecure == "true" {
tls["insecure"] = true
}
if disable_sni == "1" || disable_sni == "true" {
tls["disable_sni"] = true
}
tag := u.Fragment
if i > 0 {
tag = fmt.Sprintf("%d.%s", i, u.Fragment)
}
password, _ := u.User.Password()
tuic := map[string]interface{}{
"type": "tuic",
"tag": tag,
"server": host,
"server_port": port,
"uuid": u.User.Username(),
"password": password,
"congestion_control": query.Get("congestion_control"),
"udp_relay_mode": query.Get("udp_relay_mode"),
"tls": tls,
}
return &tuic, tag, nil
}
func ss(u *url.URL, i int) (*map[string]interface{}, string, error) {
query, _ := url.ParseQuery(u.RawQuery)
host, portStr, _ := net.SplitHostPort(u.Host)
port := 443
if len(portStr) > 0 {
port, _ = strconv.Atoi(portStr)
}
method := u.User.Username()
password, ok := u.User.Password()
if !ok {
decrypted := StrOrBase64Encoded(method)
decrypted_arr := strings.Split(decrypted, ":")
if len(decrypted_arr) > 1 {
method = decrypted_arr[0]
password = strings.Join(decrypted_arr[1:], ":")
} else {
return nil, "", common.NewError("Unsupported shadowsocks")
}
}
tag := u.Fragment
if i > 0 {
tag = fmt.Sprintf("%d.%s", i, u.Fragment)
}
ss := map[string]interface{}{
"type": "shadowsocks",
"tag": tag,
"server": host,
"server_port": port,
"method": method,
"password": password,
}
v2ray_type := query.Get("type")
if len(v2ray_type) > 0 {
pl_arr := []string{}
host_header := query.Get("host")
if query.Get("security") == "tls" {
pl_arr = append(pl_arr, "tls")
}
if v2ray_type == "quic" {
pl_arr = append(pl_arr, "mode=quic")
}
if len(host_header) > 0 {
pl_arr = append(pl_arr, "host="+host_header)
}
ss["plugin"] = "v2ray-plugin"
ss["plugin_opts"] = strings.Join(pl_arr, ";")
}
plugin := query.Get("plugin")
if len(plugin) > 0 {
pl_arr := strings.Split(plugin, ";")
if len(pl_arr) > 0 {
ss["plugin"] = pl_arr[0]
ss["plugin_opts"] = strings.Join(pl_arr[1:], ";")
}
}
return &ss, tag, nil
}
func getTransport(tp_type string, q *url.Values) *map[string]interface{} {
transport := map[string]interface{}{}
tp_host := q.Get("host")
tp_path := q.Get("path")
switch strings.ToLower(tp_type) {
case "tcp", "":
if q.Get("headerType") == "http" {
transport["type"] = "http"
if len(tp_host) > 0 {
transport["host"] = strings.Split(tp_host, ",")
}
transport["path"] = tp_path
}
case "http", "h2":
transport["type"] = "http"
if len(tp_host) > 0 {
transport["host"] = strings.Split(tp_host, ",")
}
transport["path"] = tp_path
case "ws":
transport["type"] = "ws"
transport["path"] = tp_path
if len(tp_host) > 0 {
transport["headers"] = map[string]interface{}{
"Host": tp_host,
}
}
case "quic":
transport["type"] = "quic"
case "grpc":
transport["type"] = "grpc"
transport["service_name"] = q.Get("serviceName")
case "httpupgrade":
transport["type"] = "httpupgrade"
transport["path"] = tp_path
transport["host"] = tp_host
}
return &transport
}
func getTls(security string, q *url.Values) *map[string]interface{} {
tls := map[string]interface{}{}
tls_fp := q.Get("fp")
tls_sni := q.Get("sni")
tls_insecure := q.Get("allowInsecure")
tls_alpn := q.Get("alpn")
switch security {
case "tls":
tls["enabled"] = true
case "reality":
tls["enabled"] = true
tls["reality"] = map[string]interface{}{
"enabled": true,
"public_key": q.Get("pbk"),
"short_id": q.Get("sid"),
}
}
if len(tls_sni) > 0 {
tls["server_name"] = tls_sni
}
if len(tls_alpn) > 0 {
tls["alpn"] = strings.Split(tls_alpn, ",")
}
if tls_insecure == "1" || tls_insecure == "true" {
tls["insecure"] = true
}
if len(tls_fp) > 0 {
tls["utls"] = map[string]interface{}{
"enabled": true,
"fingerprint": tls_fp,
}
}
return &tls
}
+183
View File
@@ -0,0 +1,183 @@
package util
import (
"encoding/json"
"math/rand"
"s-ui/database/model"
)
// Fill Inbound's out_json
func FillOutJson(i *model.Inbound, hostname string) error {
var outJson map[string]interface{}
err := json.Unmarshal(i.OutJson, &outJson)
if err != nil {
return err
}
if i.TlsId > 0 {
addTls(&outJson, i.Tls)
} else {
delete(outJson, "tls")
}
inbound, err := i.MarshalFull()
outJson["type"] = i.Type
outJson["tag"] = i.Tag
outJson["server"] = hostname
outJson["server_port"] = (*inbound)["listen_port"]
switch i.Type {
case "http", "socks", "mixed":
case "shadowsocks":
shadowsocksOut(&outJson, *inbound)
return nil
case "shadowtls":
shadowTlsOut(&outJson, *inbound)
case "hysteria":
hysteriaOut(&outJson, *inbound)
case "hysteria2":
hysteria2Out(&outJson, *inbound)
case "tuic":
tuicOut(&outJson, *inbound)
case "vless":
vlessOut(&outJson, *inbound)
case "trojan":
trojanOut(&outJson, *inbound)
case "vmess":
vmessOut(&outJson, *inbound)
default:
for key := range outJson {
delete(outJson, key)
}
}
i.OutJson, err = json.MarshalIndent(outJson, "", " ")
if err != nil {
return err
}
return nil
}
// addTls function
func addTls(out *map[string]interface{}, tls *model.Tls) {
var tlsServer, tlsConfig map[string]interface{}
err := json.Unmarshal(tls.Server, &tlsServer)
if err != nil {
return
}
err = json.Unmarshal(tls.Client, &tlsConfig)
if err != nil {
return
}
if enabled, ok := tlsServer["enabled"]; ok {
tlsConfig["enabled"] = enabled
}
if serverName, ok := tlsServer["server_name"]; ok {
tlsConfig["server_name"] = serverName
}
if alpn, ok := tlsServer["alpn"]; ok {
tlsConfig["alpn"] = alpn
}
if minVersion, ok := tlsServer["min_version"]; ok {
tlsConfig["min_version"] = minVersion
}
if maxVersion, ok := tlsServer["max_version"]; ok {
tlsConfig["max_version"] = maxVersion
}
if cipherSuites, ok := tlsServer["cipher_suites"]; ok {
tlsConfig["cipher_suites"] = cipherSuites
}
if reality, ok := tlsServer["reality"].(map[string]interface{}); ok && reality["enabled"].(bool) {
realityConfig := tlsConfig["reality"].(map[string]interface{})
realityConfig["enabled"] = true
if shortIDs, ok := reality["short_id"].([]interface{}); ok && len(shortIDs) > 0 {
realityConfig["short_id"] = shortIDs[rand.Intn(len(shortIDs))]
}
tlsConfig["reality"] = realityConfig
}
(*out)["tls"] = tlsConfig
}
// Protocol-specific functions
func shadowsocksOut(out *map[string]interface{}, inbound map[string]interface{}) {
if method, ok := inbound["method"].(string); ok {
(*out)["method"] = method
}
}
func shadowTlsOut(out *map[string]interface{}, inbound map[string]interface{}) {
if version, ok := inbound["version"].(float64); ok && int(version) == 3 {
(*out)["version"] = 3
} else {
for key := range *out {
delete(*out, key)
}
}
(*out)["tls"] = map[string]interface{}{"enabled": true}
}
func hysteriaOut(out *map[string]interface{}, inbound map[string]interface{}) {
if upMbps, ok := inbound["down_mbps"]; ok {
(*out)["up_mbps"] = upMbps
}
if downMbps, ok := inbound["up_mbps"]; ok {
(*out)["down_mbps"] = downMbps
}
if obfs, ok := inbound["obfs"]; ok {
(*out)["obfs"] = obfs
}
if recvWindow, ok := inbound["recv_window_conn"]; ok {
(*out)["recv_window_conn"] = recvWindow
}
if disableMTU, ok := inbound["disable_mtu_discovery"]; ok {
(*out)["disable_mtu_discovery"] = disableMTU
}
}
func hysteria2Out(out *map[string]interface{}, inbound map[string]interface{}) {
if upMbps, ok := inbound["down_mbps"]; ok {
(*out)["up_mbps"] = upMbps
}
if downMbps, ok := inbound["up_mbps"]; ok {
(*out)["down_mbps"] = downMbps
}
if obfs, ok := inbound["obfs"]; ok {
(*out)["obfs"] = obfs
}
}
func tuicOut(out *map[string]interface{}, inbound map[string]interface{}) {
if congestionControl, ok := inbound["congestion_control"].(string); ok {
(*out)["congestion_control"] = congestionControl
} else {
(*out)["congestion_control"] = "cubic"
}
if zeroRTT, ok := inbound["zero_rtt_handshake"].(bool); ok {
(*out)["zero_rtt_handshake"] = zeroRTT
}
if heartbeat, ok := inbound["heartbeat"]; ok {
(*out)["heartbeat"] = heartbeat
}
}
func vlessOut(out *map[string]interface{}, inbound map[string]interface{}) {
if transport, ok := inbound["transport"]; ok {
(*out)["transport"] = transport
}
}
func trojanOut(out *map[string]interface{}, inbound map[string]interface{}) {
if transport, ok := inbound["transport"]; ok {
(*out)["transport"] = transport
}
}
func vmessOut(out *map[string]interface{}, inbound map[string]interface{}) {
if transport, ok := inbound["transport"]; ok {
(*out)["transport"] = transport
}
}
+2 -2
View File
@@ -18,9 +18,9 @@ import (
"strconv" "strconv"
"strings" "strings"
sessions "github.com/Calidity/gin-sessions"
"github.com/Calidity/gin-sessions/cookie"
"github.com/gin-contrib/gzip" "github.com/gin-contrib/gzip"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
+4 -2
View File
@@ -1,13 +1,15 @@
#!/bin/sh #!/bin/sh
cd frontend cd frontend
npm i
npm run build npm run build
cd .. cd ..
cd backend cd backend
echo "Backend" echo "Backend"
mkdir -p web/html
rm -fr web/html/* rm -fr web/html/*
cp -R ../frontend/dist/ web/html/ cp -R ../frontend/dist/* web/html/
go build -o ../sui main.go go build -tags "with_quic,with_grpc,with_ech,with_utls,with_reality_server,with_acme,with_gvisor" -o ../sui main.go
-28
View File
@@ -1,28 +0,0 @@
FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS singbox-builder
LABEL maintainer="Alireza <alireza7@gmail.com>"
WORKDIR /app
ARG TARGETOS TARGETARCH
ARG SINGBOX_VER=v1.8.10
ARG SINGBOX_TAGS="with_quic,with_grpc,with_wireguard,with_ech,with_utls,with_reality_server,with_acme,with_v2ray_api,with_clash_api,with_gvisor"
ARG GOPROXY=""
ENV GOPROXY ${GOPROXY}
ENV CGO_ENABLED=0
ENV GOOS=$TARGETOS
ENV GOARCH=$TARGETARCH
RUN apk --no-cache --update add build-base gcc wget unzip git
RUN set -ex \
&& git clone --depth 1 --branch $SINGBOX_VER https://github.com/SagerNet/sing-box.git \
&& cd sing-box \
&& go build -v -trimpath -tags \
$SINGBOX_TAGS \
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$SINGBOX_VER\" -s -w -buildid=" \
./cmd/sing-box
FROM --platform=$BUILDPLATFORM alpine
LABEL maintainer="Alireza <alireza7@gmail.com>"
ENV TZ=Asia/Tehran
WORKDIR /app
RUN apk add --no-cache --update ca-certificates tzdata bash
COPY --from=singbox-builder /app/sing-box/sing-box .
COPY runSingbox.sh .
ENTRYPOINT [ "./runSingbox.sh" ]
-45
View File
@@ -1,45 +0,0 @@
#!/bin/bash
set -e
tokill=$$
runSingbox(){
./sing-box run &
tokill=$!
}
terminateSingbox()
{
if kill -0 $tokill > /dev/null 2>&1; then
echo "Terminating singbox PID=$tokill"
kill $tokill
while kill -0 $tokill > /dev/null 2>&1; do
sleep 1
done
fi
}
trap terminateSingbox SIGINT SIGTERM SIGKILL
runSingbox
while true
do
sleep 5
if [ -f "signal" ]; then
signal=`cat signal`
echo "Signal received: $signal"
# Remove singnal file
rm -f signal >> /dev/null 2>&1
case ${signal} in
"stop")
terminateSingbox
;;
"restart")
terminateSingbox
runSingbox
;;
esac
fi
done
+5 -30
View File
@@ -1,18 +1,12 @@
--- ---
version: "3"
services: services:
s-ui: s-ui:
image: alireza7/s-ui image: alireza7/s-ui
container_name: s-ui container_name: s-ui
hostname: "S-UI docker" hostname: "s-ui"
volumes: volumes:
- "singbox:/app/bin" - "./db:/app/db"
- "$PWD/db:/app/db" - "./cert:/app/cert"
- "$PWD/cert:/app/cert"
environment:
SINGBOX_API: "sing-box:1080"
SUI_DB_FOLDER: "db"
tty: true tty: true
restart: unless-stopped restart: unless-stopped
ports: ports:
@@ -20,28 +14,9 @@ services:
- "2096:2096" - "2096:2096"
networks: networks:
- s-ui - s-ui
entrypoint: "./sui" entrypoint: "./entrypoint.sh"
sing-box:
image: alireza7/s-ui-singbox
container_name: sing-box
volumes:
- "singbox:/app/"
- "$PWD/cert:/cert"
networks:
- s-ui
ports:
- "443:443"
- "1443:1443"
- "2443:2443"
- "3443:3443"
restart: unless-stopped
depends_on:
- s-ui
networks: networks:
s-ui: s-ui:
driver: bridge driver: bridge
volumes:
singbox:
Executable
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
./sui migrate
./sui
+1386 -2794
View File
File diff suppressed because it is too large Load Diff
+23 -24
View File
@@ -1,43 +1,42 @@
{ {
"name": "frontend", "name": "frontend",
"version": "0.0.0", "version": "1.2.0-beta.1",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite --host",
"build": "vue-tsc --noEmit && vite build", "build": "vue-tsc --noEmit && vite build",
"preview": "vite preview", "preview": "vite preview",
"lint": "eslint . --fix --ignore-path .gitignore" "lint": "eslint . --fix --ignore-path .gitignore"
}, },
"dependencies": { "dependencies": {
"@mdi/font": "7.0.96", "@mdi/font": "7.4.47",
"axios": "^1.6.5", "axios": "^1.7.4",
"chart.js": "^4.4.1", "chart.js": "^4.4.3",
"clipboard": "^2.0.11", "clipboard": "^2.0.11",
"core-js": "^3.29.0", "core-js": "^3.37.1",
"moment": "^2.30.1", "moment": "^2.30.1",
"notivue": "^2.4.4",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"qrcode.vue": "^3.4.1", "qrcode.vue": "^3.4.1",
"roboto-fontface": "*", "roboto-fontface": "^0.10.0",
"vue": "^3.2.0", "vue": "^3.4.31",
"vue-chartjs": "^5.3.0", "vue-chartjs": "^5.3.1",
"vue-i18n": "^9.8.0", "vue-i18n": "^9.14.2",
"vue-router": "^4.0.0", "vue-router": "^4.4.0",
"vue3-persian-datetime-picker": "^1.2.2", "vue3-persian-datetime-picker": "^1.2.2",
"vuetify": "^3.0.0" "vuetify": "^3.6.10"
}, },
"devDependencies": { "devDependencies": {
"@babel/types": "^7.21.4", "@babel/types": "^7.24.7",
"@types/node": "^18.15.0", "@types/node": "^20.14.9",
"@vitejs/plugin-vue": "^4.0.0", "@vitejs/plugin-vue": "^5.0.5",
"@vue/eslint-config-typescript": "^11.0.0", "eslint-plugin-vue": "^9.26.0",
"eslint": "^8.22.0",
"eslint-plugin-vue": "^9.3.0",
"material-design-icons-iconfont": "^6.7.0", "material-design-icons-iconfont": "^6.7.0",
"sass": "^1.60.0", "sass": "1.77.6",
"typescript": "^5.0.0", "typescript": "^5.5.2",
"unplugin-fonts": "^1.0.3", "unplugin-fonts": "^1.1.1",
"vite": "^4.5.3", "vite": "^5.4.6",
"vite-plugin-vuetify": "^1.0.0", "vite-plugin-vuetify": "^2.0.3",
"vue-tsc": "^1.2.0" "vue-tsc": "^2.0.22"
} }
} }
+73
View File
@@ -0,0 +1,73 @@
<template>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('out.addr')"
hide-details
required
v-model="addr.server">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('out.port')"
hide-details
type="number"
required
v-model.number="addr.server_port"></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="optionRemark">
<v-text-field
:label="$t('in.remark')"
hide-details
v-model="addr.remark">
</v-text-field>
</v-col>
</v-row>
<OutTLS :outbound="addr" v-if="optionTLS" />
<v-row>
<v-spacer></v-spacer>
<v-col cols="auto" align="end" justify="center">
<v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('in.mdOption') }}</v-btn>
</template>
<v-card>
<v-list>
<v-list-item>
<v-switch v-model="optionRemark" color="primary" :label="$t('in.remark')" hide-details></v-switch>
</v-list-item>
<v-list-item v-if="hasTls">
<v-switch v-model="optionTLS" color="primary" :label="$t('objects.tls')" hide-details></v-switch>
</v-list-item>
</v-list>
</v-card>
</v-menu>
</v-col>
</v-row>
</template>
<script lang="ts">
import OutTLS from '@/components/tls/OutTLS.vue'
export default {
props: ['addr', 'hasTls'],
data() {
return {
menu: false
}
},
computed: {
optionTLS: {
get(): boolean { return this.$props.addr.tls != undefined },
set(v:boolean) { this.$props.addr.tls = v ? { enabled: true } : undefined; }
},
optionRemark: {
get(): boolean { return this.$props.addr.remark != undefined },
set(v:boolean) { this.$props.addr.remark = v ? '' : undefined }
}
},
components: {
OutTLS
}
}
</script>
+9 -1
View File
@@ -42,6 +42,7 @@
<script lang="ts"> <script lang="ts">
import DatePicker from 'vue3-persian-datetime-picker' import DatePicker from 'vue3-persian-datetime-picker'
import { i18n } from '@/locales' import { i18n } from '@/locales'
import 'moment/locale/vi'
import 'moment/locale/zh-cn' import 'moment/locale/zh-cn'
import 'moment/locale/zh-tw' import 'moment/locale/zh-tw'
@@ -58,7 +59,14 @@ export default {
computed: { computed: {
locale() { locale() {
const l = i18n.global.locale.value const l = i18n.global.locale.value
return l.replace('zh', 'zh-') switch (l) {
case "zhHans":
return "zh-cn"
case "zhHant":
return "zh-tw"
default:
return l
}
}, },
dateFormatted() { dateFormatted() {
if (this.expDate == 0) return i18n.global.t('unlimited') if (this.expDate == 0) return i18n.global.t('unlimited')
+1 -1
View File
@@ -89,7 +89,7 @@
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start"> <v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details>{{ $t('dial.options') }}</v-btn> <v-btn v-bind="props" hide-details variant="tonal">{{ $t('dial.options') }}</v-btn>
</template> </template>
<v-card> <v-card>
<v-list> <v-list>
-213
View File
@@ -1,213 +0,0 @@
<template>
<v-card :subtitle="$t('objects.tls')">
<v-row v-if="tlsOptional">
<v-col cols="auto">
<v-switch color="primary" :label="$t('tls.enable')" v-model="tlsEnable" hide-details></v-switch>
</v-col>
</v-row>
<template v-if="tls.enabled">
<v-row>
<v-col cols="auto">
<v-btn-toggle v-model="usePath"
class="rounded-xl"
density="compact"
variant="outlined"
shaped
mandatory>
<v-btn
@click="tls.key=undefined; tls.certificate=undefined"
>{{ $t('tls.usePath') }}</v-btn>
<v-btn
@click="tls.key_path=undefined; tls.certificate_path=undefined"
>{{ $t('tls.useText') }}</v-btn>
</v-btn-toggle>
</v-col>
</v-row>
<v-row v-if="usePath == 0">
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('tls.certPath')"
hide-details
v-model="tls.certificate_path">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('tls.keyPath')"
hide-details
v-model="tls.key_path">
</v-text-field>
</v-col>
</v-row>
<v-row v-else>
<v-col cols="12" sm="6">
<v-textarea
:label="$t('tls.cert')"
hide-details
v-model="certText">
</v-textarea>
</v-col>
<v-col cols="12" sm="6">
<v-textarea
:label="$t('tls.key')"
hide-details
v-model="keyText">
</v-textarea>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4" v-if="tls.server_name != undefined">
<v-text-field
label="SNI"
hide-details
v-model="tls.server_name">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="tls.alpn">
<v-select
hide-details
label="ALPN"
multiple
:items="alpn"
v-model="tls.alpn">
</v-select>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4" v-if="tls.min_version">
<v-select
hide-details
:label="$t('tls.minVer')"
:items="tlsVersions"
v-model="tls.min_version">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="tls.max_version">
<v-select
hide-details
:label="$t('tls.maxVer')"
:items="tlsVersions"
v-model="tls.max_version">
</v-select>
</v-col>
</v-row>
<v-row>
<v-col cols="12" md="8" v-if="tls.cipher_suites != undefined">
<v-select
hide-details
:label="$t('tls.cs')"
multiple
:items="cipher_suites"
v-model="tls.cipher_suites">
</v-select>
</v-col>
</v-row>
</template>
<v-card-actions v-if="tls.enabled">
<v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start" v-if="tls.enabled">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details>{{ $t('tls.options') }}</v-btn>
</template>
<v-card>
<v-list>
<v-list-item>
<v-switch v-model="optionSNI" color="primary" label="SNI" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionALPN" color="primary" label="ALPN" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionMinV" color="primary" :label="$t('tls.minVer')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionMaxV" color="primary" :label="$t('tls.maxVer')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionCS" color="primary" :label="$t('tls.cs')" hide-details></v-switch>
</v-list-item>
</v-list>
</v-card>
</v-menu>
</v-card-actions>
</v-card>
</template>
<script lang="ts">
import { iTls, defaultInTls } from '@/types/inTls'
export default {
props: ['inbound'],
data() {
return {
menu: false,
usePath: 0,
defaults: defaultInTls,
alpn: [
{ title: "H3", value: 'h3' },
{ title: "H2", value: 'h2' },
{ title: "Http/1.1", value: 'http/1.1' },
],
tlsVersions: [ '1.0', '1.1', '1.2', '1.3' ],
cipher_suites: [
{ title: "RSA-AES128-CBC-SHA", value: "TLS_RSA_WITH_AES_128_CBC_SHA" },
{ title: "RSA-AES256-CBC-SHA", value: "TLS_RSA_WITH_AES_256_CBC_SHA" },
{ title: "RSA-AES128-GCM-SHA256", value: "TLS_RSA_WITH_AES_128_GCM_SHA256" },
{ title: "RSA-AES256-GCM-SHA384", value: "TLS_RSA_WITH_AES_256_GCM_SHA384" },
{ title: "AES128-GCM-SHA256", value: "TLS_AES_128_GCM_SHA256" },
{ title: "AES256-GCM-SHA384", value: "TLS_AES_256_GCM_SHA384" },
{ title: "CHACHA20-POLY1305-SHA256", value: "TLS_CHACHA20_POLY1305_SHA256" },
{ title: "ECDHE-ECDSA-AES128-CBC-SHA", value: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA" },
{ title: "ECDHE-ECDSA-AES256-CBC-SHA", value: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA" },
{ title: "ECDHE-RSA-AES128-CBC-SHA", value: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA" },
{ title: "ECDHE-RSA-AES256-CBC-SHA", value: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA" },
{ title: "ECDHE-ECDSA-AES128-GCM-SHA256", value: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" },
{ title: "ECDHE-ECDSA-AES256-GCM-SHA384", value: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" },
{ title: "ECDHE-RSA-AES128-GCM-SHA256", value: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" },
{ title: "ECDHE-RSA-AES256-GCM-SHA384", value: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" },
{ title: "ECDHE-ECDSA-CHACHA20-POLY1305-SHA256", value: "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256" },
{ title: "ECDHE-RSA-CHACHA20-POLY1305-SHA256", value: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256" }
]
}
},
computed: {
tls(): iTls {
return <iTls> this.$props.inbound.tls
},
tlsEnable: {
get() { return Object.hasOwn(this.$props.inbound.tls, 'enabled') ? this.tls.enabled : false },
set(newValue: boolean) { this.$props.inbound.tls = newValue ? { enabled: true } : {} }
},
tlsOptional(): boolean {
return !['hysteria','hysteria2','tuic','naive'].includes(this.$props.inbound.type)
},
certText: {
get(): string { return this.tls.certificate ? this.tls.certificate.join('\n') : '' },
set(newValue:string) { this.tls.certificate = newValue.split('\n') }
},
keyText: {
get(): string { return this.tls.key ? this.tls.key.join('\n') : '' },
set(newValue:string) { this.tls.key = newValue.split('\n') }
},
optionSNI: {
get(): boolean { return this.tls.server_name != undefined },
set(v:boolean) { this.$props.inbound.tls.server_name = v ? '' : undefined }
},
optionALPN: {
get(): boolean { return this.tls.alpn != undefined },
set(v:boolean) { this.$props.inbound.tls.alpn = v ? defaultInTls.alpn : undefined }
},
optionMinV: {
get(): boolean { return this.tls.min_version != undefined },
set(v:boolean) { this.$props.inbound.tls.min_version = v ? defaultInTls.min_version : undefined }
},
optionMaxV: {
get(): boolean { return this.tls.max_version != undefined },
set(v:boolean) { this.$props.inbound.tls.max_version = v ? defaultInTls.max_version : undefined }
},
optionCS: {
get(): boolean { return this.tls.cipher_suites != undefined },
set(v:boolean) { this.$props.inbound.tls.cipher_suites = v ? defaultInTls.cipher_suites : undefined }
}
}
}
</script>
+5 -42
View File
@@ -1,6 +1,6 @@
<template> <template>
<v-card :subtitle="$t('objects.listen')"> <v-card :subtitle="$t('objects.listen')">
<v-row> <v-row v-if="inbound.type != 'tun'">
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-text-field <v-text-field
:label="$t('in.addr')" :label="$t('in.addr')"
@@ -14,6 +14,8 @@
:label="$t('in.port')" :label="$t('in.port')"
hide-details hide-details
type="number" type="number"
min="1"
max="65535"
required required
v-model.number="inbound.listen_port"></v-text-field> v-model.number="inbound.listen_port"></v-text-field>
</v-col> </v-col>
@@ -27,24 +29,6 @@
v-model="inbound.detour"> v-model="inbound.detour">
</v-select> </v-select>
</v-col> </v-col>
<v-col cols="12" sm="6" md="4">
<v-switch v-model="inbound.sniff" color="primary" :label="$t('listen.sniffing')" hide-details></v-switch>
</v-col>
</v-row>
<v-row v-if="inbound.sniff">
<v-col cols="12" sm="6" md="4">
<v-switch v-model="inbound.sniff_override_destination" color="primary" :label="$t('listen.sniffingOverride')" hide-details></v-switch>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('listen.sniffingTimeout')"
hide-details
type="number"
min="50"
step="50"
:suffix="$t('date.ms')"
v-model.number="sniffTimeout"></v-text-field>
</v-col>
</v-row> </v-row>
<v-row v-if="optionTCP"> <v-row v-if="optionTCP">
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
@@ -68,21 +52,11 @@
v-model.number="udpTimeout"></v-text-field> v-model.number="udpTimeout"></v-text-field>
</v-col> </v-col>
</v-row> </v-row>
<v-row v-if="optionDS"> <v-card-actions class="pt-0" v-if="inbound.type != 'tun'">
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
:label="$t('listen.domainStrategy')"
:items="['prefer_ipv4','prefer_ipv6','ipv4_only','ipv6_only']"
v-model="inbound.domain_strategy">
</v-select>
</v-col>
</v-row>
<v-card-actions class="pt-0">
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start"> <v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details>{{ $t('listen.options') }}</v-btn> <v-btn v-bind="props" hide-details variant="tonal">{{ $t('listen.options') }}</v-btn>
</template> </template>
<v-card> <v-card>
<v-list> <v-list>
@@ -95,9 +69,6 @@
<v-list-item> <v-list-item>
<v-switch v-model="optionUDP" color="primary" :label="$t('listen.udpOptions')" hide-details></v-switch> <v-switch v-model="optionUDP" color="primary" :label="$t('listen.udpOptions')" hide-details></v-switch>
</v-list-item> </v-list-item>
<v-list-item>
<v-switch v-model="optionDS" color="primary" :label="$t('listen.domainStrategy')" hide-details></v-switch>
</v-list-item>
</v-list> </v-list>
</v-card> </v-card>
</v-menu> </v-menu>
@@ -118,10 +89,6 @@ export default {
get() { return this.$props.inbound.udp_timeout ? parseInt(this.$props.inbound.udp_timeout.replace('m','')) : 5 }, get() { return this.$props.inbound.udp_timeout ? parseInt(this.$props.inbound.udp_timeout.replace('m','')) : 5 },
set(newValue:number) { this.$props.inbound.udp_timeout = newValue > 0 ? newValue + 'm' : '5m' } set(newValue:number) { this.$props.inbound.udp_timeout = newValue > 0 ? newValue + 'm' : '5m' }
}, },
sniffTimeout: {
get() { return this.$props.inbound.sniff_timeout ? parseInt(this.$props.inbound.sniff_timeout.replace('ms','')) : 300 },
set(newValue:number) { this.$props.inbound.sniff_timeout = newValue > 0 ? newValue + 'ms' : '300ms' }
},
optionTCP: { optionTCP: {
get(): boolean { get(): boolean {
return this.$props.inbound.tcp_fast_open != undefined && return this.$props.inbound.tcp_fast_open != undefined &&
@@ -145,10 +112,6 @@ export default {
optionDetour: { optionDetour: {
get(): boolean { return this.$props.inbound.detour != undefined }, get(): boolean { return this.$props.inbound.detour != undefined },
set(v:boolean) { this.$props.inbound.detour = v ? this.inTags[0]?? '' : undefined } set(v:boolean) { this.$props.inbound.detour = v ? this.inTags[0]?? '' : undefined }
},
optionDS: {
get(): boolean { return this.$props.inbound.domain_strategy != undefined },
set(v:boolean) { this.$props.inbound.domain_strategy = v ? 'prefer_ipv4' : undefined }
} }
} }
} }
+42 -5
View File
@@ -1,5 +1,10 @@
<template> <template>
<v-container class="fill-height"> <LogVue
v-model="logModal.visible"
:visible="logModal.visible"
@close="closeLogs"
/>
<v-container class="fill-height" :loading="loading">
<v-responsive :class="reloadItems.length>0 ? 'fill-height text-center' : 'align-center'" > <v-responsive :class="reloadItems.length>0 ? 'fill-height text-center' : 'align-center'" >
<v-row class="d-flex align-center justify-center"> <v-row class="d-flex align-center justify-center">
<v-col cols="auto"> <v-col cols="auto">
@@ -10,7 +15,7 @@
<v-col cols="auto"> <v-col cols="auto">
<v-dialog v-model="menu" :close-on-content-click="false" transition="scale-transition" max-width="800"> <v-dialog v-model="menu" :close-on-content-click="false" transition="scale-transition" max-width="800">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn v-bind="props" variant="tonal">{{ $t('main.tiles') }} <v-icon icon="mdi-star-plus" /></v-btn> <v-btn v-bind="props" hide-details variant="tonal">{{ $t('main.tiles') }} <v-icon icon="mdi-star-plus" /></v-btn>
</template> </template>
<v-card rounded="xl"> <v-card rounded="xl">
<v-card-title> <v-card-title>
@@ -45,7 +50,7 @@
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12" sm="6" md="3" v-for="i in reloadItems" :key="i"> <v-col cols="12" sm="6" md="3" v-for="i in reloadItems" :key="i">
<v-card class="rounded-lg" variant="outlined" height="200px" <v-card class="rounded-lg" variant="outlined" height="210px"
:title="menuItems.flatMap(cat => cat.value).find(m => m.value == i)?.title"> :title="menuItems.flatMap(cat => cat.value).find(m => m.value == i)?.title">
<v-card-text style="padding: 0 16px;" align="center" justify="center"> <v-card-text style="padding: 0 16px;" align="center" justify="center">
<Gauge :tilesData="tilesData" :type="i" v-if="i.charAt(0) == 'g'" /> <Gauge :tilesData="tilesData" :type="i" v-if="i.charAt(0) == 'g'" />
@@ -80,13 +85,19 @@
</v-col> </v-col>
<v-col cols="3">S-UI</v-col> <v-col cols="3">S-UI</v-col>
<v-col cols="9"> <v-col cols="9">
<v-chip density="compact" color="primary" variant="flat"> <v-chip density="compact" color="blue">
<v-tooltip activator="parent" location="top"> <v-tooltip activator="parent" location="top">
{{ $t('main.info.threads') }}: {{ tilesData.sys?.appThreads }}<br /> {{ $t('main.info.threads') }}: {{ tilesData.sys?.appThreads }}<br />
{{ $t('main.info.memory') }}: {{ HumanReadable.sizeFormat(tilesData.sys?.appMem) }} {{ $t('main.info.memory') }}: {{ HumanReadable.sizeFormat(tilesData.sys?.appMem) }}
</v-tooltip> </v-tooltip>
v{{ tilesData.sys?.appVersion }} v{{ tilesData.sys?.appVersion }}
</v-chip> </v-chip>
<v-chip density="compact" color="transparent" style="cursor: pointer;" @click="openLogs()">
<v-tooltip activator="parent" location="top">
{{ $t('basic.log.title') + " - S-UI" }}
</v-tooltip>
<v-icon icon="mdi-list-box-outline" color="blue" />
</v-chip>
</v-col> </v-col>
<v-col cols="3">{{ $t('main.info.uptime') }}</v-col> <v-col cols="3">{{ $t('main.info.uptime') }}</v-col>
<v-col cols="9">{{ HumanReadable.formatSecond(tilesData.uptime) }}</v-col> <v-col cols="9">{{ HumanReadable.formatSecond(tilesData.uptime) }}</v-col>
@@ -97,7 +108,13 @@
<v-col cols="4">{{ $t('main.info.running') }}</v-col> <v-col cols="4">{{ $t('main.info.running') }}</v-col>
<v-col cols="8"> <v-col cols="8">
<v-chip density="compact" color="success" variant="flat" v-if="tilesData.sbd?.running">{{ $t('yes') }}</v-chip> <v-chip density="compact" color="success" variant="flat" v-if="tilesData.sbd?.running">{{ $t('yes') }}</v-chip>
<v-chip density="compact" color="error" variant="flat" v-else>{{ $t('no') }}</v-chip> <v-chip density="compact" color="error" variant="flat" v-else>{{ $t('no') }}</v-chip>
<v-chip density="compact" color="transparent" v-if="tilesData.sbd?.running && !loading" style="cursor: pointer;" @click="restartSingbox()">
<v-tooltip activator="parent" location="top">
{{ $t('actions.restartSb') }}
</v-tooltip>
<v-icon icon="mdi-restart" color="warning" />
</v-chip>
</v-col> </v-col>
<v-col cols="4">{{ $t('main.info.memory') }}</v-col> <v-col cols="4">{{ $t('main.info.memory') }}</v-col>
<v-col cols="8"> <v-col cols="8">
@@ -148,7 +165,9 @@ import Gauge from '@/components/tiles/Gauge.vue'
import History from '@/components/tiles/History.vue' import History from '@/components/tiles/History.vue'
import { computed, onBeforeUnmount, onMounted, ref } from 'vue' import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { i18n } from '@/locales' import { i18n } from '@/locales'
import LogVue from '@/layouts/modals/Logs.vue'
const loading = ref(false)
const menu = ref(false) const menu = ref(false)
const menuItems = [ const menuItems = [
{ title: i18n.global.t('main.gauges'), value: [ { title: i18n.global.t('main.gauges'), value: [
@@ -215,4 +234,22 @@ onMounted(() => {
onBeforeUnmount(() => { onBeforeUnmount(() => {
stopTimer() stopTimer()
}) })
const logModal = ref({
visible: false,
})
const openLogs = () => {
logModal.value.visible = true
}
const closeLogs = () => {
logModal.value.visible = false
}
const restartSingbox = async () => {
loading.value = true
await HttpUtils.post('api/restartSb',{})
loading.value = false
}
</script> </script>
+1
View File
@@ -85,6 +85,7 @@ export default {
}, },
computed: { computed: {
mux(): oMultiplex { mux(): oMultiplex {
if (!Object.hasOwn(this.$props.data,"multiplex")) this.$props.data.multiplex = {}
return <oMultiplex> this.$props.data.multiplex return <oMultiplex> this.$props.data.multiplex
}, },
muxEnable: { muxEnable: {
+123
View File
@@ -0,0 +1,123 @@
<template>
<v-card :subtitle="type">
<v-row>
<v-col cols="12" sm="6" md="4" v-if="type == inTypes.SOCKS">
<v-select
hide-details
:items="['4','4a','5']"
:label="$t('version')"
v-model="inData.out_json.version">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="needNetwork">
<Network :data="inData.out_json" />
</v-col>
<v-col cols="12" sm="6" md="4" v-if="needUot">
<UoT :data="inData.out_json" />
</v-col>
<v-col cols="12" sm="6" md="4" v-if="type == inTypes.HTTP">
<v-text-field
:label="$t('transport.path')"
hide-details
v-model="inData.out_json.path">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="type == inTypes.VMess || type == inTypes.VLESS">
<v-select
hide-details
:label="$t('types.vless.udpEnc')"
:items="['none','packetaddr','xudp']"
v-model="packet_encoding">
</v-select>
</v-col>
<template v-if="type == inTypes.VMess">
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
:label="$t('types.vmess.security')"
:items="vmessSecurities"
v-model="inData.out_json.security">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch v-model="inData.out_json.global_padding" color="primary" :label="$t('types.vmess.globalPadding')" hide-details></v-switch>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch v-model="inData.out_json.authenticated_length" color="primary" :label="$t('types.vmess.authLen')" hide-details></v-switch>
</v-col>
</template>
<v-col cols="12" sm="6" md="4" v-if="type == inTypes.Hysteria">
<v-text-field
label="Recv window"
hide-details
type="number"
min="0"
v-model.number="inData.out_json.recv_window">
</v-text-field>
</v-col>
<template v-if="type == inTypes.TUIC">
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
label="UDP Relay Mode"
:items="['native', 'quic']"
clearable
@click:clear="delete inData.out_json.udp_relay_mode"
v-model="inData.out_json.udp_relay_mode">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" label="UDP Over Stream" v-model="inData.out_json.udp_over_stream" hide-details></v-switch>
</v-col>
</template>
</v-row>
<Headers :data="inData.out_json" v-if="type == inTypes.HTTP" />
</v-card>
</template>
<script lang="ts">
import { InTypes } from '@/types/inbounds'
import Network from './Network.vue'
import UoT from './UoT.vue'
import Headers from './Headers.vue'
export default {
props: ['inData', 'type'],
data() {
return {
inTypes: InTypes,
vmessSecurities: [
"auto",
"none",
"zero",
"aes-128-gcm",
"aes-128-ctr",
"chacha20-poly1305",
],
haveNetwork: [
InTypes.SOCKS,
InTypes.Shadowsocks,
InTypes.VMess,
InTypes.Trojan,
InTypes.Hysteria,
InTypes.VLESS,
InTypes.TUIC,
InTypes.Hysteria2,
],
havUoT: [
InTypes.SOCKS,
InTypes.Shadowsocks,
],
}
},
computed: {
needNetwork():boolean { return this.haveNetwork.includes(this.$props.type) },
needUot():boolean { return this.havUoT.includes(this.$props.type) },
packet_encoding: {
get() { return this.$props.inData.out_json.packet_encoding != undefined ? this.$props.inData.out_json.packet_encoding : 'none'; },
set(v:string) { this.$props.inData.out_json.packet_encoding = v != "none" ? v : undefined }
},
},
components: { Network, UoT, Headers }
}
</script>
+1 -1
View File
@@ -116,7 +116,7 @@
</v-col> </v-col>
<v-col cols="12" sm="6" v-if="rule.source_ip_cidr != undefined"> <v-col cols="12" sm="6" v-if="rule.source_ip_cidr != undefined">
<v-text-field <v-text-field
:label="$t('rule.srcIp') + ' ' + $t('commaSeparated')" :label="$t('rule.srcCidr') + ' ' + $t('commaSeparated')"
hide-details hide-details
v-model="source_ip_cidr"></v-text-field> v-model="source_ip_cidr"></v-text-field>
</v-col> </v-col>
+452
View File
@@ -0,0 +1,452 @@
<template>
<v-card>
<v-row>
<v-col cols="12" sm="6" md="3">
<v-select
v-model="ruleToDirect"
:items="geoList"
:label="$t('setting.toDirect')"
multiple
chips
hide-details
></v-select>
</v-col>
<v-col cols="12" sm="6" md="3">
<v-select
v-model="ruleToBlock"
:items="geoList"
:label="$t('setting.toBlock')"
multiple
chips
hide-details
></v-select>
</v-col>
</v-row>
<v-row v-if="enableLog">
<v-col cols="12" sm="6" md="3" lg="2">
<v-select
hide-details
:label="$t('basic.log.level')"
:items="levels"
v-model="subJsonExt.log.level">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="3" lg="2">
<v-switch v-model="subJsonExt.log.timestamp" color="primary" :label="$t('setting.timestamp')" hide-details />
</v-col>
</v-row>
<v-row v-if="enableDns">
<v-col cols="12" sm="6" md="3" lg="2">
<v-text-field
v-model="proxyDns"
hide-details
:label="$t('setting.globalDns')"
></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="3" lg="2">
<v-text-field
v-model="directDns"
hide-details
clearable
:label="$t('setting.directDns')"
></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="3" v-if="directDns.length>0">
<v-select
v-model="dnsToDirect"
:items="geositeList"
:label="$t('setting.toDirectDns')"
multiple
chips
hide-details
></v-select>
</v-col>
</v-row>
<template v-if="enableInb">
<v-row>
<v-col cols="12" sm="6" md="3">
<v-combobox
v-model="inbounds[0].address"
:items="defaultInb[0].address"
chips
multiple
hide-details
:label="$t('in.addr')"
></v-combobox>
</v-col>
<v-col cols="12" sm="6" md="3" lg="2">
<v-text-field
type="number"
v-model.number="inbounds[0].mtu"
hide-details
label="MTU"
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="3">
<v-combobox
v-model="inbounds[0].exclude_package"
:items="['ir.mci.ecareapp','com.myirancell']"
chips
multiple
hide-details
:label="$t('setting.excludePkg')"
></v-combobox>
</v-col>
<v-col cols="12" sm="6" md="3" lg="2">
<v-switch
v-model="platformProxy"
hide-details
color="primary"
label="Platform HTTP proxy"
></v-switch>
</v-col>
</v-row>
</template>
<v-card-actions>
<v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('setting.jsonSubOptions') }}</v-btn>
</template>
<v-card>
<v-list>
<v-list-item>
<v-switch v-model="enableLog" color="primary" :label="$t('basic.log.title')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="enableDns" color="primary" label="DNS" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="enableInb" color="primary" :label="$t('objects.inbound')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="enableExp" color="primary" label="Experimental" hide-details></v-switch>
</v-list-item>
</v-list>
</v-card>
</v-menu>
</v-card-actions>
</v-card>
</template>
<script lang="ts">
export default {
props: ['settings'],
data() {
return {
menu: false,
subJsonExt: <any>{},
levels: ["trace", "debug", "info", "warn", "error", "fatal", "panic"],
defaultLog: {
"level": "info",
"timestamp": true
},
defaultInb: [
{
"type": "tun",
"address": [
"172.19.0.1/30",
"fdfe:dcba:9876::1/126"
],
"mtu": 9000,
"auto_route": true,
"strict_route": false,
"sniff": true,
"endpoint_independent_nat": false,
"stack": "system",
"exclude_package": [],
"platform": {
"http_proxy": {
"enabled": true,
"server": "127.0.0.1",
"server_port": 2080
}
}
},
{
"type": "mixed",
"listen": "127.0.0.1",
"listen_port": 2080,
"sniff": true,
"users": []
}
],
defaultExp: {
"clash_api": {
"external_controller": "127.0.0.1:9090",
"external_ui": "ui",
"secret": "",
"external_ui_download_url": "https://mirror.ghproxy.com/https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip",
"external_ui_download_detour": "direct",
"default_mode": "rule"
},
"cache_file": {
"enabled": true,
"store_fakeip": false
}
},
defaultDns: {
"servers": [
{
"address": "tcp://8.8.8.8",
"detour": "proxy",
"address_resolver": "local-dns",
"tag": "proxy-dns"
},
{
"tag": "local-dns",
"address": "local",
"detour": "direct"
},
{
"address": "rcode://success",
"tag": "block"
}
],
"rules": [
{
"clash_mode": "Global",
"source_ip_cidr": [
"172.19.0.0/30"
],
"server": "proxy-dns"
},
{
"source_ip_cidr": [
"172.19.0.0/30"
],
"server": "proxy-dns"
}
],
"final": "local-dns",
"strategy": "prefer_ipv4"
},
geositeList: [
{ title: "Private", value: "geosite-private" },
{ title: "Ads", value: "geosite-ads" },
{ title: "🇮🇷 Iran", value: "geosite-ir" },
{ title: "🇨🇳 China", value: "geosite-cn" },
{ title: "🇻🇳 Vietnam", value: "geosite-vn" },
],
geoList: [
{ title: "Site-Private", value: "geoip-private" },
{ title: "IP-Private", value: "geosite-private" },
{ title: "Site-Ads", value: "geosite-ads" },
{ title: "🇮🇷 Site-Iran", value: "geosite-ir" },
{ title: "🇮🇷 IP-Iran", value: "geoip-ir" },
{ title: "🇨🇳 Site-China", value: "geosite-cn" },
{ title: "🇨🇳 IP-China", value: "geoip-cn" },
{ title: "🇻🇳 Site-Vietnam", value: "geosite-vn" },
{ title: "🇻🇳 IP-Vietnam", value: "geoip-vn" },
],
geo: [
{
tag: "geosite-ads",
type: "remote",
format: "binary",
url: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/category-ads-all.srs",
download_detour: "direct"
},
{
tag: "geosite-private",
type: "remote",
format: "binary",
url: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/private.srs",
download_detour: "direct"
},
{
tag: "geosite-ir",
type: "remote",
format: "binary",
url: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/category-ir.srs",
download_detour: "direct"
},
{
tag: "geosite-cn",
type: "remote",
format: "binary",
url: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/cn.srs",
download_detour: "direct"
},
{
tag: "geosite-vn",
type: "remote",
format: "binary",
url: "https://github.com/Thaomtam/Geosite-vn/raw/rule-set/Geosite-vn.srs",
download_detour: "direct"
},
{
tag: "geoip-private",
type: "remote",
format: "binary",
url: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geoip/private.srs",
download_detour: "direct"
},
{
tag: "geoip-ir",
type: "remote",
format: "binary",
url: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geoip/ir.srs",
download_detour: "direct"
},
{
tag: "geoip-cn",
type: "remote",
format: "binary",
url: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geoip/cn.srs",
download_detour: "direct"
},
{
tag: "geoip-vn",
type: "remote",
format: "binary",
url: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geoip/vn.srs",
download_detour: "direct"
}
],
}
},
computed: {
enableLog: {
get() :boolean { return this.subJsonExt?.log != undefined },
set(v:boolean) { v ? this.subJsonExt.log = this.defaultLog : delete this.subJsonExt.log }
},
enableDns: {
get() :boolean { return this.subJsonExt?.dns != undefined },
set(v:boolean) {
if (v) {
this.subJsonExt.dns = this.defaultDns
if (this.rules == undefined) this.subJsonExt.rules = []
this.subJsonExt.rules.unshift({ protocol: "dns", outbound: "dns-out" })
} else {
delete this.subJsonExt.dns
const ruleDnsIndex = this.subJsonExt?.rules?.findIndex((r:any) => r.protocol == "dns" && r.outbound == "dns-out")
if (ruleDnsIndex >= 0) this.subJsonExt.rules.splice(ruleDnsIndex,1)
if (this.rules.length == 0) delete this.subJsonExt.rules
}
}
},
enableInb: {
get() :boolean { return this.subJsonExt?.inbounds != undefined },
set(v:boolean) { v ? this.subJsonExt.inbounds = this.defaultInb.slice() : delete this.subJsonExt.inbounds }
},
enableExp: {
get() :boolean { return this.subJsonExt?.experimental != undefined },
set(v:boolean) { v ? this.subJsonExt.experimental = this.defaultExp : delete this.subJsonExt.experimental }
},
dns():any { return this.subJsonExt?.dns?? undefined },
proxyDns: {
get() :string { return this.dns?.servers[0]?.address?? "" },
set(v:string) { this.dns.servers[0].address = v.length>0 ? v : "8.8.8.8" }
},
directDns: {
get() :string { return this.dns?.servers?.findLast((d:any) => d.tag == "direct-dns")?.address?? "" },
set(v:string) {
const sIndex = this.dns.servers.findIndex((d:any) => d.tag == "direct-dns")
if (v?.length>0) {
if (sIndex === -1) {
this.dns.servers.push({ tag: "direct-dns", address: v, detour: "direct" })
this.dns.rules.push({ clash_mode: "Direct", server: "direct-dns" })
} else {
this.dns.servers[sIndex].address = v
}
} else {
this.dns.servers.splice(sIndex,1)
this.dns.rules = this.dns.rules.filter((r:any) => r.server != "direct-dns")
}
},
},
dnsToDirect: {
get() :string[] {
const ruleIndex = this.dns?.rules?.findIndex((r:any) => r.server == "direct-dns" && Object.hasOwn(r,'rule_set'))
return ruleIndex >= 0 ? this.dns.rules[ruleIndex].rule_set : []
},
set(v:string[]) {
const ruleIndex = this.dns?.rules?.findIndex((r:any) => r.server == "direct-dns" && Object.hasOwn(r,'rule_set'))
if (v.length>0) {
if (ruleIndex >= 0){
this.dns.rules[ruleIndex].rule_set = v
} else {
this.dns.rules.push({ rule_set: v, server: "direct-dns" })
}
} else {
if (ruleIndex != -1) this.dns.rules.splice(ruleIndex,1)
}
this.updateRuleSets()
}
},
inbounds():any[] { return this.subJsonExt?.inbounds?? undefined },
platformProxy: {
get() :boolean { return this.inbounds[0]?.platform != undefined },
set(v:boolean) { this.subJsonExt.inbounds[0].platform = v ? this.defaultInb[0].platform : undefined }
},
rules():any { return this.subJsonExt?.rules?? undefined },
ruleToDirect: {
get() :string[] {
const ruleIndex = this.rules?.findIndex((r:any) => r.outbound == "direct" && Object.hasOwn(r,'rule_set'))
return ruleIndex >= 0 ? this.rules[ruleIndex].rule_set : []
},
set(v:string[]) {
const ruleIndex = this.rules?.findIndex((r:any) => r.outbound == "direct" && Object.hasOwn(r,'rule_set'))
if (v.length>0) {
if (ruleIndex >= 0){
this.rules[ruleIndex].rule_set = v
} else {
if (this.rules == undefined) this.subJsonExt.rules = []
this.rules.push({ rule_set: v, outbound: "direct" })
}
} else {
if (ruleIndex != -1) this.rules.splice(ruleIndex,1)
}
this.updateRuleSets()
}
},
ruleToBlock: {
get() :string[] {
const ruleIndex = this.rules?.findIndex((r:any) => r.outbound == "block" && Object.hasOwn(r,'rule_set'))
return ruleIndex >= 0 ? this.rules[ruleIndex].rule_set : []
},
set(v:string[]) {
const ruleIndex = this.rules?.findIndex((r:any) => r.outbound == "block" && Object.hasOwn(r,'rule_set'))
if (v.length>0) {
if (ruleIndex >= 0){
this.rules[ruleIndex].rule_set = v
} else {
if (this.rules == undefined) this.subJsonExt.rules = []
this.rules.push({ rule_set: v, outbound: "block" })
}
} else {
if (ruleIndex != -1) this.rules.splice(ruleIndex,1)
}
this.updateRuleSets()
}
}
},
methods: {
updateRuleSets(){
let tags = <string[]>[]
if (this.dns?.rules?.length>0) this.dns.rules.forEach((r:any) => { if (r.rule_set) tags.push(...r.rule_set) })
if (this.rules?.length>0) this.rules.forEach((r:any) => { if (r.rule_set) tags.push(...r.rule_set) })
if (tags.length>0){
this.subJsonExt.rule_set = this.geo.filter((g:any) => tags.includes(g.tag))
} else {
delete this.subJsonExt.rule_set
}
if (this.rules.length == 0) delete this.subJsonExt.rules
}
},
mounted(){
this.subJsonExt = this.$props.settings?.subJsonExt?.length>0 ? JSON.parse(this.$props.settings.subJsonExt) : <any>{}
},
watch:{
subJsonExt:{
handler(v) {
this.$props.settings.subJsonExt = Object.keys(v).length>0 ? JSON.stringify(v, null, 2) : ""
},
deep: true
},
}
}
</script>
+1 -1
View File
@@ -16,7 +16,7 @@
<script lang="ts"> <script lang="ts">
export default { export default {
props: ['inbound', 'id'], props: ['inbound'],
data() { data() {
return { return {
hasUser: false, hasUser: false,
+21 -2
View File
@@ -4,7 +4,7 @@
<v-text-field <v-text-field
:label="$t('out.addr')" :label="$t('out.addr')"
hide-details hide-details
v-model="data.server"> v-model="address">
</v-text-field> </v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
@@ -13,7 +13,16 @@
type="number" type="number"
min="0" min="0"
hide-details hide-details
v-model="data.server_port"> v-model="port">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
label="KeepAlive"
type="number"
min="0"
hide-details
v-model="data.persistent_keepalive_interval">
</v-text-field> </v-text-field>
</v-col> </v-col>
</v-row> </v-row>
@@ -36,6 +45,8 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { KeepAlive } from 'vue';
export default { export default {
props: ['data'], props: ['data'],
data() { data() {
@@ -54,6 +65,14 @@ export default {
} }
} }
}, },
address: {
get() { return this.$props.data.address },
set(v:string) { this.$props.data.address = v.length > 0 ? v : undefined }
},
port: {
get() { return this.$props.data.port },
set(v:number) { this.$props.data.port = v > 0 ? v : undefined }
}
} }
} }
</script> </script>
+31 -11
View File
@@ -1,18 +1,38 @@
<template> <template>
<v-snackbar <Notivue v-slot="item">
v-model="sb.showMsg" <NotivueSwipe :item="item">
location="top" <Notification
:color="snackbar.color" :item="item"
:timeout="snackbar.timeout"> :theme="theme"
{{ snackbar.message }} :dir="direction"
</v-snackbar> :icons="outlinedIcons"
:hideClose="true"
@click="item.clear"
/>
</NotivueSwipe>
</Notivue>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue' import { Notivue, Notification, NotivueSwipe, outlinedIcons, pastelTheme, darkTheme } from 'notivue'
import Message from '@/store/modules/message' import { computed } from 'vue'
import { useTheme } from 'vuetify'
import vuetify from '@/plugins/vuetify';
const sb = Message() const Theme = useTheme()
const snackbar = ref(sb.snackbar) const theme = computed(() =>{
return Theme.global.name.value == "light" ? pastelTheme : darkTheme
})
const direction = computed(() => {
return vuetify.locale.isRtl ? 'rtl' : 'ltr'
})
</script> </script>
<style>
:root {
--nv-z: 10020;
}
</style>
+2 -12
View File
@@ -1,7 +1,7 @@
<template> <template>
<v-card subtitle="Direct"> <v-card subtitle="Direct">
<v-row> <v-row>
<v-col cols="12" sm="6" md="4" v-if="direction == 'in'"> <v-col cols="12" sm="6" md="4">
<Network :data="data" /> <Network :data="data" />
</v-col> </v-col>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
@@ -20,16 +20,6 @@
v-model.number="override_port"> v-model.number="override_port">
</v-text-field> </v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="6" md="4" v-if="direction == 'out'">
<v-select
:label="$t('types.direct.proxyProtocol')"
:items="[1,2]"
hide-details
clearable
@click:clear="delete data.proxy_protocol"
v-model.number="data.proxy_protocol">
</v-select>
</v-col>
</v-row> </v-row>
</v-card> </v-card>
</template> </template>
@@ -38,7 +28,7 @@
import Network from '@/components/Network.vue' import Network from '@/components/Network.vue'
export default { export default {
props: ['direction','data'], props: ['data'],
data() { data() {
return {} return {}
}, },
@@ -87,7 +87,7 @@
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start"> <v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details>{{ $t('types.hy.hyOptions') }}</v-btn> <v-btn v-bind="props" hide-details variant="tonal">{{ $t('types.hy.hyOptions') }}</v-btn>
</template> </template>
<v-card> <v-card>
<v-list> <v-list>
+144 -40
View File
@@ -1,29 +1,5 @@
<template> <template>
<v-card subtitle="Hysteria2"> <v-card subtitle="Hysteria2">
<v-row v-if="direction == 'in'">
<v-col cols="12" sm="6" md="4">
<v-text-field
label="HTTP3 server on auth fail"
hide-details
v-model="data.masquerade">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch v-model="data.ignore_client_bandwidth" color="primary" :label="$t('types.hy.ignoreBw')" hide-details></v-switch>
</v-col>
</v-row>
<v-row v-else>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('types.pw')"
hide-details
v-model="data.password">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<Network :data="data" />
</v-col>
</v-row>
<v-row v-if="!data.ignore_client_bandwidth"> <v-row v-if="!data.ignore_client_bandwidth">
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-text-field <v-text-field
@@ -46,26 +22,124 @@
</v-text-field> </v-text-field>
</v-col> </v-col>
</v-row> </v-row>
<v-row v-if="data.obfs"> <template v-if="direction == 'in'">
<v-col cols="12" sm="6" md="4"> <v-row>
<v-text-field <v-col cols="12" sm="6" md="4">
:label="$t('types.hy.obfs')" <v-switch v-model="data.ignore_client_bandwidth" color="primary" :label="$t('types.hy.ignoreBw')" hide-details></v-switch>
hide-details </v-col>
v-model="data.obfs.password"> <v-col cols="12" sm="6" md="4" v-if="data.obfs != undefined">
</v-text-field> <v-text-field
</v-col> :label="$t('types.hy.obfs')"
</v-row> hide-details
v-model="data.obfs.password">
</v-text-field>
</v-col>
</v-row>
<v-card subtitle="Hysteria2 Masquerade" v-if="data.masquerade != undefined">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-select v-model="masqueradeType" hide-details :label="$t('type')" :items="masqTypes"></v-select>
</v-col>
<v-col cols="12" sm="8" v-if="masqueradeType == ''">
<v-text-field
label="HTTP3 server on auth fails"
placeholder="file:///var/www | http://127.0.0.1:8080"
v-model="data.masquerade"
hide-details>
</v-text-field>
</v-col>
<v-col cols="12" sm="8" v-if="masqueradeType == 'file'">
<v-text-field
label="File server root directory"
placeholder="/var/www"
v-model="data.masquerade.directory"
hide-details>
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="masqueradeType == 'string'">
<v-text-field
label="HTTP Code"
type="number"
min="100"
max="599"
v-model.number="data.masquerade.status_code"
hide-details>
</v-text-field>
</v-col>
</v-row>
<v-row v-if="masqueradeType == 'proxy'">
<v-col cols="12" sm="6">
<v-text-field
label="Target URL"
placeholder="http://example.com:8080"
v-model="data.masquerade.url"
hide-details>
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
label="Rewrite Host"
placeholder="example.com"
v-model="data.masquerade.rewrite_host"
hide-details>
</v-text-field>
</v-col>
</v-row>
<template v-if="masqueradeType == 'string'">
<v-row>
<v-col cols="12" sm="8">
<v-text-field
label="Content"
v-model="data.masquerade.content"
hide-details>
</v-text-field>
</v-col>
</v-row>
<Headers :data="data.masquerade" />
</template>
</v-card>
</template>
<template v-else>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('types.pw')"
hide-details
v-model="data.password">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<Network :data="data" />
</v-col>
<v-col cols="12" sm="8" v-if="optionMPort">
<v-text-field
:label="$t('rule.portRange') + ' ' + $t('commaSeparated')"
v-model="server_ports">
</v-text-field>
</v-col>
</v-row>
</template>
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start"> <v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details>{{ $t('types.hy.hy2Options') }}</v-btn> <v-btn v-bind="props" hide-details variant="tonal">{{ $t('types.hy.hy2Options') }}</v-btn>
</template> </template>
<v-card> <v-card>
<v-list> <v-list>
<v-list-item> <template v-if="direction == 'in'">
<v-switch v-model="optionObfs" color="primary" :label="$t('types.hy.obfs')" hide-details></v-switch> <v-list-item>
</v-list-item> <v-switch v-model="optionObfs" color="primary" :label="$t('types.hy.obfs')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionMasq" color="primary" label="Masquerade" hide-details></v-switch>
</v-list-item>
</template>
<template v-else>
<v-list-item>
<v-switch v-model="optionMPort" color="primary" :label="$t('rule.portRange')" hide-details></v-switch>
</v-list-item>
</template>
</v-list> </v-list>
</v-card> </v-card>
</v-menu> </v-menu>
@@ -75,28 +149,58 @@
<script lang="ts"> <script lang="ts">
import Network from '@/components/Network.vue' import Network from '@/components/Network.vue'
import Headers from '@/components/Headers.vue'
import { i18n } from '@/locales'
export default { export default {
props: ['direction', 'data'], props: ['direction', 'data'],
data() { data() {
return { return {
menu: false, menu: false,
masqTypes: [
{ title: i18n.global.t('rule.simple'), value: '' },
{ title: "File server", value: "file" },
{ title: "Reverse Proxy", value: "proxy" },
{ title: "Fixed response", value: "string" },
]
} }
}, },
computed: { computed: {
down_mbps: { down_mbps: {
get() { return this.$props.data.down_mbps?? 0 }, get() { return this.$props.data.down_mbps?? 0 },
set(newValue:number) { this.$props.data.down_mbps = newValue>0 ? newValue : undefined } set(v:number) { this.$props.data.down_mbps = v>0 ? v : undefined }
}, },
up_mbps: { up_mbps: {
get() { return this.$props.data.up_mbps?? 0 }, get() { return this.$props.data.up_mbps?? 0 },
set(newValue:number) { this.$props.data.up_mbps = newValue>0 ? newValue : undefined } set(v:number) { this.$props.data.up_mbps = v>0 ? v : undefined }
},
server_ports: {
get() { return this.$props.data.server_ports?.join(',')?? [] },
set(v:string) { this.$props.data.server_ports = v.length > 0 ? v.split(',') : undefined }
},
masqueradeType: {
get() { return typeof this.$props.data.masquerade === 'object' ? this.$props.data.masquerade.type?? '' : '' },
set(v:string) {
if (v == '') {
this.$props.data.masquerade = ''
} else {
this.$props.data.masquerade = { type: v }
}
}
}, },
optionObfs: { optionObfs: {
get(): boolean { return this.$props.data.obfs != undefined }, get(): boolean { return this.$props.data.obfs != undefined },
set(v:boolean) { this.$props.data.obfs = v ? { type: "salamander", password: "" } : undefined } set(v:boolean) { this.$props.data.obfs = v ? { type: "salamander", password: "" } : undefined }
},
optionMasq: {
get(): boolean { return this.$props.data.masquerade != undefined },
set(v:boolean) { this.$props.data.masquerade = v ? "" : undefined }
},
optionMPort: {
get(): boolean { return this.$props.data.server_ports != undefined },
set(v:boolean) { this.$props.data.server_ports = v ? [] : undefined }
} }
}, },
components: { Network } components: { Network, Headers }
} }
</script> </script>
@@ -31,7 +31,7 @@
type="number" type="number"
min="0" min="0"
hide-details hide-details
v-model="server_port"> v-model.number="server_port">
</v-text-field> </v-text-field>
</v-col> </v-col>
</v-row> </v-row>
@@ -39,7 +39,7 @@
<v-row v-if="Inbound.handshake_for_server_name != undefined"> <v-row v-if="Inbound.handshake_for_server_name != undefined">
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-text-field <v-text-field
:label="$t('types.shdwTls.adHS')" :label="$t('types.shdwTls.addHS')"
hide-details hide-details
append-icon="mdi-plus" append-icon="mdi-plus"
@click:append="addHandshakeServer()" @click:append="addHandshakeServer()"
@@ -78,7 +78,7 @@
type="number" type="number"
min="0" min="0"
hide-details hide-details
v-model="value.server_port"> v-model.number="value.server_port">
</v-text-field> </v-text-field>
</v-col> </v-col>
</v-row> </v-row>
@@ -114,22 +114,22 @@ export default {
set(newValue: any) { set(newValue: any) {
switch (newValue) { switch (newValue) {
case 1: case 1:
this.Inbound.password = undefined delete this.Inbound.password
this.Inbound.users = undefined delete this.Inbound.users
this.Inbound.handshake_for_server_name = undefined delete this.Inbound.handshake_for_server_name
break; break;
case 2: case 2:
if (!this.Inbound.password) { if (!this.Inbound.password) {
this.Inbound.password = "" this.Inbound.password = ""
} }
this.Inbound.users = undefined delete this.Inbound.users
if (!this.Inbound.handshake_for_server_name) { if (!this.Inbound.handshake_for_server_name) {
this.Inbound.handshake_for_server_name = {} this.Inbound.handshake_for_server_name = {}
} }
break; break;
case 3: case 3:
this.Inbound.password = undefined delete this.Inbound.password
if (Object.hasOwn(this.Inbound, 'users')) { if (!Object.hasOwn(this.Inbound, 'users')) {
this.Inbound.users = [] this.Inbound.users = []
} }
if (!this.Inbound.handshake_for_server_name) { if (!this.Inbound.handshake_for_server_name) {
@@ -6,12 +6,10 @@
hide-details hide-details
:label="$t('in.ssMethod')" :label="$t('in.ssMethod')"
:items="ssMethods" :items="ssMethods"
@update:model-value="direction == 'in' ? changeMethod($event) : undefined"
v-model="data.method"> v-model="data.method">
</v-select> </v-select>
</v-col> </v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field v-model="data.password" :label="$t('types.pw')" hide-details></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<Network :data="data" /> <Network :data="data" />
</v-col> </v-col>
@@ -19,12 +17,24 @@
<UoT :data="data" /> <UoT :data="data" />
</v-col> </v-col>
</v-row> </v-row>
<v-row v-if="data.method.startsWith('2022') || direction == 'out'">
<v-col cols="12" sm="8">
<v-text-field
v-model="data.password"
:label="$t('types.pw')"
hide-details
:append-inner-icon="direction == 'in' ? 'mdi-refresh' : undefined"
@click:append-inner="changeMethod(data.method)">
</v-text-field>
</v-col>
</v-row>
</v-card> </v-card>
</template> </template>
<script lang="ts"> <script lang="ts">
import Network from '@/components/Network.vue' import Network from '@/components/Network.vue'
import UoT from '@/components/UoT.vue'; import UoT from '@/components/UoT.vue';
import RandomUtil from '@/plugins/randomUtil';
export default { export default {
props: ['direction','data'], props: ['direction','data'],
@@ -43,6 +53,15 @@ export default {
] ]
} }
}, },
methods: {
changeMethod(ssMethod :string) {
if (ssMethod.startsWith('2022')) {
this.$props.data.password = ssMethod == "2022-blake3-aes-128-gcm" ? RandomUtil.randomShadowsocksPassword(16) : RandomUtil.randomShadowsocksPassword(32)
} else {
this.$props.data.password = ''
}
}
},
components: { Network, UoT } components: { Network, UoT }
} }
</script> </script>
+1 -1
View File
@@ -77,7 +77,7 @@
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start"> <v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details>{{ $t('types.ssh.options') }}</v-btn> <v-btn v-bind="props" hide-details variant="tonal">{{ $t('types.ssh.options') }}</v-btn>
</template> </template>
<v-card> <v-card>
<v-list> <v-list>
+62
View File
@@ -0,0 +1,62 @@
<template>
<v-card subtitle="Tun">
<v-row>
<v-col cols="12" sm="8">
<v-text-field v-model="addrs" :label="$t('types.tun.addr') + ' ' + $t('commaSeparated')" hide-details></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-text-field v-model="data.interface_name" :label="$t('types.tun.ifName')" hide-details></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field type="number" v-model.number="data.mtu" label="MTU" hide-details></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-text-field
type="number"
v-model.number="udpTimeout"
label="UDP timeout"
min="1"
:suffix="$t('date.m')"
hide-details>
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-select
v-model="data.stack"
label="Stack"
:items="['system','gvisor','mixed']"
hide-details
></v-select>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch v-model="data.endpoint_independent_nat" color="primary" label="Independent NAT" hide-details></v-switch>
</v-col>
</v-row>
</v-card>
</template>
<script lang="ts">
export default {
props: ['data'],
data() {
return {
menu: false
}
},
computed: {
addrs: {
get() { return this.$props.data.address?.join(',') },
set(v:string) { this.$props.data.address = v.length > 0 ? v.split(',') : undefined }
},
udpTimeout: {
get() { return this.$props.data.udp_timeout ? parseInt(this.$props.data.udp_timeout.replace('m','')) : 5 },
set(v:number) { this.$props.data.udp_timeout = v > 0 ? v + 'm' : '5m' }
}
},
}
</script>
@@ -55,7 +55,7 @@
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start"> <v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details>{{ $t('types.lb.urlTestOptions') }}</v-btn> <v-btn v-bind="props" hide-details variant="tonal">{{ $t('types.lb.urlTestOptions') }}</v-btn>
</template> </template>
<v-card> <v-card>
<v-list> <v-list>
+53 -47
View File
@@ -2,22 +2,40 @@
<v-card subtitle="Wireguard"> <v-card subtitle="Wireguard">
<v-row> <v-row>
<v-col cols="12" sm="8"> <v-col cols="12" sm="8">
<v-text-field v-model="data.private_key" :label="$t('types.wg.privKey')" hide-details></v-text-field> <v-text-field
v-model="data.private_key"
:label="$t('types.wg.privKey')"
append-icon="mdi-key-star"
@click:append="newKey()"
hide-details>
</v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="8"> <v-col cols="12" sm="8">
<v-text-field v-model="data.peer_public_key" :label="$t('types.wg.pubKey')" hide-details></v-text-field> <v-text-field v-model="address" :label="$t('types.wg.localIp') + ' ' + $t('commaSeparated')" hide-details></v-text-field>
</v-col>
<v-col cols="12" sm="8" v-if="data.pre_shared_key != undefined">
<v-text-field v-model="data.pre_shared_key" :label="$t('types.wg.psk')" hide-details></v-text-field>
</v-col>
<v-col cols="12" sm="8">
<v-text-field v-model="local_ips" :label="$t('types.wg.localIp') + ' ' + $t('commaSeparated')" hide-details></v-text-field>
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12" sm="6" md="4" v-if="data.reserved != undefined"> <v-col cols="12" sm="6" md="4">
<v-text-field v-model="reserved" :label="'Reserved ' + $t('commaSeparated')" hide-details></v-text-field> <v-text-field
:label="$t('in.port')"
hide-details
type="number"
min=1
v-model.number="data.listen_port">
</v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="6" md="4" v-if="data.udp_timeout != undefined">
<v-text-field
label="UDP Timeout"
hide-details
type="number"
min=0
:suffix="$t('date.m')"
v-model.number="udp_timeout">
</v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4" v-if="data.workers != undefined"> <v-col cols="12" sm="6" md="4" v-if="data.workers != undefined">
<v-text-field <v-text-field
:label="$t('types.wg.worker')" :label="$t('types.wg.worker')"
@@ -39,37 +57,26 @@
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<Network :data="data" /> <v-switch v-model="data.system" color="primary" :label="$t('types.wg.sysIf')" hide-details></v-switch>
</v-col> </v-col>
<v-col cols="12" sm="6" md="4" v-if="data.interface_name != undefined"> <v-col cols="12" sm="6" md="4" v-if="data.name != undefined">
<v-text-field <v-text-field
:label="$t('types.wg.ifName')" :label="$t('types.wg.ifName')"
hide-details hide-details
v-model.number="data.interface_name"> v-model="data.name">
</v-text-field> </v-text-field>
</v-col> </v-col>
</v-row> </v-row>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-switch v-model="data.system_interface" color="primary" :label="$t('types.wg.sysIf')" hide-details></v-switch>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch v-model="data.gso" color="primary" :label="$t('types.wg.gso')" hide-details></v-switch>
</v-col>
</v-row>
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start"> <v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details>{{ $t('types.wg.options') }}</v-btn> <v-btn v-bind="props" hide-details variant="tonal">{{ $t('types.wg.options') }}</v-btn>
</template> </template>
<v-card> <v-card>
<v-list> <v-list>
<v-list-item> <v-list-item>
<v-switch v-model="optionPsk" color="primary" :label="$t('types.wg.psk')" hide-details></v-switch> <v-switch v-model="optionUdp" color="primary" label="UDP Timeout" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionRsrv" color="primary" label="Reserved" hide-details></v-switch>
</v-list-item> </v-list-item>
<v-list-item> <v-list-item>
<v-switch v-model="optionWorker" color="primary" :label="$t('types.wg.worker')" hide-details></v-switch> <v-switch v-model="optionWorker" color="primary" :label="$t('types.wg.worker')" hide-details></v-switch>
@@ -80,9 +87,6 @@
<v-list-item> <v-list-item>
<v-switch v-model="optionInterface" color="primary" :label="$t('types.wg.ifName')" hide-details></v-switch> <v-switch v-model="optionInterface" color="primary" :label="$t('types.wg.ifName')" hide-details></v-switch>
</v-list-item> </v-list-item>
<v-list-item>
<v-switch v-model="optionPeers" color="primary" :label="$t('types.wg.multiPeer')" hide-details></v-switch>
</v-list-item>
</v-list> </v-list>
</v-card> </v-card>
</v-menu> </v-menu>
@@ -95,7 +99,7 @@
<template v-for="(p, index) in data.peers"> <template v-for="(p, index) in data.peers">
<v-card style="margin-top: 1rem;"> <v-card style="margin-top: 1rem;">
<v-card-subtitle> <v-card-subtitle>
{{ $t('types.wg.peer') + ' ' + (index+1) }} <v-icon icon="mdi-delete" @click="data.peers.splice(index,1)" /> {{ $t('types.wg.peer') + ' ' + (index+1) }} <v-icon icon="mdi-delete" @click="data.peers.splice(index,1)" v-if="data.peers.length > 1" />
</v-card-subtitle> </v-card-subtitle>
<Peer :data="p" /> <Peer :data="p" />
</v-card> </v-card>
@@ -104,12 +108,11 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import Network from '@/components/Network.vue'
import Peer from '@/components/WgPeer.vue' import Peer from '@/components/WgPeer.vue'
import { WgPeer } from '@/types/outbounds'
export default { export default {
props: ['data'], props: ['data'],
emits: ["newWgKey"],
data() { data() {
return { return {
menu: false, menu: false,
@@ -117,13 +120,16 @@ export default {
}, },
methods: { methods: {
addPeer() { addPeer() {
this.$props.data.peers.push({server: '', port: ''}) this.$props.data.peers.push(this.$props.data.peers[0])
} },
newKey() {
this.$emit('newWgKey')
},
}, },
computed: { computed: {
optionPsk: { optionUdp: {
get(): boolean { return this.$props.data.pre_shared_key != undefined }, get(): boolean { return this.$props.data.udp_timeout != undefined },
set(v:boolean) { this.$props.data.pre_shared_key = v ? "" : undefined } set(v:boolean) { this.$props.data.udp_timeout = v ? "5m" : undefined }
}, },
optionRsrv: { optionRsrv: {
get(): boolean { return this.$props.data.reserved != undefined }, get(): boolean { return this.$props.data.reserved != undefined },
@@ -138,16 +144,12 @@ export default {
set(v:boolean) { this.$props.data.mtu = v ? 1408 : undefined } set(v:boolean) { this.$props.data.mtu = v ? 1408 : undefined }
}, },
optionInterface: { optionInterface: {
get(): boolean { return this.$props.data.interface_name != undefined }, get(): boolean { return this.$props.data.name != undefined },
set(v:boolean) { this.$props.data.interface_name = v ? "" : undefined } set(v:boolean) { this.$props.data.name = v ? "" : undefined }
}, },
optionPeers: { address: {
get(): boolean { return this.$props.data.peers != undefined }, get() { return this.$props.data.address?.join(',') },
set(v:boolean) { this.$props.data.peers = v ? <WgPeer[]>[] : undefined } set(v:string) { this.$props.data.address = v.length > 0 ? v.split(',') : undefined }
},
local_ips: {
get() { return this.$props.data.local_address?.join(',') },
set(v:string) { this.$props.data.local_address = v.length > 0 ? v.split(',') : undefined }
}, },
reserved: { reserved: {
get() { return this.$props.data.reserved?.join(',') }, get() { return this.$props.data.reserved?.join(',') },
@@ -157,7 +159,11 @@ export default {
} }
} }
}, },
udp_timeout: {
get() { return this.$props.data.udp_timeout ? parseInt(this.$props.data.udp_timeout.replace('m','')) : 5 },
set(v:number) { this.$props.data.udp_timeout = v > 0 ? v + 'm' : '5m' }
}
}, },
components: { Network, Peer } components: { Peer }
} }
</script> </script>
+1 -2
View File
@@ -49,8 +49,7 @@ const gaugeColor = computed(() => {
background: `rgb(var(--v-theme-${gaugeColor}))` background: `rgb(var(--v-theme-${gaugeColor}))`
}"> }">
</div> </div>
<span class="gauge__cover" dir="ltr" v-html="data.text"> <div class="gauge__cover"><span dir="ltr" v-html="data.text"></span></div>
</span>
</div> </div>
</div> </div>
</template> </template>
+252
View File
@@ -0,0 +1,252 @@
<template>
<v-card subtitle="ACME" style="background-color: inherit;">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" :label="$t('enable')" v-model="enabled" hide-details></v-switch>
</v-col>
<v-col cols="12" md="8" v-if="enabled">
<v-text-field
:label="$t('rule.domain') + ' ' + $t('commaSeparated')"
hide-details
v-model="domains">
</v-text-field>
</v-col>
</v-row>
<template v-if="enabled">
<v-row>
<v-col cols="12" sm="6" md="4" v-if="optionDir">
<v-text-field
:label="$t('tls.acme.dataDir')"
hide-details
v-model="acme.data_directory">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="optionDefault">
<v-combobox
v-model="acme.default_server_name"
:items="acme.domain"
:label="$t('tls.acme.defaultDomain')"
hide-details
></v-combobox>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="optionEmail">
<v-text-field
:label="$t('email')"
hide-details
v-model="acme.email">
</v-text-field>
</v-col>
</v-row>
<v-row v-if="optionChallenge">
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" :label="$t('tls.acme.httpChallenge')" v-model="acme.disable_http_challenge" hide-details></v-switch>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" :label="$t('tls.acme.tlsChallenge')" v-model="acme.disable_tls_alpn_challenge" hide-details></v-switch>
</v-col>
</v-row>
<v-row v-if="optionPorts">
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('tls.acme.altHport')"
hide-details
type="number"
min=1
max="65532"
v-model.number="acme.alternative_http_port">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('tls.acme.altTport')"
hide-details
type="number"
min=1
max="65532"
v-model.number="acme.alternative_tls_port">
</v-text-field>
</v-col>
</v-row>
<v-row v-if="optionProvider">
<v-col cols="12" sm="6" md="4">
<v-select
v-model="caProvider"
:items="providerList"
:label="$t('tls.acme.caProvider')"
hide-details
></v-select>
</v-col>
<v-col cols="12" md="8" v-if="caProvider == ''">
<v-text-field
:label="$t('tls.acme.customCa')"
hide-details
v-model="acme.provider">
</v-text-field>
</v-col>
</v-row>
<v-row v-if="acme.external_account != undefined">
<v-col cols="12" sm="6" md="4">
<v-text-field
label="Key ID"
hide-details
v-model="acme.external_account.key_id">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
label="MAC Key"
hide-details
v-model="acme.external_account.mac_key">
</v-text-field>
</v-col>
</v-row>
<v-row v-if="acme.dns01_challenge != undefined">
<v-col cols="12" sm="6" md="4">
<v-select
:label="$t('tls.acme.dns01Provider')"
hide-details
:items="dnsProviders.map(d => d.provider)"
@update:model-value="acme.dns01_challenge = { provider: $event }"
v-model="acme.dns01_challenge.provider">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4"
v-for="item in dnsProviders.filter(d => d.provider == acme.dns01_challenge?.provider)[0]?.params"
:key="item">
<v-text-field
:label="item"
hide-details
v-model="acme.dns01_challenge[item]">
</v-text-field>
</v-col>
</v-row>
<v-card-actions>
<v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('tls.acme.options') }}</v-btn>
</template>
<v-card>
<v-list>
<v-list-item>
<v-switch v-model="optionDir" color="primary" :label="$t('tls.acme.dataDir')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionDefault" color="primary" :label="$t('tls.acme.defaultDomain')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionEmail" color="primary" :label="$t('email')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionChallenge" color="primary" :label="$t('tls.acme.disableChallenges')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionPorts" color="primary" :label="$t('tls.acme.altPorts')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionProvider" color="primary" :label="$t('tls.acme.caProvider')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionExt" color="primary" :label="$t('tls.acme.extAcc')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionDns01" color="primary" :label="$t('tls.acme.dns01')" hide-details></v-switch>
</v-list-item>
</v-list>
</v-card>
</v-menu>
</v-card-actions>
</template>
</v-card>
</template>
<script lang="ts">
import { acme } from '@/types/tls'
export default {
props: ['tls'],
data() {
return {
menu: false,
providerList: [
{ title: "Let's Encrypt", value: "letsencrypt" },
{ title: "ZeroSSL", value: "zerossl" },
{ title: "Custom", value: "" }
],
dnsProviders: [
{ provider: "cloudflare", params: [ "api_token" ] },
{ provider: "alidns", params: [ "access_key_id","access_key_secret","region_id" ] }
]
}
},
computed: {
acme() {
return <acme>this.$props.tls.acme
},
enabled: {
get() { return this.acme != undefined },
set(v: boolean) { this.$props.tls.acme = v ? { domain: [] } : undefined }
},
domains: {
get() { return this.acme?.domain ? this.acme.domain.join(',') : "" },
set(v: string) {
if(!v.endsWith(',')) {
this.acme.domain = v.length > 0 ? v.split(',') : []
}
}
},
caProvider: {
get() { return this.acme?.provider && ['letsencrypt','zerossl'].includes(this.acme.provider) ? this.acme?.provider : '' },
set(v: string) { this.acme.provider = ['letsencrypt','zerossl'].includes(v) ? v : 'https://' }
},
optionDir: {
get(): boolean { return this.acme?.data_directory != undefined },
set(v:boolean) { this.acme.data_directory = v ? '' : undefined }
},
optionDefault: {
get(): boolean { return this.acme?.default_server_name != undefined },
set(v:boolean) { this.acme.default_server_name = v ? this.domains.length>0 ? this.domains[0] : '' : undefined }
},
optionEmail: {
get(): boolean { return this.acme?.email != undefined },
set(v:boolean) { this.acme.email = v ? '' : undefined }
},
optionChallenge: {
get(): boolean { return this.acme?.disable_http_challenge != undefined || this.acme?.disable_tls_alpn_challenge != undefined },
set(v:boolean) {
if (v) {
this.acme.disable_http_challenge = false
this.acme.disable_tls_alpn_challenge = false
} else {
delete this.acme.disable_http_challenge
delete this.acme.disable_tls_alpn_challenge
}
}
},
optionPorts: {
get(): boolean { return this.acme?.alternative_http_port != undefined || this.acme?.alternative_tls_port != undefined },
set(v:boolean) {
if (v) {
this.acme.alternative_http_port = 80
this.acme.alternative_tls_port = 443
} else {
delete this.acme.alternative_http_port
delete this.acme.alternative_tls_port
}
}
},
optionProvider: {
get(): boolean { return this.acme?.provider != undefined },
set(v:boolean) { this.acme.provider = v ? 'letsencrypt' : undefined }
},
optionExt: {
get(): boolean { return this.acme?.external_account != undefined },
set(v:boolean) { this.acme.external_account = v ? { key_id: '', mac_key: '' } : undefined }
},
optionDns01: {
get(): boolean { return this.acme?.dns01_challenge != undefined },
set(v:boolean) { this.acme.dns01_challenge = v ? { provider: 'cloudflare' } : undefined }
},
}
}
</script>
+163
View File
@@ -0,0 +1,163 @@
<template>
<v-card subtitle="ECH" style="background-color: inherit;">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" :label="$t('enable')" v-model="enabled" hide-details></v-switch>
</v-col>
</v-row>
<template v-if="enabled">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" label="Post-Quantum Schemes" v-model="ech.pq_signature_schemes_enabled" hide-details></v-switch>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" label="Disable Adaptive Size" v-model="ech.dynamic_record_sizing_disabled" hide-details></v-switch>
</v-col>
</v-row>
<v-row>
<v-col cols="auto">
<v-btn-toggle v-model="useEchPath"
class="rounded-xl"
density="compact"
variant="outlined"
shaped
mandatory>
<v-btn
@click="delete ech.key"
>{{ $t('tls.usePath') }}</v-btn>
<v-btn
@click="delete ech.key_path"
>{{ $t('tls.useText') }}</v-btn>
</v-btn-toggle>
</v-col>
<v-spacer></v-spacer>
<v-col cols="auto">
<v-btn
variant="tonal"
density="compact"
icon="mdi-key-star"
@click="genECH"
:loading="loading">
<v-icon />
<v-tooltip activator="parent" location="top">
{{ $t('actions.generate') }}
</v-tooltip>
</v-btn>
</v-col>
</v-row>
<v-row v-if="useEchPath == 0">
<v-col cols="12">
<v-text-field
:label="$t('tls.keyPath')"
hide-details
v-model="ech.key_path">
</v-text-field>
</v-col>
</v-row>
<v-row v-else>
<v-col cols="12">
<v-textarea
:label="$t('tls.key')"
hide-details
v-model="echKeyText">
</v-textarea>
</v-col>
</v-row>
<v-row>
<v-col cols="12">
<v-textarea
:label="$t('tls.cert')"
hide-details
v-model="echConfigText">
</v-textarea>
</v-col>
</v-row>
</template>
</v-card>
</template>
<script lang="ts">
import { i18n } from '@/locales'
import HttpUtils from '@/plugins/httputil'
import { ech } from '@/types/tls'
import { push } from 'notivue'
export default {
props: ['iTls','oTls'],
data() {
return {
useEchPath: this.$props.iTls?.ech?.key? 1:0,
loading: false,
}
},
methods: {
async genECH(){
this.loading = true
const msg = await HttpUtils.get('api/keypairs', {
k: "ech",
o: this.iTls.server_name?? "''" + "," + this.iTls.ech.pq_signature_schemes_enabled?? false
})
this.loading = false
if (msg.success && this.iTls.ech && this.oTls.ech) {
this.iTls.ech.key_path=undefined
this.useEchPath = 1
if (msg.obj.length>0){
let config = <string[]>[]
let key = <string[]>[]
let isConfig = false
let isKey = false
msg.obj.forEach((line:string) => {
if (line === "-----BEGIN ECH CONFIGS-----") {
isConfig = true
isKey = false
config.push(line)
} else if (line === "-----END ECH CONFIGS-----") {
isConfig = false
config.push(line)
} else if (line === "-----BEGIN ECH KEYS-----") {
isKey = true
isConfig = false
key.push(line)
} else if (line === "-----END ECH KEYS-----") {
isKey = false
key.push(line)
} else if (isConfig) {
config.push(line)
} else if (isKey) {
key.push(line)
}
})
this.iTls.ech.key = key?? undefined
this.oTls.ech.config = config?? undefined
} else {
push.error({
message: i18n.global.t('error') + ": " + msg.obj
})
}
}
},
},
computed: {
ech() {
return <ech>this.$props.iTls.ech
},
enabled: {
get() { return this.ech?.enabled?? false },
set(v: boolean) {
this.$props.iTls.ech = v ? { enabled: true } : undefined
this.$props.oTls.ech = v ? {} : undefined
}
},
echKeyText: {
get(): string { return this.ech?.key ? this.ech.key.join('\n') : '' },
set(newValue:string) { this.ech.key = newValue.split('\n') }
},
echConfigText: {
get(): string { return this.oTls.ech?.config ? this.oTls.ech.config.join('\n') : '' },
set(newValue:string) { this.oTls.ech.config = newValue.split('\n') }
},
}
}
</script>
+26
View File
@@ -0,0 +1,26 @@
<template>
<v-card :subtitle="$t('objects.tls')">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
:label="$t('template')"
:items="tlsItems"
v-model="inbound.tls_id">
</v-select>
</v-col>
</v-row>
</v-card>
</template>
<script lang="ts">
import { i18n } from '@/locales'
export default {
props: ['inbound', 'tlsConfigs'],
computed: {
tlsItems(): any[] {
return [ { title: i18n.global.t('none'), value: 0 }, ...this.$props.tlsConfigs?.map((t:any) => { return { title: t.name, value: t.id } } )]
}
}
}
</script>
@@ -177,7 +177,7 @@
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start"> <v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details>{{ $t('tls.options') }}</v-btn> <v-btn v-bind="props" hide-details variant="tonal">{{ $t('tls.options') }}</v-btn>
</template> </template>
<v-card> <v-card>
<v-list> <v-list>
@@ -216,14 +216,14 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { oTls, defaultOutTls } from '@/types/outTls' import { oTls, defaultOutTls } from '@/types/tls'
export default { export default {
props: ['outbound'], props: ['outbound'],
data() { data() {
return { return {
menu: false, menu: false,
usePath: 0, usePath: this.$props.outbound?.tls?.certificate? 1:0,
useEchPath: 0, useEchPath: this.$props.outbound?.tls.ech?.config? 1:0,
defaults: defaultOutTls, defaults: defaultOutTls,
alpn: [ alpn: [
{ title: "H3", value: 'h3' }, { title: "H3", value: 'h3' },
@@ -252,11 +252,6 @@ export default {
], ],
fingerprints: [ fingerprints: [
{ title: "Chrome", value: "chrome" }, { title: "Chrome", value: "chrome" },
{ title: "Chrome PSK", value: "chrome_psk" },
{ title: "Chrome PSK Shuffle", value: "chrome_psk_shuffle" },
{ title: "Chrome Padding PSK Shuffle", value: "chrome_padding_psk_shuffle" },
{ title: "Chrome Post-Quantum", value: "chrome_pq" },
{ title: "Chrome Post-Quantum PSK", value: "chrome_pq_psk" },
{ title: "Firefox", value: "firefox" }, { title: "Firefox", value: "firefox" },
{ title: "Microsoft Edge", value: "edge" }, { title: "Microsoft Edge", value: "edge" },
{ title: "Apple Safari", value: "safari" }, { title: "Apple Safari", value: "safari" },
@@ -275,7 +270,7 @@ export default {
}, },
tlsEnable: { tlsEnable: {
get() { return Object.hasOwn(this.tls, 'enabled') ? this.tls.enabled : false }, get() { return Object.hasOwn(this.tls, 'enabled') ? this.tls.enabled : false },
set(newValue: boolean) { this.$props.outbound.tls = newValue ? { enabled: true } : {} } set(newValue: boolean) { this.$props.outbound.tls = newValue ? { enabled: true } : { enabled: false } }
}, },
disable_sni: { disable_sni: {
get() { return this.tls.disable_sni ?? false }, get() { return this.tls.disable_sni ?? false },
+4 -23
View File
@@ -2,44 +2,25 @@
<v-app-bar :elevation="5"> <v-app-bar :elevation="5">
<v-icon v-if="isMobile" icon="mdi-menu" @click="$emit('toggleDrawer')" /> <v-icon v-if="isMobile" icon="mdi-menu" @click="$emit('toggleDrawer')" />
<span v-else style="width: 24px"></span> <span v-else style="width: 24px"></span>
<v-app-bar-title :text="$t(<string>$router.currentRoute.value.name)" class="align-center text-center " /> <v-app-bar-title :text="$t(<string>route.name)" class="align-center text-center " />
<v-btn prepend-icon="mdi-content-save" v-if="stateChange" :text="$t('actions.save')" @click="saveChanges"></v-btn>
<v-icon icon="mdi-theme-light-dark" @click="toggleTheme()" style="margin: 0 10px;"></v-icon> <v-icon icon="mdi-theme-light-dark" @click="toggleTheme()" style="margin: 0 10px;"></v-icon>
</v-app-bar> </v-app-bar>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onMounted, ref,watch } from "vue" import { ref } from "vue"
import { useTheme } from "vuetify" import { useTheme } from "vuetify"
import { FindDiff } from "@/plugins/utils" import { useRoute } from "vue-router";
import Data from "@/store/modules/data"
defineProps(['isMobile']) defineProps(['isMobile'])
const route = useRoute();
const theme = useTheme() const theme = useTheme()
const darkMode = ref(localStorage.getItem('theme') == "dark") const darkMode = ref(localStorage.getItem('theme') == "dark")
const store = Data()
const toggleTheme = () => { const toggleTheme = () => {
darkMode.value = !darkMode.value darkMode.value = !darkMode.value
theme.global.name.value = darkMode.value ? "dark" : "light" theme.global.name.value = darkMode.value ? "dark" : "light"
localStorage.setItem('theme', theme.global.name.value) localStorage.setItem('theme', theme.global.name.value)
} }
const saveChanges = () => {
store.pushData()
}
const oldData = computed((): any => {
return {config: store.oldData.config, clients: store.oldData.clients}
})
const newData = computed((): any => {
return {config: store.config, clients: store.clients}
})
const stateChange = computed((): any => {
return !FindDiff.deepCompare(newData.value,oldData.value)
})
</script> </script>
+2
View File
@@ -53,7 +53,9 @@ const menu = [
{ title: 'pages.inbounds', icon: 'mdi-cloud-download', path: '/inbounds' }, { title: 'pages.inbounds', icon: 'mdi-cloud-download', path: '/inbounds' },
{ title: 'pages.clients', icon: 'mdi-account-multiple', path: '/clients' }, { title: 'pages.clients', icon: 'mdi-account-multiple', path: '/clients' },
{ title: 'pages.outbounds', icon: 'mdi-cloud-upload', path: '/outbounds' }, { title: 'pages.outbounds', icon: 'mdi-cloud-upload', path: '/outbounds' },
{ title: 'pages.endpoints', icon: 'mdi-cloud-tags', path: '/endpoints' },
{ title: 'pages.rules', icon: 'mdi-routes', path: '/rules' }, { title: 'pages.rules', icon: 'mdi-routes', path: '/rules' },
{ title: 'pages.tls', icon: 'mdi-certificate', path: '/tls' },
{ title: 'pages.basics', icon: 'mdi-application-cog', path: '/basics' }, { title: 'pages.basics', icon: 'mdi-application-cog', path: '/basics' },
{ title: 'pages.admins', icon: 'mdi-account-tie', path: '/admins' }, { title: 'pages.admins', icon: 'mdi-account-tie', path: '/admins' },
{ title: 'pages.settings', icon: 'mdi-cog', path: '/settings' }, { title: 'pages.settings', icon: 'mdi-cog', path: '/settings' },
+145
View File
@@ -0,0 +1,145 @@
<template>
<v-dialog transition="dialog-bottom-transition" width="90%" max-width="800" :loading="loading">
<v-card class="rounded-lg">
<v-card-title>
<v-row>
<v-col>{{ $t('admin.changes') }}</v-col>
<v-spacer></v-spacer>
<v-col cols="auto"><v-icon icon="mdi-close-box" @click="$emit('close')" /></v-col>
</v-row>
</v-card-title>
<v-divider></v-divider>
<v-card-text>
<v-row>
<v-col cols="12" sm="4" md="3">
<v-select
hide-details
:label="$t('admin.actor')"
:items="['', 'DepleteJob', ...admins]"
v-model="user"
@update:model-value="loadData">
</v-select>
</v-col>
<v-col cols="12" sm="4" md="3">
<v-select
hide-details
:label="$t('admin.key')"
:items="['', 'inbounds', 'outbounds', 'clients', 'route', 'tls', 'experimental']"
v-model="key"
@update:model-value="loadData">
</v-select>
</v-col>
<v-col cols="6" sm="4" md="3">
<v-select
hide-details
:label="$t('count')"
:items="[10,20,30,50,100]"
v-model.number="chngCount"
@update:model-value="loadData">
</v-select>
</v-col>
<v-col cols="auto" align="center" justify="center">
<v-btn
icon="mdi-refresh"
variant="tonal"
:loading="loading"
@click="loadData">
<v-icon />
</v-btn>
</v-col>
</v-row>
<v-data-table
:headers="changesHeaders"
:items="changes"
item-value="id"
density="compact"
show-expand
items-per-page="10"
>
<template v-slot:item.dateTime="{ value }">
<v-chip variant="text" dir="ltr" density="compact">
{{ dateFormatted(value) }}
</v-chip>
</template>
<template v-slot:item.action="{ value }">
<v-chip density="compact">
{{ $t('actions.' + value) }}
</v-chip>
</template>
<template v-slot:expanded-row="{ columns, item }">
<tr>
<td :colspan="columns.length">
<v-card dir="ltr" v-if="item.index>0">Index: {{ item.index }}</v-card>
<v-card style="background-color: background" dir="ltr"><pre>{{ item.obj }}</pre></v-card>
</td>
</tr>
</template>
</v-data-table>
</v-card-text>
</v-card>
</v-dialog>
</template>
<script lang="ts">
import { i18n } from '@/locales'
import HttpUtils from '@/plugins/httputil'
export default {
props: ['admins', 'actor', 'visible'],
data() {
return {
loading: false,
changes: <any[]>[],
user: '',
key: '',
chngCount: 10,
expanded: [],
changesHeaders: [
{ title: 'ID', key: 'id' },
{ title: i18n.global.t('admin.date') + '-' + i18n.global.t('admin.time'), key: 'dateTime' },
{ title: i18n.global.t('admin.actor'), key: 'Actor' },
{ title: i18n.global.t('admin.key'), key: 'key' },
{ title: i18n.global.t('admin.action'), key: 'action' },
],
}
},
methods: {
async loadData() {
this.loading = true
const data = await HttpUtils.get('api/changes',{ a: this.user, k: this.key, c: this.chngCount })
if (data.success) {
this.changes = data.obj?? []
this.loading = false
}
},
dateFormatted(dt: number): string {
const date = new Date(dt*1000)
return date.toLocaleString(this.locale)
},
},
computed: {
locale() {
const l = i18n.global.locale.value
switch (l) {
case "zhHans":
return "zh-cn"
case "zhHant":
return "zh-tw"
default:
return l
}
},
},
watch: {
visible(newValue) {
this.changes = []
this.user = this.$props.actor
this.key = ''
this.chngCount = 10
if (newValue) {
this.loadData()
}
},
},
}
</script>
+63 -37
View File
@@ -5,7 +5,7 @@
{{ $t('actions.' + title) + " " + $t('objects.client') }} {{ $t('actions.' + title) + " " + $t('objects.client') }}
</v-card-title> </v-card-title>
<v-divider></v-divider> <v-divider></v-divider>
<v-card-text style="padding: 0 16px;"> <v-card-text style="padding: 0 16px; overflow-y: scroll;">
<v-container style="padding: 0;"> <v-container style="padding: 0;">
<v-tabs <v-tabs
v-model="tab" v-model="tab"
@@ -21,6 +21,9 @@
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-switch color="primary" v-model="client.enable" :label="$t('enable')" hide-details></v-switch> <v-switch color="primary" v-model="client.enable" :label="$t('enable')" hide-details></v-switch>
</v-col> </v-col>
<v-col cols="12" sm="6" md="4">
<v-combobox v-model="client.group" :items="groups" :label="$t('client.group')" hide-details></v-combobox>
</v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
@@ -38,52 +41,74 @@
<DatePick :expiry="expDate" @submit="setDate" /> <DatePick :expiry="expDate" @submit="setDate" />
</v-col> </v-col>
</v-row> </v-row>
<v-row v-if="id > 0">
<v-col cols="12" sm="6" md="4" class="d-flex flex-column">
<div class="d-flex justify-space-between align-center">
<div>
{{ $t('stats.usage') }}: {{ total }}<sup dir="ltr" v-if="percent>0">({{ percent }}%)</sup>
</div>
<v-btn density="compact" variant="text" icon="mdi-restore" @click="client.up=0;client.down=0">
<v-tooltip activator="parent" location="top">
{{ $t('reset') }}
</v-tooltip>
<v-icon />
</v-btn>
</div>
<v-progress-linear
v-model="percent"
:color="percentColor"
v-if="client.volume>0"
bottom
>
</v-progress-linear>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-icon icon="mdi-upload" color="orange" /><span class="text-orange">{{ up }}</span>
/
<v-icon icon="mdi-download" color="success" /><span class="text-success">{{ down }}</span>
</v-col>
</v-row>
<v-row> <v-row>
<v-col> <v-col>
<v-combobox <v-select
v-model="clientInbounds" v-model="clientInbounds"
:items="inboundTags" :items="inboundTags"
:label="$t('client.inboundTags')" :label="$t('client.inboundTags')"
multiple multiple
chips chips
hide-details hide-details
></v-combobox> ></v-select>
</v-col>
</v-row>
<v-row>
<v-col cols="auto">
<v-switch v-model="clientStats" color="primary" :label="$t('stats.enable')" hide-details></v-switch>
</v-col> </v-col>
</v-row> </v-row>
</v-window-item> </v-window-item>
<v-window-item value="t2"> <v-window-item value="t2">
<v-row v-for="(value, key) in clientConfig" :key="key"> <v-row v-for="key in Object.keys(clientConfig)">
<v-col cols="12" md="3" align="end" align-self="center"> <v-col cols="12" md="3" align="end" align-self="center">
{{ key }} {{ key }}
</v-col> </v-col>
<v-col> <v-col>
<v-text-field <v-text-field
v-if="value.password != undefined" v-if="clientConfig[key].password != undefined"
label="Password" label="Password"
v-model="value.password" v-model="clientConfig[key].password"
hide-details> hide-details>
</v-text-field> </v-text-field>
<v-text-field <v-text-field
v-if="value.uuid != undefined" v-if="clientConfig[key].uuid != undefined"
label="UUID" label="UUID"
v-model="value.uuid" v-model="clientConfig[key].uuid"
hide-details> hide-details>
</v-text-field> </v-text-field>
<v-text-field <v-text-field
v-if="value.flow != undefined" v-if="key == 'vless'"
label="Flow" label="Flow"
v-model="value.flow" v-model="clientConfig[key].flow"
hide-details> hide-details>
</v-text-field> </v-text-field>
<v-text-field <v-text-field
v-if="value.auth_str != undefined" v-if="key == 'hysteria'"
label="Auth" label="Auth"
v-model="value.auth_str" v-model="clientConfig[key].auth_str"
hide-details> hide-details>
</v-text-field> </v-text-field>
</v-col> </v-col>
@@ -153,19 +178,18 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Link } from '@/plugins/link' import { createClient, randomConfigs, updateConfigs, Link } from '@/types/clients'
import { createClient, randomConfigs, updateConfigs } from '@/types/clients'
import DatePick from '@/components/DateTime.vue' import DatePick from '@/components/DateTime.vue'
import { HumanReadable } from '@/plugins/utils'
export default { export default {
props: ['visible', 'data', 'index', 'inboundTags', 'stats'], props: ['visible', 'data', 'id', 'inboundTags', 'groups'],
emits: ['close', 'save'], emits: ['close', 'save'],
data() { data() {
return { return {
client: createClient(), client: createClient(),
title: "add", title: "add",
loading: false, loading: false,
clientStats: false,
tab: "t1", tab: "t1",
clientConfig: <any>[], clientConfig: <any>[],
links: <Link[]>[], links: <Link[]>[],
@@ -175,22 +199,20 @@ export default {
}, },
methods: { methods: {
updateData() { updateData() {
if (this.$props.index != -1) { if (this.$props.id > 0) {
const newData = JSON.parse(this.$props.data) const newData = JSON.parse(this.$props.data)
this.client = createClient(newData) this.client = createClient(newData)
this.title = "edit" this.title = "edit"
this.clientConfig = JSON.parse(this.client.config) this.clientConfig = this.client.config
} }
else { else {
this.client = createClient() this.client = createClient()
this.title = "add" this.title = "add"
this.clientConfig = randomConfigs('client') this.clientConfig = randomConfigs('client')
} }
this.clientStats = this.$props.stats this.links = this.client.links.filter(l => l.type == 'local')
const allLinks = <Link[]>JSON.parse(this.client.links) this.extLinks = this.client.links.filter(l => l.type == 'external')
this.links = allLinks.filter(l => l.type == 'local') this.subLinks = this.client.links.filter(l => l.type == 'sub')
this.extLinks = allLinks.filter(l => l.type == 'external')
this.subLinks = allLinks.filter(l => l.type == 'sub')
this.tab = "t1" this.tab = "t1"
}, },
closeModal() { closeModal() {
@@ -199,12 +221,11 @@ export default {
}, },
saveChanges() { saveChanges() {
this.loading = true this.loading = true
this.client.config = updateConfigs(JSON.stringify(this.clientConfig), this.client.name) this.client.config = updateConfigs(this.clientConfig, this.client.name)
this.client.links = JSON.stringify([ this.client.links = [
...this.links, ...this.extLinks.filter(l => l.uri != ''),
...this.extLinks.filter(l => l.uri != ''), ...this.subLinks.filter(l => l.uri != '')]
...this.subLinks.filter(l => l.uri != '')]) this.$emit('save', this.client)
this.$emit('save', this.client, this.clientStats)
this.loading = false this.loading = false
}, },
setDate(newDate:number){ setDate(newDate:number){
@@ -213,8 +234,8 @@ export default {
}, },
computed: { computed: {
clientInbounds: { clientInbounds: {
get() { return this.client.inbounds == "" ? [] : this.client.inbounds.split(',').filter(i => this.inboundTags.includes(i)) }, get() { return this.client.inbounds.length>0 ? this.client.inbounds : [] },
set(newValue:string[]) { this.client.inbounds = newValue.length == 0 ? "" : newValue.join(',') } set(v:number[]) { this.client.inbounds = v.length == 0 ? [] : v }
}, },
expDate: { expDate: {
get() { return this.client.expiry}, get() { return this.client.expiry},
@@ -223,7 +244,12 @@ export default {
Volume: { Volume: {
get() { return this.client.volume == 0 ? 0 : (this.client.volume / (1024 ** 3)) }, get() { return this.client.volume == 0 ? 0 : (this.client.volume / (1024 ** 3)) },
set(v:number) { this.client.volume = v > 0 ? v*(1024 ** 3) : 0 } set(v:number) { this.client.volume = v > 0 ? v*(1024 ** 3) : 0 }
} },
up() :string { return HumanReadable.sizeFormat(this.client.up) },
down() :string { return HumanReadable.sizeFormat(this.client.down) },
total() :string { return HumanReadable.sizeFormat(this.client.down + this.client.up) },
percent() :number { return this.client.volume>0 ? Math.round((this.client.up + this.client.down) *100 / this.client.volume) : 0 },
percentColor() :string { return (this.client.up+this.client.down) >= this.client.volume ? 'error' : this.percent>90 ? 'warning' : 'success' },
}, },
watch: { watch: {
visible(newValue) { visible(newValue) {
+194
View File
@@ -0,0 +1,194 @@
<template>
<v-dialog transition="dialog-bottom-transition" width="800">
<v-card class="rounded-lg">
<v-card-title>
{{ $t('actions.addbulk') }}
</v-card-title>
<v-divider></v-divider>
<v-card-text style="padding: 0 16px; overflow-y: scroll;">
<v-container style="padding: 0;">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-text-field v-model.number="count" type="number" min="1" max="100" :label="$t('count')" hide-details></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="8">
<v-combobox
chips
multiple
v-model="bulkData.name"
:items="patterns"
:label="$t('client.name')"
hide-details>
</v-combobox>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="8">
<v-combobox
chips
multiple
v-model="bulkData.desc"
:items="patterns"
:label="$t('client.desc')"
hide-details>
</v-combobox>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-combobox v-model="bulkData.group" :items="groups" :label="$t('client.group')" hide-details></v-combobox>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field v-model.number="bulkData.Volume" type="number" min="0" :label="$t('stats.volume')" suffix="GiB" hide-details></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<DatePick :expiry="bulkData.expiry" @submit="setDate" />
</v-col>
</v-row>
<v-row>
<v-col>
<v-combobox
v-model="bulkData.clientInbounds"
:items="inboundTags"
:label="$t('client.inboundTags')"
:return-object="false"
multiple
chips
hide-details
></v-combobox>
</v-col>
</v-row>
</v-container>
<pre dir="ltr">{{ bulkData }}</pre>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="blue-darken-1"
variant="outlined"
@click="closeModal"
>
{{ $t('actions.close') }}
</v-btn>
<v-btn
color="blue-darken-1"
variant="tonal"
:loading="loading"
@click="saveChanges"
>
{{ $t('actions.save') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script lang="ts">
import DatePick from '@/components/DateTime.vue'
import { push } from 'notivue'
import RandomUtil from '@/plugins/randomUtil'
import { Client, createClient, randomConfigs } from '@/types/clients'
import { i18n } from '@/locales';
export default {
props: ['visible', 'inboundTags', 'groups'],
emits: ['close', 'save'],
data() {
return {
count: 1,
clients: <Client[]>[],
bulkData: {
name: <any[]>[],
desc: <any[]>[],
group: '',
clientInbounds: [],
expiry: 0,
Volume: 0,
},
patterns: [
{ title: i18n.global.t("bulk.random"), value: "random" },
{ title: i18n.global.t("bulk.order"), value: "order" },
],
loading: false,
}
},
methods: {
resetData() {
this.count = 1,
this.clients = [],
this.bulkData = {
name: [this.patterns[1], "-", this.patterns[0]],
desc: [],
group: '',
clientInbounds: [],
expiry: 0,
Volume: 0,
}
},
closeModal() {
this.$emit('close')
},
saveChanges() {
if (this.bulkData.name.findIndex(n => typeof(n) == 'object') == -1) {
push.error(i18n.global.t('error.dplData'))
return
}
this.clients = []
this.loading = true
for(let i=0;i<this.count;i++){
const name = this.genByPattern(this.bulkData.name, i)
this.clients.push(createClient({
enable: true,
name: name,
config: randomConfigs(name),
inbounds: this.bulkData.clientInbounds,
links: [],
volume: this.bulkData.Volume*(1024 ** 3),
expiry: this.bulkData.expiry,
up: 0,
down: 0,
desc: this.genByPattern(this.bulkData.desc, i),
group: this.bulkData.group
}))
}
this.$emit('save', this.clients)
this.loading = false
},
genByPattern(pattern: any[], order :number){
if (pattern.length == 0) return RandomUtil.randomSeq(8)
let result = ''
pattern.forEach(p => {
switch(typeof p){
case 'object':
switch(p.value){
case "random":
result += RandomUtil.randomSeq(8)
break
case "order":
result += order+1
}
break
default:
result += p
}
})
return result
},
setDate(v:number){
this.bulkData.expiry = v
}
},
computed: {},
watch: {
visible(newValue) {
if (newValue) {
this.resetData()
}
},
},
components: { DatePick },
}
</script>
+144
View File
@@ -0,0 +1,144 @@
<template>
<v-dialog transition="dialog-bottom-transition" width="800">
<v-card class="rounded-lg">
<v-card-title>
{{ $t('actions.' + title) + " " + $t('objects.endpoint') }}
</v-card-title>
<v-divider></v-divider>
<v-card-text style="padding: 0 16px; overflow-y: scroll;">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
:label="$t('type')"
:items="Object.keys(epTypes).map((key,index) => ({title: key, value: Object.values(epTypes)[index]}))"
v-model="endpoint.type"
@update:modelValue="changeType">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field v-model="endpoint.tag" :label="$t('objects.tag')" hide-details></v-text-field>
</v-col>
</v-row>
<Wireguard v-if="endpoint.type == epTypes.Wireguard" :data="endpoint" @newWgKey="newWgKey" />
<Dial :dial="endpoint" :outTags="tags" />
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="blue-darken-1"
variant="text"
@click="closeModal"
>
{{ $t('actions.close') }}
</v-btn>
<v-btn
color="blue-darken-1"
variant="text"
:loading="loading"
@click="saveChanges"
>
{{ $t('actions.save') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script lang="ts">
import { EpTypes, createEndpoint } from '@/types/endpoints'
import RandomUtil from '@/plugins/randomUtil'
import Dial from '@/components/Dial.vue'
import Wireguard from '@/components/protocols/Wireguard.vue'
import HttpUtils from '@/plugins/httputil'
import { push } from 'notivue'
import { i18n } from '@/locales'
export default {
props: ['visible', 'data', 'id', 'tags'],
emits: ['close', 'save'],
data() {
return {
endpoint: createEndpoint("wireguard",{ "tag": "" }),
title: "add",
tab: "t1",
link: "",
loading: false,
epTypes: EpTypes,
}
},
methods: {
async updateData() {
if (this.$props.id > 0) {
const newData = JSON.parse(this.$props.data)
this.endpoint = createEndpoint(newData.type, newData)
this.title = "edit"
}
else {
const port = RandomUtil.randomIntRange(10000, 60000)
const randomIPoctet = RandomUtil.randomIntRange(1, 255)
this.endpoint = createEndpoint("wireguard",{
tag: "wireguard-" + RandomUtil.randomSeq(3),
address: ['10.0.0.'+ randomIPoctet.toString() +'/32','fe80::'+ randomIPoctet.toString(16) +'/128'],
listen_port: port,
private_key: (await this.genWgKey()).private_key,
peers: [{
public_key: (await this.genWgKey()).public_key,
allowed_ips: ['0.0.0.0/0', '::/0']
}]
})
this.title = "add"
}
this.tab = "t1"
},
changeType() {
// Tag change only in add endpoint
const tag = this.$props.id > 0 ? this.endpoint.tag : this.endpoint.type + "-" + RandomUtil.randomSeq(3)
// Use previous data
const prevConfig = { id: this.endpoint.id, tag: tag ,listen: this.endpoint.listen, listen_port: this.endpoint.listen_port }
this.endpoint = createEndpoint(this.endpoint.type, prevConfig)
},
closeModal() {
this.updateData() // reset
this.$emit('close')
},
saveChanges() {
this.loading = true
this.$emit('save', this.endpoint)
this.loading = false
},
async genWgKey(){
this.loading = true
const msg = await HttpUtils.get('api/keypairs', { k: "wireguard" })
this.loading = false
let result = { private_key: "", public_key: "" }
if (msg.success) {
msg.obj.forEach((line:string) => {
if (line.startsWith("PrivateKey")){
result.private_key = line.substring(12)
}
if (line.startsWith("PublicKey")){
result.public_key = line.substring(11)
}
})
} else {
push.error({
message: i18n.global.t('error') + ": " + msg.obj
})
}
return result
},
async newWgKey(){
const newKeys = await this.genWgKey()
this.endpoint.private_key = newKeys.private_key
}
},
watch: {
visible(newValue) {
if (newValue) {
this.updateData()
}
},
},
components: { Dial, Wireguard }
}
</script>

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