Update On Sat Jan 4 19:31:20 CET 2025

This commit is contained in:
github-action[bot]
2025-01-04 19:31:21 +01:00
parent 8cf2bd5d97
commit 07c63fb97d
322 changed files with 32797 additions and 631 deletions

1
.github/update.log vendored
View File

@@ -872,3 +872,4 @@ Update On Tue Dec 31 19:31:50 CET 2024
Update On Wed Jan 1 19:33:17 CET 2025 Update On Wed Jan 1 19:33:17 CET 2025
Update On Thu Jan 2 19:33:11 CET 2025 Update On Thu Jan 2 19:33:11 CET 2025
Update On Fri Jan 3 19:34:50 CET 2025 Update On Fri Jan 3 19:34:50 CET 2025
Update On Sat Jan 4 19:31:12 CET 2025

View File

@@ -1586,7 +1586,7 @@ dependencies = [
"tracing-subscriber", "tracing-subscriber",
"url", "url",
"uuid", "uuid",
"webview2-com 0.33.0", "webview2-com",
"which 7.0.1", "which 7.0.1",
"whoami", "whoami",
"window-vibrancy", "window-vibrancy",
@@ -8436,7 +8436,7 @@ dependencies = [
"url", "url",
"urlpattern", "urlpattern",
"webkit2gtk", "webkit2gtk",
"webview2-com 0.34.0", "webview2-com",
"window-vibrancy", "window-vibrancy",
"windows 0.58.0", "windows 0.58.0",
] ]
@@ -8745,7 +8745,7 @@ dependencies = [
"tauri-utils", "tauri-utils",
"url", "url",
"webkit2gtk", "webkit2gtk",
"webview2-com 0.34.0", "webview2-com",
"windows 0.58.0", "windows 0.58.0",
"wry", "wry",
] ]
@@ -10225,20 +10225,6 @@ dependencies = [
"rustls-pki-types", "rustls-pki-types",
] ]
[[package]]
name = "webview2-com"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f61ff3d9d0ee4efcb461b14eb3acfda2702d10dc329f339303fc3e57215ae2c"
dependencies = [
"webview2-com-macros",
"webview2-com-sys 0.33.0",
"windows 0.58.0",
"windows-core 0.58.0",
"windows-implement 0.58.0",
"windows-interface 0.58.0",
]
[[package]] [[package]]
name = "webview2-com" name = "webview2-com"
version = "0.34.0" version = "0.34.0"
@@ -10246,7 +10232,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "823e7ebcfaea51e78f72c87fc3b65a1e602c321f407a0b36dbb327d7bb7cd921" checksum = "823e7ebcfaea51e78f72c87fc3b65a1e602c321f407a0b36dbb327d7bb7cd921"
dependencies = [ dependencies = [
"webview2-com-macros", "webview2-com-macros",
"webview2-com-sys 0.34.0", "webview2-com-sys",
"windows 0.58.0", "windows 0.58.0",
"windows-core 0.58.0", "windows-core 0.58.0",
"windows-implement 0.58.0", "windows-implement 0.58.0",
@@ -10264,17 +10250,6 @@ dependencies = [
"syn 2.0.94", "syn 2.0.94",
] ]
[[package]]
name = "webview2-com-sys"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3a3e2eeb58f82361c93f9777014668eb3d07e7d174ee4c819575a9208011886"
dependencies = [
"thiserror 1.0.69",
"windows 0.58.0",
"windows-core 0.58.0",
]
[[package]] [[package]]
name = "webview2-com-sys" name = "webview2-com-sys"
version = "0.34.0" version = "0.34.0"
@@ -11021,7 +10996,7 @@ dependencies = [
"url", "url",
"webkit2gtk", "webkit2gtk",
"webkit2gtk-sys", "webkit2gtk-sys",
"webview2-com 0.34.0", "webview2-com",
"windows 0.58.0", "windows 0.58.0",
"windows-core 0.58.0", "windows-core 0.58.0",
"windows-version", "windows-version",

View File

@@ -214,7 +214,7 @@ windows-sys = { version = "0.59", features = [
"Win32_System_SystemInformation", "Win32_System_SystemInformation",
] } ] }
windows-core = "0.58.0" windows-core = "0.58.0"
webview2-com = "0.33" webview2-com = "0.34"
[features] [features]
default = ["custom-protocol", "default-meta"] default = ["custom-protocol", "default-meta"]

View File

@@ -16,9 +16,9 @@
"@emotion/styled": "11.14.0", "@emotion/styled": "11.14.0",
"@juggle/resize-observer": "3.4.0", "@juggle/resize-observer": "3.4.0",
"@material/material-color-utilities": "0.3.0", "@material/material-color-utilities": "0.3.0",
"@mui/icons-material": "6.3.0", "@mui/icons-material": "6.3.1",
"@mui/lab": "6.0.0-beta.21", "@mui/lab": "6.0.0-beta.22",
"@mui/material": "6.3.0", "@mui/material": "6.3.1",
"@nyanpasu/interface": "workspace:^", "@nyanpasu/interface": "workspace:^",
"@nyanpasu/ui": "workspace:^", "@nyanpasu/ui": "workspace:^",
"@tanstack/router-zod-adapter": "1.81.5", "@tanstack/router-zod-adapter": "1.81.5",
@@ -54,7 +54,7 @@
"@emotion/react": "11.14.0", "@emotion/react": "11.14.0",
"@iconify/json": "2.2.291", "@iconify/json": "2.2.291",
"@monaco-editor/react": "4.6.0", "@monaco-editor/react": "4.6.0",
"@tanstack/react-query": "5.62.11", "@tanstack/react-query": "5.62.15",
"@tanstack/react-router": "1.89.2", "@tanstack/react-router": "1.89.2",
"@tanstack/router-devtools": "1.89.2", "@tanstack/router-devtools": "1.89.2",
"@tanstack/router-plugin": "1.87.13", "@tanstack/router-plugin": "1.87.13",
@@ -79,7 +79,7 @@
"meta-json-schema": "1.19.1", "meta-json-schema": "1.19.1",
"monaco-yaml": "5.2.3", "monaco-yaml": "5.2.3",
"nanoid": "5.0.9", "nanoid": "5.0.9",
"sass-embedded": "1.83.0", "sass-embedded": "1.83.1",
"shiki": "1.26.1", "shiki": "1.26.1",
"tailwindcss-textshadow": "2.1.3", "tailwindcss-textshadow": "2.1.3",
"unplugin-auto-import": "0.19.0", "unplugin-auto-import": "0.19.0",

View File

@@ -17,9 +17,9 @@
}, },
"dependencies": { "dependencies": {
"@material/material-color-utilities": "0.3.0", "@material/material-color-utilities": "0.3.0",
"@mui/icons-material": "6.3.0", "@mui/icons-material": "6.3.1",
"@mui/lab": "6.0.0-beta.21", "@mui/lab": "6.0.0-beta.22",
"@mui/material": "6.3.0", "@mui/material": "6.3.1",
"@radix-ui/react-portal": "1.1.3", "@radix-ui/react-portal": "1.1.3",
"@radix-ui/react-scroll-area": "1.2.2", "@radix-ui/react-scroll-area": "1.2.2",
"@tauri-apps/api": "2.2.0", "@tauri-apps/api": "2.2.0",
@@ -42,7 +42,7 @@
"@types/d3-interpolate-path": "2.0.3", "@types/d3-interpolate-path": "2.0.3",
"clsx": "2.1.1", "clsx": "2.1.1",
"d3-interpolate-path": "2.3.0", "d3-interpolate-path": "2.3.0",
"sass-embedded": "1.83.0", "sass-embedded": "1.83.1",
"tailwind-merge": "2.6.0", "tailwind-merge": "2.6.0",
"typescript-plugin-css-modules": "5.1.0", "typescript-plugin-css-modules": "5.1.0",
"vite-plugin-dts": "4.4.0" "vite-plugin-dts": "4.4.0"

View File

@@ -61,7 +61,7 @@
"@commitlint/config-conventional": "19.6.0", "@commitlint/config-conventional": "19.6.0",
"@eslint/compat": "1.2.4", "@eslint/compat": "1.2.4",
"@ianvs/prettier-plugin-sort-imports": "4.4.0", "@ianvs/prettier-plugin-sort-imports": "4.4.0",
"@tauri-apps/cli": "2.2.0", "@tauri-apps/cli": "2.2.2",
"@types/fs-extra": "11.0.4", "@types/fs-extra": "11.0.4",
"@types/lodash-es": "4.17.12", "@types/lodash-es": "4.17.12",
"@types/node": "22.10.5", "@types/node": "22.10.5",

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,14 @@
//go:build !unix && !windows
package tun
import "net"
func isIPv6Supported() bool {
lis, err := net.ListenPacket("udp6", "[::1]:0")
if err != nil {
return false
}
_ = lis.Close()
return true
}

View File

@@ -0,0 +1,16 @@
//go:build unix
package tun
import (
"golang.org/x/sys/unix"
)
func isIPv6Supported() bool {
sock, err := unix.Socket(unix.AF_INET6, unix.SOCK_DGRAM, unix.IPPROTO_UDP)
if err != nil {
return false
}
_ = unix.Close(sock)
return true
}

View File

@@ -0,0 +1,24 @@
//go:build windows
package tun
import (
"golang.org/x/sys/windows"
)
func isIPv6Supported() bool {
var wsaData windows.WSAData
err := windows.WSAStartup(uint32(0x202), &wsaData)
if err != nil {
// Failing silently: it is not our duty to report such errors
return true
}
defer windows.WSACleanup()
sock, err := windows.Socket(windows.AF_INET6, windows.SOCK_DGRAM, windows.IPPROTO_UDP)
if err != nil {
return false
}
_ = windows.Closesocket(sock)
return true
}

View File

@@ -49,6 +49,10 @@ type EventLogger interface {
} }
func (s *Server) Serve() error { func (s *Server) Serve() error {
if !isIPv6Supported() {
s.Logger.Warn("tun-pre-check", zap.String("msg", "IPv6 is not supported or enabled on this system, TUN device is created without IPv6 support."))
s.Inet6Address = nil
}
tunOpts := tun.Options{ tunOpts := tun.Options{
Name: s.IfName, Name: s.IfName,
Inet4Address: s.Inet4Address, Inet4Address: s.Inet4Address,

View File

@@ -1,4 +1,21 @@
--- /dev/null --- /dev/null
+++ b/arch/arm/dts/rk3399-fine-3399-u-boot.dtsi
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+#include "rk3399-u-boot.dtsi"
+#include "rk3399-sdram-lpddr4-100.dtsi"
+
+/ {
+ chosen {
+ u-boot,spl-boot-order = "same-as-spl", &sdhci, &sdmmc;
+ };
+};
+
+&vdd_log {
+ regulator-init-microvolt = <950000>;
+};
--- /dev/null
+++ b/arch/arm/dts/rk3399-fine-3399.dts +++ b/arch/arm/dts/rk3399-fine-3399.dts
@@ -0,0 +1,789 @@ @@ -0,0 +1,789 @@
+// SPDX-License-Identifier: (GPL-2.0+ OR MIT) +// SPDX-License-Identifier: (GPL-2.0+ OR MIT)

View File

@@ -1,22 +1,12 @@
FROM golang:1.20-alpine AS builder FROM golang:1.20-alpine AS builder
RUN apk update && apk add --no-cache git RUN apk update && apk add --no-cache git
RUN git clone https://github.com/enfein/mieru.git /build RUN git clone https://github.com/enfein/mieru.git /build
WORKDIR /build WORKDIR /build
RUN GOOS=linux CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o mita cmd/mita/mita.go
RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w" -o mita cmd/mita/mita.go
FROM alpine AS base FROM alpine AS base
COPY --from=builder /build/mita /usr/local/bin/ COPY --from=builder /build/mita /usr/local/bin/
WORKDIR /app WORKDIR /app
COPY . . COPY . .
RUN chmod +x ./start.sh && adduser -H -D -g "" mita && mkdir -p /etc/mita RUN chmod +x ./start.sh && adduser -H -D -g "" mita && mkdir -p /etc/mita
CMD ["./start.sh"] CMD ["./start.sh"]

View File

@@ -6,9 +6,7 @@ mita run &
sleep 2 sleep 2
mita apply config ./conf/config.json mita apply config ./conf/config.json
mita start mita start
mita describe config mita describe config
wait -n wait -n

View File

@@ -13,8 +13,6 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
//go:build !android
package protocol package protocol
import "time" import "time"

View File

@@ -1,25 +0,0 @@
// Copyright (C) 2024 mieru authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//go:build android
package protocol
import "time"
const (
// tickInterval is the event trigger interval.
tickInterval = 5 * time.Millisecond
)

View File

@@ -21,7 +21,6 @@ import (
"io" "io"
"math" "math"
"net" "net"
"runtime"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
@@ -579,7 +578,8 @@ func (s *Session) runOutputLoop(ctx context.Context) error {
func (s *Session) runOutputOnceStream() { func (s *Session) runOutputOnceStream() {
if s.outputHasErr.Load() { if s.outputHasErr.Load() {
runtime.Gosched() // Can't run output.
time.Sleep(tickInterval)
return return
} }
@@ -605,7 +605,8 @@ func (s *Session) runOutputOnceStream() {
func (s *Session) runOutputOncePacket() { func (s *Session) runOutputOncePacket() {
if s.outputHasErr.Load() { if s.outputHasErr.Load() {
runtime.Gosched() // Can't run output.
time.Sleep(tickInterval)
return return
} }

View File

@@ -46,9 +46,13 @@ endef
define Package/luci-theme-$(THEME_NAME)/postinst define Package/luci-theme-$(THEME_NAME)/postinst
#!/bin/sh #!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] || { if [ -z "$${IPKG_INSTROOT}" ]; then
( . /etc/uci-defaults/30-luci-theme-$(THEME_NAME) ) && rm -f /etc/uci-defaults/30-luci-theme-$(THEME_NAME) if [ -f /etc/uci-defaults/30-luci-theme-$(THEME_NAME) ]; then
} . /etc/uci-defaults/30-luci-theme-$(THEME_NAME)
rm -f /etc/uci-defaults/30-luci-theme-$(THEME_NAME)
fi
fi
exit 0
endef endef
$(eval $(call BuildPackage,luci-theme-$(THEME_NAME))) $(eval $(call BuildPackage,luci-theme-$(THEME_NAME)))

View File

@@ -71,6 +71,9 @@ DBAI (Device Berbasis Arm Indonesia)
this theme using bootstrap framework + vanilla css this theme using bootstrap framework + vanilla css
icons made by me + flaticons icons made by me + flaticons
### Attention
This theme required luci-theme-alpha-config installed
donate donate
buy me a padang rice or coffee buy me a padang rice or coffee
https://saweria.co/derisamedia https://saweria.co/derisamedia

View File

@@ -273,7 +273,7 @@ jobs:
echo "CONFIG_PACKAGE_luci-app-passwall=m" >> .config echo "CONFIG_PACKAGE_luci-app-passwall=m" >> .config
echo "CONFIG_PACKAGE_luci-app-passwall_Iptables_Transparent_Proxy=y" >> .config echo "CONFIG_PACKAGE_luci-app-passwall_Iptables_Transparent_Proxy=y" >> .config
echo "CONFIG_PACKAGE_luci-app-passwall_Nftables_Transparent_Proxy=y" >> .config echo "CONFIG_PACKAGE_luci-app-passwall_Nftables_Transparent_Proxy=y" >> .config
echo "CONFIG_PACKAGE_luci-app-passwall_INCLUDE_ChinaDNS_NG=y" >> .config echo "CONFIG_PACKAGE_luci-app-passwall_INCLUDE_Geoview=y" >> .config
echo "CONFIG_PACKAGE_luci-app-passwall_INCLUDE_Haproxy=y" >> .config echo "CONFIG_PACKAGE_luci-app-passwall_INCLUDE_Haproxy=y" >> .config
echo "CONFIG_PACKAGE_luci-app-passwall_INCLUDE_Hysteria=y" >> .config echo "CONFIG_PACKAGE_luci-app-passwall_INCLUDE_Hysteria=y" >> .config
echo "CONFIG_PACKAGE_luci-app-passwall_INCLUDE_NaiveProxy=y" >> .config echo "CONFIG_PACKAGE_luci-app-passwall_INCLUDE_NaiveProxy=y" >> .config
@@ -288,7 +288,6 @@ jobs:
echo "CONFIG_PACKAGE_luci-app-passwall_INCLUDE_Trojan_Plus=y" >> .config echo "CONFIG_PACKAGE_luci-app-passwall_INCLUDE_Trojan_Plus=y" >> .config
echo "CONFIG_PACKAGE_luci-app-passwall_INCLUDE_tuic_client=y" >> .config echo "CONFIG_PACKAGE_luci-app-passwall_INCLUDE_tuic_client=y" >> .config
echo "CONFIG_PACKAGE_luci-app-passwall_INCLUDE_V2ray_Geodata=y" >> .config echo "CONFIG_PACKAGE_luci-app-passwall_INCLUDE_V2ray_Geodata=y" >> .config
echo "CONFIG_PACKAGE_luci-app-passwall_INCLUDE_V2ray_Geoview=y" >> .config
echo "CONFIG_PACKAGE_luci-app-passwall_INCLUDE_V2ray_Plugin=y" >> .config echo "CONFIG_PACKAGE_luci-app-passwall_INCLUDE_V2ray_Plugin=y" >> .config
echo "CONFIG_PACKAGE_luci-app-passwall_INCLUDE_Xray=y" >> .config echo "CONFIG_PACKAGE_luci-app-passwall_INCLUDE_Xray=y" >> .config
echo "CONFIG_PACKAGE_luci-app-passwall_INCLUDE_Xray_Plugin=y" >> .config echo "CONFIG_PACKAGE_luci-app-passwall_INCLUDE_Xray_Plugin=y" >> .config

View File

@@ -12,6 +12,7 @@ PKG_RELEASE:=1
PKG_CONFIG_DEPENDS:= \ PKG_CONFIG_DEPENDS:= \
CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \ CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \
CONFIG_PACKAGE_$(PKG_NAME)_Nftables_Transparent_Proxy \ CONFIG_PACKAGE_$(PKG_NAME)_Nftables_Transparent_Proxy \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Geoview \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Haproxy \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Haproxy \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Hysteria \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Hysteria \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_NaiveProxy \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_NaiveProxy \
@@ -26,7 +27,6 @@ PKG_CONFIG_DEPENDS:= \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Trojan_Plus \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Trojan_Plus \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_tuic_client \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_tuic_client \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Geodata \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Geodata \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Geoview \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Plugin \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Plugin \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Xray \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Xray \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Xray_Plugin CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Xray_Plugin
@@ -64,6 +64,11 @@ config PACKAGE_$(PKG_NAME)_Nftables_Transparent_Proxy
select PACKAGE_kmod-nft-nat select PACKAGE_kmod-nft-nat
default y if PACKAGE_firewall4 default y if PACKAGE_firewall4
config PACKAGE_$(PKG_NAME)_INCLUDE_Geoview
bool "Include Geoview"
select PACKAGE_geoview
default y if aarch64||arm||i386||x86_64
config PACKAGE_$(PKG_NAME)_INCLUDE_Haproxy config PACKAGE_$(PKG_NAME)_INCLUDE_Haproxy
bool "Include Haproxy" bool "Include Haproxy"
select PACKAGE_haproxy select PACKAGE_haproxy
@@ -141,11 +146,6 @@ config PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Geodata
select PACKAGE_v2ray-geosite select PACKAGE_v2ray-geosite
default n default n
config PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Geoview
bool "Include V2ray_Geoview"
select PACKAGE_geoview
default y if aarch64||arm||i386||x86_64
config PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Plugin config PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Plugin
bool "Include V2ray-Plugin (Shadowsocks Plugin)" bool "Include V2ray-Plugin (Shadowsocks Plugin)"
select PACKAGE_v2ray-plugin select PACKAGE_v2ray-plugin

View File

@@ -290,6 +290,9 @@ o:depends({ _tcp_node_bool = "1" })
o:value("dnsmasq", "Dnsmasq") o:value("dnsmasq", "Dnsmasq")
o:value("chinadns-ng", translate("ChinaDNS-NG (recommended)")) o:value("chinadns-ng", translate("ChinaDNS-NG (recommended)"))
o = s:option(DummyValue, "view_chinadns_log", " ")
o.template = appname .. "/acl/view_chinadns_log"
o = s:option(Flag, "filter_proxy_ipv6", translate("Filter Proxy Host IPv6"), translate("Experimental feature.")) o = s:option(Flag, "filter_proxy_ipv6", translate("Filter Proxy Host IPv6"), translate("Experimental feature."))
o.default = "0" o.default = "0"
o:depends({ _tcp_node_bool = "1" }) o:depends({ _tcp_node_bool = "1" })
@@ -418,6 +421,4 @@ o:value("direct", translate("Direct DNS"))
o.description = desc .. "</ul>" o.description = desc .. "</ul>"
o:depends({dns_shunt = "dnsmasq", tcp_proxy_mode = "proxy", chn_list = "direct"}) o:depends({dns_shunt = "dnsmasq", tcp_proxy_mode = "proxy", chn_list = "direct"})
m:append(Template(appname .. "/acl/footer"))
return m return m

View File

@@ -5,12 +5,6 @@ local api = require "luci.passwall.api"
//<![CDATA[ //<![CDATA[
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
setTimeout(function () { setTimeout(function () {
var url = window.location.href;
var sid_match = url.match(/\/acl_config\/(cfg[0-9a-f]+)/);
var sid = sid_match ? sid_match[1] : null;
if (!sid) {
return;
}
var selects = document.querySelectorAll("select[id*='dns_shunt']"); var selects = document.querySelectorAll("select[id*='dns_shunt']");
selects.forEach(function (select) { selects.forEach(function (select) {
if (select.value === "chinadns-ng") { if (select.value === "chinadns-ng") {
@@ -32,7 +26,7 @@ local api = require "luci.passwall.api"
logLink.href = "#"; logLink.href = "#";
logLink.className = "log-link"; logLink.className = "log-link";
logLink.style.marginLeft = "10px"; logLink.style.marginLeft = "10px";
logLink.setAttribute("onclick", "window.open('" + '<%=api.url("get_chinadns_log")%>' + "?flag=" + sid + "', '_blank')"); logLink.setAttribute("onclick", "window.open('" + '<%=api.url("get_chinadns_log") .. "?flag=" .. section%>' + "', '_blank')");
select.insertAdjacentElement("afterend", logLink); select.insertAdjacentElement("afterend", logLink);
} }
}, 1000); }, 1000);

View File

@@ -34,13 +34,15 @@ $Version = $Version -replace '"'
$PackageReleasePath = "${PSScriptRoot}\release" $PackageReleasePath = "${PSScriptRoot}\release"
$PackageName = "shadowsocks-v${Version}.${TargetTriple}.zip" $PackageName = "shadowsocks-v${Version}.${TargetTriple}.zip"
$PackagePath = "${PackageReleasePath}\${PackageName}" $PackagePath = "${PackageReleasePath}\${PackageName}"
$ReleaseBuildPath = "${PSScriptRoot}\..\target\release"
Write-Host $Version Write-Host $Version
Write-Host $PackageReleasePath Write-Host $PackageReleasePath
Write-Host $PackageName Write-Host $PackageName
Write-Host $PackagePath Write-Host $PackagePath
Write-Host $ReleaseBuildPath
Push-Location "${PSScriptRoot}\..\target\release" Push-Location $ReleaseBuildPath
$ProgressPreference = "SilentlyContinue" $ProgressPreference = "SilentlyContinue"
New-Item "${PackageReleasePath}" -ItemType Directory -ErrorAction SilentlyContinue New-Item "${PackageReleasePath}" -ItemType Directory -ErrorAction SilentlyContinue
@@ -48,7 +50,7 @@ $CompressParam = @{
LiteralPath = "sslocal.exe", "ssserver.exe", "ssurl.exe", "ssmanager.exe", "ssservice.exe" LiteralPath = "sslocal.exe", "ssserver.exe", "ssurl.exe", "ssmanager.exe", "ssservice.exe"
DestinationPath = "${PackagePath}" DestinationPath = "${PackagePath}"
} }
if ([System.IO.File]::Exists("sswinservice.exe")) { if ([System.IO.File]::Exists("$ReleaseBuildPath\sswinservice.exe")) {
$CompressParam.LiteralPath += "sswinservice.exe" $CompressParam.LiteralPath += "sswinservice.exe"
} }
Compress-Archive @CompressParam Compress-Archive @CompressParam
@@ -60,3 +62,5 @@ $PackageHash = (Get-FileHash -Path "${PackagePath}" -Algorithm SHA256).Hash
"${PackageHash} ${PackageName}" | Out-File -FilePath "${PackageChecksumPath}" "${PackageHash} ${PackageName}" | Out-File -FilePath "${PackageChecksumPath}"
Write-Host "Created release packet checksum ${PackageChecksumPath}" Write-Host "Created release packet checksum ${PackageChecksumPath}"
Pop-Location

View File

@@ -174,9 +174,9 @@ return baseclass.extend({
], ],
rules_logical_payload_count: { rules_logical_payload_count: {
'AND': 2, 'AND': { req: 2, opt: undefined },
'OR': 2, 'OR': { req: 2, opt: undefined },
'NOT': 1, 'NOT': { req: 1, opt: 0 },
//'SUB-RULE': 0, //'SUB-RULE': 0,
}, },

View File

@@ -125,13 +125,20 @@ class RulesEntry {
return this.payload[n] || {}; return this.payload[n] || {};
} }
setPayload(n, obj) { getPayloads() {
return this.payload || [];
}
setPayload(n, obj, limit) {
this.payload[n] ||= {}; this.payload[n] ||= {};
Object.keys(obj).forEach((key) => { Object.keys(obj).forEach((key) => {
this.payload[n][key] = obj[key] || null; this.payload[n][key] = obj[key] || null;
}); });
if (limit)
this.payload.splice(limit);
return this return this
} }
@@ -152,7 +159,7 @@ class RulesEntry {
var logical = hm.rules_logical_type.map(e => e[0] || e).includes(this.type), var logical = hm.rules_logical_type.map(e => e[0] || e).includes(this.type),
factor = ''; factor = '';
if (logical) { if (logical) {
let n = hm.rules_logical_payload_count[this.type] || 0; let n = hm.rules_logical_payload_count[this.type] ? hm.rules_logical_payload_count[this.type].opt : 0;
factor = '(%s)'.format(this.payload.slice(0, n).map((payload) => { factor = '(%s)'.format(this.payload.slice(0, n).map((payload) => {
return '(%s%s)'.format(payload.type || '', payload.factor || ''); return '(%s%s)'.format(payload.type || '', payload.factor || '');
}).join('')); }).join(''));
@@ -215,8 +222,35 @@ function renderPayload(s, total, uciconfig) {
o.rmempty = false; o.rmempty = false;
o.modalonly = true; o.modalonly = true;
} }
var initDynamicPayload = function(o, n, key, uciconfig) {
o.load = L.bind(function(n, key, uciconfig, section_id) {
return new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getPayloads().slice(n).map(e => e[key]);
}, o, n, key, uciconfig);
o.validate = function(section_id, value) {
value = this.formvalue(section_id);
var UIEl = this.section.getUIElement(section_id, 'entry');
var rule = new RulesEntry(UIEl.getValue());
let n = this.option.match(/^payload(\d+)_/)[1];
let limit = rule.getPayloads().length;
value.forEach((val) => {
rule.setPayload(n, {factor: val}); n++;
});
rule.setPayload(limit, {factor: null}, limit);
var newvalue = rule.toString();
UIEl.node.previousSibling.innerText = newvalue;
UIEl.setValue(newvalue);
return true;
}
o.write = function() {};
o.rmempty = true;
o.modalonly = true;
}
var o, prefix; var o, prefix;
// StaticList payload
for (var n=0; n<total; n++) { for (var n=0; n<total; n++) {
prefix = `payload${n}_`; prefix = `payload${n}_`;
@@ -226,7 +260,7 @@ function renderPayload(s, total, uciconfig) {
o.value.apply(o, res); o.value.apply(o, res);
}) })
Object.keys(hm.rules_logical_payload_count).forEach((key) => { Object.keys(hm.rules_logical_payload_count).forEach((key) => {
if (n < hm.rules_logical_payload_count[key]) if (n < hm.rules_logical_payload_count[key].req)
o.depends('type', key); o.depends('type', key);
}) })
initPayload(o, n, 'type', uciconfig); initPayload(o, n, 'type', uciconfig);
@@ -297,6 +331,68 @@ function renderPayload(s, total, uciconfig) {
return new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getPayload(n)[key]; return new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getPayload(n)[key];
}, o, n, 'factor', uciconfig) }, o, n, 'factor', uciconfig)
} }
// DynamicList payload
var extenbox = {};
Object.entries(hm.rules_logical_payload_count).filter(e => e[1].opt === undefined).forEach((e) => {
let n = e[1].req;
if (!Array.isArray(extenbox[n]))
extenbox[n] = [];
extenbox[n].push(e[0]);
})
Object.keys(extenbox).forEach((n) => {
prefix = `payload${n}_`;
o = s.option(form.DynamicList, prefix + 'type', _('Type') + ' ++');
o.default = hm.rules_type[0][0];
hm.rules_type.forEach((res) => {
o.value.apply(o, res);
})
extenbox[n].forEach((type) => {
o.depends('type', type);
})
initDynamicPayload(o, n, 'type', uciconfig);
o.validate = function(section_id, value) {
value = this.formvalue(section_id);
var UIEl = this.section.getUIElement(section_id, 'entry');
var rule = new RulesEntry(UIEl.getValue());
let n = this.option.match(/^payload(\d+)_/)[1];
value.forEach((val) => {
rule.setPayload(n, {type: val}); n++;
});
rule.setPayload(n, {factor: null}, n);
var newvalue = rule.toString();
UIEl.node.previousSibling.innerText = newvalue;
UIEl.setValue(newvalue);
return true;
}
o = s.option(form.DynamicList, prefix + 'fused', _('Factor') + ' ++',
_('Content will not be verified, Please make sure you enter it correctly.'));
o.value('', _('-- Please choose --'));
extenbox[n].forEach((type) => {
o.depends(Object.fromEntries([['type', type], [prefix + 'type', /.+/]]));
})
initDynamicPayload(o, n, 'factor', uciconfig);
o.load = L.bind(function(n, key, uciconfig, section_id) {
let fusedval = [
['', _('-- Please choose --')],
['NETWORK', '-- NETWORK --'],
['udp', _('UDP')],
['tcp', _('TCP')],
['RULESET', '-- RULE-SET --']
];
hm.loadRulesetLabel.call(this, null, section_id);
this.keylist = [...fusedval.map(e => e[0]), ...this.keylist];
this.vallist = [...fusedval.map(e => e[1]), ...this.vallist];
this.super('load', section_id);
return new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getPayloads().slice(n).map(e => e[key]);
}, o, n, 'factor', uciconfig)
})
} }
function renderRules(s, uciconfig) { function renderRules(s, uciconfig) {
@@ -356,7 +452,7 @@ function renderRules(s, uciconfig) {
o.rmempty = false; o.rmempty = false;
o.modalonly = true; o.modalonly = true;
renderPayload(s, Math.max(...Object.values(hm.rules_logical_payload_count)), uciconfig); renderPayload(s, Math.max(...Object.values(hm.rules_logical_payload_count).map(e => e.req)), uciconfig);
o = s.option(form.ListValue, 'detour', _('Proxy group')); o = s.option(form.ListValue, 'detour', _('Proxy group'));
o.renderWidget = function(/* ... */) { o.renderWidget = function(/* ... */) {
@@ -1018,7 +1114,7 @@ return view.extend({
so.rmempty = false; so.rmempty = false;
so.editable = true; so.editable = true;
so = ss.option(form.ListValue, 'proxy', _('Proxy group'), so = ss.option(form.ListValue, 'proxy', _('Proxy group override'),
_('Override the Proxy group of DNS server.')); _('Override the Proxy group of DNS server.'));
so.renderWidget = function(/* ... */) { so.renderWidget = function(/* ... */) {
var frameEl = form.ListValue.prototype.renderWidget.apply(this, arguments); var frameEl = form.ListValue.prototype.renderWidget.apply(this, arguments);

View File

@@ -1667,6 +1667,10 @@ msgstr ""
msgid "Proxy group" msgid "Proxy group"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/fchomo/client.js:1117
msgid "Proxy group override"
msgstr ""
#: htdocs/luci-static/resources/view/fchomo/global.js:450 #: htdocs/luci-static/resources/view/fchomo/global.js:450
msgid "Proxy mode" msgid "Proxy mode"
msgstr "" msgstr ""

View File

@@ -1690,6 +1690,10 @@ msgstr "代理链"
msgid "Proxy group" msgid "Proxy group"
msgstr "代理组" msgstr "代理组"
#: htdocs/luci-static/resources/view/fchomo/client.js:1117
msgid "Proxy group override"
msgstr "代理组覆盖"
#: htdocs/luci-static/resources/view/fchomo/global.js:450 #: htdocs/luci-static/resources/view/fchomo/global.js:450
msgid "Proxy mode" msgid "Proxy mode"
msgstr "代理模式" msgstr "代理模式"

View File

@@ -1690,6 +1690,10 @@ msgstr "代理鏈"
msgid "Proxy group" msgid "Proxy group"
msgstr "代理組" msgstr "代理組"
#: htdocs/luci-static/resources/view/fchomo/client.js:1117
msgid "Proxy group override"
msgstr "代理組覆蓋"
#: htdocs/luci-static/resources/view/fchomo/global.js:450 #: htdocs/luci-static/resources/view/fchomo/global.js:450
msgid "Proxy mode" msgid "Proxy mode"
msgstr "代理模式" msgstr "代理模式"

View File

@@ -12,6 +12,7 @@ PKG_RELEASE:=1
PKG_CONFIG_DEPENDS:= \ PKG_CONFIG_DEPENDS:= \
CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \ CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \
CONFIG_PACKAGE_$(PKG_NAME)_Nftables_Transparent_Proxy \ CONFIG_PACKAGE_$(PKG_NAME)_Nftables_Transparent_Proxy \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Geoview \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Haproxy \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Haproxy \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Hysteria \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Hysteria \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_NaiveProxy \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_NaiveProxy \
@@ -26,7 +27,6 @@ PKG_CONFIG_DEPENDS:= \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Trojan_Plus \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Trojan_Plus \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_tuic_client \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_tuic_client \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Geodata \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Geodata \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Geoview \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Plugin \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Plugin \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Xray \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Xray \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Xray_Plugin CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Xray_Plugin
@@ -64,6 +64,11 @@ config PACKAGE_$(PKG_NAME)_Nftables_Transparent_Proxy
select PACKAGE_kmod-nft-nat select PACKAGE_kmod-nft-nat
default y if PACKAGE_firewall4 default y if PACKAGE_firewall4
config PACKAGE_$(PKG_NAME)_INCLUDE_Geoview
bool "Include Geoview"
select PACKAGE_geoview
default y if aarch64||arm||i386||x86_64
config PACKAGE_$(PKG_NAME)_INCLUDE_Haproxy config PACKAGE_$(PKG_NAME)_INCLUDE_Haproxy
bool "Include Haproxy" bool "Include Haproxy"
select PACKAGE_haproxy select PACKAGE_haproxy
@@ -141,11 +146,6 @@ config PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Geodata
select PACKAGE_v2ray-geosite select PACKAGE_v2ray-geosite
default n default n
config PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Geoview
bool "Include V2ray_Geoview"
select PACKAGE_geoview
default y if aarch64||arm||i386||x86_64
config PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Plugin config PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Plugin
bool "Include V2ray-Plugin (Shadowsocks Plugin)" bool "Include V2ray-Plugin (Shadowsocks Plugin)"
select PACKAGE_v2ray-plugin select PACKAGE_v2ray-plugin

View File

@@ -290,6 +290,9 @@ o:depends({ _tcp_node_bool = "1" })
o:value("dnsmasq", "Dnsmasq") o:value("dnsmasq", "Dnsmasq")
o:value("chinadns-ng", translate("ChinaDNS-NG (recommended)")) o:value("chinadns-ng", translate("ChinaDNS-NG (recommended)"))
o = s:option(DummyValue, "view_chinadns_log", " ")
o.template = appname .. "/acl/view_chinadns_log"
o = s:option(Flag, "filter_proxy_ipv6", translate("Filter Proxy Host IPv6"), translate("Experimental feature.")) o = s:option(Flag, "filter_proxy_ipv6", translate("Filter Proxy Host IPv6"), translate("Experimental feature."))
o.default = "0" o.default = "0"
o:depends({ _tcp_node_bool = "1" }) o:depends({ _tcp_node_bool = "1" })
@@ -418,6 +421,4 @@ o:value("direct", translate("Direct DNS"))
o.description = desc .. "</ul>" o.description = desc .. "</ul>"
o:depends({dns_shunt = "dnsmasq", tcp_proxy_mode = "proxy", chn_list = "direct"}) o:depends({dns_shunt = "dnsmasq", tcp_proxy_mode = "proxy", chn_list = "direct"})
m:append(Template(appname .. "/acl/footer"))
return m return m

View File

@@ -5,12 +5,6 @@ local api = require "luci.passwall.api"
//<![CDATA[ //<![CDATA[
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
setTimeout(function () { setTimeout(function () {
var url = window.location.href;
var sid_match = url.match(/\/acl_config\/(cfg[0-9a-f]+)/);
var sid = sid_match ? sid_match[1] : null;
if (!sid) {
return;
}
var selects = document.querySelectorAll("select[id*='dns_shunt']"); var selects = document.querySelectorAll("select[id*='dns_shunt']");
selects.forEach(function (select) { selects.forEach(function (select) {
if (select.value === "chinadns-ng") { if (select.value === "chinadns-ng") {
@@ -32,7 +26,7 @@ local api = require "luci.passwall.api"
logLink.href = "#"; logLink.href = "#";
logLink.className = "log-link"; logLink.className = "log-link";
logLink.style.marginLeft = "10px"; logLink.style.marginLeft = "10px";
logLink.setAttribute("onclick", "window.open('" + '<%=api.url("get_chinadns_log")%>' + "?flag=" + sid + "', '_blank')"); logLink.setAttribute("onclick", "window.open('" + '<%=api.url("get_chinadns_log") .. "?flag=" .. section%>' + "', '_blank')");
select.insertAdjacentElement("afterend", logLink); select.insertAdjacentElement("afterend", logLink);
} }
}, 1000); }, 1000);

View File

@@ -6,12 +6,12 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=sing-box PKG_NAME:=sing-box
PKG_VERSION:=1.10.5 PKG_VERSION:=1.10.6
PKG_RELEASE:=1 PKG_RELEASE:=1
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/SagerNet/sing-box/tar.gz/v$(PKG_VERSION)? PKG_SOURCE_URL:=https://codeload.github.com/SagerNet/sing-box/tar.gz/v$(PKG_VERSION)?
PKG_HASH:=ca0385b45d160c2c2a1d0e09665f4f04caac27cb3dd9d6132173316dfd873b75 PKG_HASH:=bb3ca59c848f1509d0e183c54c91716993ce77ab0c9c48a0de0bcd4cc1f0e021
PKG_LICENSE:=GPL-3.0-or-later PKG_LICENSE:=GPL-3.0-or-later
PKG_LICENSE_FILES:=LICENSE PKG_LICENSE_FILES:=LICENSE

View File

@@ -74,3 +74,4 @@ jobs:
file: ${{ github.workspace }}/v2rayN*.zip file: ${{ github.workspace }}/v2rayN*.zip
tag: ${{ github.event.inputs.release_tag }} tag: ${{ github.event.inputs.release_tag }}
file_glob: true file_glob: true
prerelease: true

View File

@@ -75,3 +75,4 @@ jobs:
file: ${{ github.workspace }}/v2rayN*.zip file: ${{ github.workspace }}/v2rayN*.zip
tag: ${{ github.event.inputs.release_tag }} tag: ${{ github.event.inputs.release_tag }}
file_glob: true file_glob: true
prerelease: true

View File

@@ -65,3 +65,4 @@ jobs:
file: ${{ github.workspace }}/v2rayN*.zip file: ${{ github.workspace }}/v2rayN*.zip
tag: ${{ github.event.inputs.release_tag }} tag: ${{ github.event.inputs.release_tag }}
file_glob: true file_glob: true
prerelease: true

View File

@@ -11,16 +11,16 @@ echo "When this file exists, app will not store configs under this folder" > "$P
chmod +x "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN" chmod +x "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN"
mkdir -p "$PackagePath/icons.iconset" mkdir -p "$PackagePath/icons.iconset"
sips -z 16 16 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN.png" --out "$PackagePath/icons.iconset/icon_16x16.png" sips -z 16 16 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN2.png" --out "$PackagePath/icons.iconset/icon_16x16.png"
sips -z 32 32 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN.png" --out "$PackagePath/icons.iconset/icon_16x16@2x.png" sips -z 32 32 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN2.png" --out "$PackagePath/icons.iconset/icon_16x16@2x.png"
sips -z 32 32 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN.png" --out "$PackagePath/icons.iconset/icon_32x32.png" sips -z 32 32 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN2.png" --out "$PackagePath/icons.iconset/icon_32x32.png"
sips -z 64 64 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN.png" --out "$PackagePath/icons.iconset/icon_32x32@2x.png" sips -z 64 64 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN2.png" --out "$PackagePath/icons.iconset/icon_32x32@2x.png"
sips -z 128 128 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN.png" --out "$PackagePath/icons.iconset/icon_128x128.png" sips -z 128 128 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN2.png" --out "$PackagePath/icons.iconset/icon_128x128.png"
sips -z 256 256 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN.png" --out "$PackagePath/icons.iconset/icon_128x128@2x.png" sips -z 256 256 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN2.png" --out "$PackagePath/icons.iconset/icon_128x128@2x.png"
sips -z 256 256 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN.png" --out "$PackagePath/icons.iconset/icon_256x256.png" sips -z 256 256 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN2.png" --out "$PackagePath/icons.iconset/icon_256x256.png"
sips -z 512 512 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN.png" --out "$PackagePath/icons.iconset/icon_256x256@2x.png" sips -z 512 512 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN2.png" --out "$PackagePath/icons.iconset/icon_256x256@2x.png"
sips -z 512 512 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN.png" --out "$PackagePath/icons.iconset/icon_512x512.png" sips -z 512 512 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN2.png" --out "$PackagePath/icons.iconset/icon_512x512.png"
sips -z 1024 1024 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN.png" --out "$PackagePath/icons.iconset/icon_512x512@2x.png" sips -z 1024 1024 "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN2.png" --out "$PackagePath/icons.iconset/icon_512x512@2x.png"
iconutil -c icns "$PackagePath/icons.iconset" -o "$PackagePath/v2rayN.app/Contents/Resources/AppIcon.icns" iconutil -c icns "$PackagePath/icons.iconset" -o "$PackagePath/v2rayN.app/Contents/Resources/AppIcon.icns"
cat >"$PackagePath/v2rayN.app/Contents/Info.plist" <<-EOF cat >"$PackagePath/v2rayN.app/Contents/Info.plist" <<-EOF

View File

@@ -15,8 +15,15 @@
return; return;
} }
var fileName = Uri.UnescapeDataString(string.Join(" ", args)); var argData = Uri.UnescapeDataString(string.Join(" ", args));
UpgradeApp.Upgrade(fileName); if (argData.Equals("rebootas"))
{
Thread.Sleep(1000);
Utils.StartV2RayN();
return;
}
UpgradeApp.Upgrade(argData);
} }
} }
} }

View File

@@ -21,11 +21,11 @@ namespace AmazTool
Console.WriteLine(Resx.Resource.TryTerminateProcess); Console.WriteLine(Resx.Resource.TryTerminateProcess);
try try
{ {
var existing = Process.GetProcessesByName(V2rayN); var existing = Process.GetProcessesByName(Utils.V2rayN);
foreach (var pp in existing) foreach (var pp in existing)
{ {
var path = pp.MainModule?.FileName ?? ""; var path = pp.MainModule?.FileName ?? "";
if (path.StartsWith(GetPath(V2rayN))) if (path.StartsWith(Utils.GetPath(Utils.V2rayN)))
{ {
pp?.Kill(); pp?.Kill();
pp?.WaitForExit(1000); pp?.WaitForExit(1000);
@@ -42,7 +42,7 @@ namespace AmazTool
StringBuilder sb = new(); StringBuilder sb = new();
try try
{ {
string thisAppOldFile = $"{GetExePath()}.tmp"; string thisAppOldFile = $"{Utils.GetExePath()}.tmp";
File.Delete(thisAppOldFile); File.Delete(thisAppOldFile);
string splitKey = "/"; string splitKey = "/";
@@ -62,12 +62,12 @@ namespace AmazTool
if (lst.Length == 1) continue; if (lst.Length == 1) continue;
string fullName = string.Join(splitKey, lst[1..lst.Length]); string fullName = string.Join(splitKey, lst[1..lst.Length]);
if (string.Equals(GetExePath(), GetPath(fullName), StringComparison.OrdinalIgnoreCase)) if (string.Equals(Utils.GetExePath(), Utils.GetPath(fullName), StringComparison.OrdinalIgnoreCase))
{ {
File.Move(GetExePath(), thisAppOldFile); File.Move(Utils.GetExePath(), thisAppOldFile);
} }
string entryOutputPath = GetPath(fullName); string entryOutputPath = Utils.GetPath(fullName);
Directory.CreateDirectory(Path.GetDirectoryName(entryOutputPath)!); Directory.CreateDirectory(Path.GetDirectoryName(entryOutputPath)!);
entry.ExtractToFile(entryOutputPath, true); entry.ExtractToFile(entryOutputPath, true);
@@ -92,39 +92,11 @@ namespace AmazTool
Console.WriteLine(Resx.Resource.Restartv2rayN); Console.WriteLine(Resx.Resource.Restartv2rayN);
Waiting(2); Waiting(2);
Process process = new()
{ Utils.StartV2RayN();
StartInfo = new()
{
UseShellExecute = true,
FileName = V2rayN,
WorkingDirectory = StartupPath()
}
};
process.Start();
} }
private static string GetExePath() public static void Waiting(int second)
{
return Environment.ProcessPath ?? Process.GetCurrentProcess().MainModule?.FileName ?? string.Empty;
}
private static string StartupPath()
{
return AppDomain.CurrentDomain.BaseDirectory;
}
private static string GetPath(string fileName)
{
string startupPath = StartupPath();
if (string.IsNullOrEmpty(fileName))
{
return startupPath;
}
return Path.Combine(startupPath, fileName);
}
private static void Waiting(int second)
{ {
for (var i = second; i > 0; i--) for (var i = second; i > 0; i--)
{ {
@@ -132,7 +104,5 @@ namespace AmazTool
Thread.Sleep(1000); Thread.Sleep(1000);
} }
} }
private static string V2rayN => "v2rayN";
} }
} }

View File

@@ -0,0 +1,43 @@
using System.Diagnostics;
namespace AmazTool
{
internal class Utils
{
public static string GetExePath()
{
return Environment.ProcessPath ?? Process.GetCurrentProcess().MainModule?.FileName ?? string.Empty;
}
public static string StartupPath()
{
return AppDomain.CurrentDomain.BaseDirectory;
}
public static string GetPath(string fileName)
{
string startupPath = StartupPath();
if (string.IsNullOrEmpty(fileName))
{
return startupPath;
}
return Path.Combine(startupPath, fileName);
}
public static string V2rayN => "v2rayN";
public static void StartV2RayN()
{
Process process = new()
{
StartInfo = new()
{
UseShellExecute = true,
FileName = V2rayN,
WorkingDirectory = StartupPath()
}
};
process.Start();
}
}
}

View File

@@ -0,0 +1,105 @@
using System.Diagnostics;
namespace ServiceLib.Common;
public static class ProcUtils
{
private static readonly string _tag = "ProcUtils";
public static void ProcessStart(string? fileName, string arguments = "")
{
ProcessStart(fileName, arguments, null);
}
public static int? ProcessStart(string? fileName, string arguments, string? dir)
{
if (fileName.IsNullOrEmpty())
{
return null;
}
try
{
if (fileName.Contains(' ')) fileName = fileName.AppendQuotes();
if (arguments.Contains(' ')) arguments = arguments.AppendQuotes();
Process process = new()
{
StartInfo = new ProcessStartInfo
{
UseShellExecute = true,
FileName = fileName,
Arguments = arguments,
WorkingDirectory = dir
}
};
process.Start();
return process.Id;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return null;
}
public static void RebootAsAdmin(bool blAdmin = true)
{
try
{
ProcessStartInfo startInfo = new()
{
UseShellExecute = true,
Arguments = Global.RebootAs,
WorkingDirectory = Utils.StartupPath(),
FileName = Utils.GetExePath().AppendQuotes(),
Verb = blAdmin ? "runas" : null,
};
Process.Start(startInfo);
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
}
public static async Task ProcessKill(int pid)
{
try
{
await ProcessKill(Process.GetProcessById(pid), false);
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
}
public static async Task ProcessKill(Process? proc, bool review)
{
if (proc is null)
{
return;
}
var fileName = review ? proc?.MainModule?.FileName : null;
var processName = review ? proc?.ProcessName : null;
try { proc?.Kill(true); } catch (Exception ex) { Logging.SaveLog(_tag, ex); }
try { proc?.Kill(); } catch (Exception ex) { Logging.SaveLog(_tag, ex); }
try { proc?.Close(); } catch (Exception ex) { Logging.SaveLog(_tag, ex); }
try { proc?.Dispose(); } catch (Exception ex) { Logging.SaveLog(_tag, ex); }
await Task.Delay(300);
if (review && fileName != null)
{
var proc2 = Process.GetProcessesByName(processName)
.FirstOrDefault(t => t.MainModule?.FileName == fileName);
if (proc2 != null)
{
Logging.SaveLog($"{_tag}, KillProcess not completing the job");
await ProcessKill(proc2, false);
proc2 = null;
}
}
}
}

View File

@@ -517,10 +517,10 @@ namespace ServiceLib.Common
#region #region
public static bool UpgradeAppExists(out string fileName) public static bool UpgradeAppExists(out string upgradeFileName)
{ {
fileName = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, GetExeName("AmazTool")); upgradeFileName = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, GetExeName("AmazTool"));
return File.Exists(fileName); return File.Exists(upgradeFileName);
} }
/// <summary> /// <summary>
@@ -591,26 +591,6 @@ namespace ServiceLib.Common
return Guid.TryParse(strSrc, out _); return Guid.TryParse(strSrc, out _);
} }
public static void ProcessStart(string? fileName, string arguments = "")
{
try
{
if (fileName.IsNullOrEmpty())
{
return;
}
if (fileName.Contains(' ')) fileName = fileName.AppendQuotes();
if (arguments.Contains(' ')) arguments = arguments.AppendQuotes();
Process.Start(new ProcessStartInfo(fileName, arguments) { UseShellExecute = true });
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
}
public static Dictionary<string, string> GetSystemHosts() public static Dictionary<string, string> GetSystemHosts()
{ {
var systemHosts = new Dictionary<string, string>(); var systemHosts = new Dictionary<string, string>();

View File

@@ -32,9 +32,9 @@ namespace ServiceLib.Handler
{ {
if (it.CoreType == ECoreType.v2rayN) if (it.CoreType == ECoreType.v2rayN)
{ {
if (Utils.UpgradeAppExists(out var fileName)) if (Utils.UpgradeAppExists(out var upgradeFileName))
{ {
await Utils.SetLinuxChmod(fileName); await Utils.SetLinuxChmod(upgradeFileName);
} }
continue; continue;
} }
@@ -55,7 +55,7 @@ namespace ServiceLib.Handler
{ {
if (node == null) if (node == null)
{ {
ShowMsg(false, ResUI.CheckServerSettings); UpdateFunc(false, ResUI.CheckServerSettings);
return; return;
} }
@@ -63,13 +63,13 @@ namespace ServiceLib.Handler
var result = await CoreConfigHandler.GenerateClientConfig(node, fileName); var result = await CoreConfigHandler.GenerateClientConfig(node, fileName);
if (result.Success != true) if (result.Success != true)
{ {
ShowMsg(true, result.Msg); UpdateFunc(true, result.Msg);
return; return;
} }
ShowMsg(true, $"{node.GetSummary()}"); UpdateFunc(true, $"{node.GetSummary()}");
ShowMsg(false, $"{Utils.GetRuntimeInfo()}"); UpdateFunc(false, $"{Utils.GetRuntimeInfo()}");
ShowMsg(false, string.Format(ResUI.StartService, DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"))); UpdateFunc(false, string.Format(ResUI.StartService, DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")));
await CoreStop(); await CoreStop();
await Task.Delay(100); await Task.Delay(100);
await CoreStart(node); await CoreStart(node);
@@ -81,15 +81,23 @@ namespace ServiceLib.Handler
var coreType = selecteds.Exists(t => t.ConfigType is EConfigType.Hysteria2 or EConfigType.TUIC or EConfigType.WireGuard) ? ECoreType.sing_box : ECoreType.Xray; var coreType = selecteds.Exists(t => t.ConfigType is EConfigType.Hysteria2 or EConfigType.TUIC or EConfigType.WireGuard) ? ECoreType.sing_box : ECoreType.Xray;
var configPath = Utils.GetConfigPath(Global.CoreSpeedtestConfigFileName); var configPath = Utils.GetConfigPath(Global.CoreSpeedtestConfigFileName);
var result = await CoreConfigHandler.GenerateClientSpeedtestConfig(_config, configPath, selecteds, coreType); var result = await CoreConfigHandler.GenerateClientSpeedtestConfig(_config, configPath, selecteds, coreType);
ShowMsg(false, result.Msg); UpdateFunc(false, result.Msg);
if (result.Success != true) if (result.Success != true)
{ {
return -1; return -1;
} }
ShowMsg(false, string.Format(ResUI.StartService, DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"))); UpdateFunc(false, string.Format(ResUI.StartService, DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")));
ShowMsg(false, configPath); UpdateFunc(false, configPath);
return await CoreStartSpeedtest(configPath, coreType);
var coreInfo = CoreInfoHandler.Instance.GetCoreInfo(coreType);
var proc = await RunProcess(coreInfo, Global.CoreSpeedtestConfigFileName, true, false);
if (proc is null)
{
return -1;
}
return proc.Id;
} }
public async Task CoreStop() public async Task CoreStop()
@@ -98,13 +106,13 @@ namespace ServiceLib.Handler
{ {
if (_process != null) if (_process != null)
{ {
await KillProcess(_process, true); await ProcUtils.ProcessKill(_process, true);
_process = null; _process = null;
} }
if (_processPre != null) if (_processPre != null)
{ {
await KillProcess(_processPre, true); await ProcUtils.ProcessKill(_processPre, true);
_processPre = null; _processPre = null;
} }
@@ -120,41 +128,8 @@ namespace ServiceLib.Handler
} }
} }
public async Task CoreStopPid(int pid)
{
try
{
await KillProcess(Process.GetProcessById(pid), false);
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
}
#region Private #region Private
private string CoreFindExe(CoreInfo coreInfo)
{
var fileName = string.Empty;
foreach (var name in coreInfo.CoreExes)
{
var vName = Utils.GetBinPath(Utils.GetExeName(name), coreInfo.CoreType.ToString());
if (File.Exists(vName))
{
fileName = vName;
break;
}
}
if (Utils.IsNullOrEmpty(fileName))
{
var msg = string.Format(ResUI.NotFoundCore, Utils.GetBinPath("", coreInfo.CoreType.ToString()), string.Join(", ", coreInfo.CoreExes.ToArray()), coreInfo.Url);
Logging.SaveLog(msg);
ShowMsg(false, msg);
}
return fileName;
}
private async Task CoreStart(ProfileItem node) private async Task CoreStart(ProfileItem node)
{ {
var coreType = _config.RunningCoreType = AppHandler.Instance.GetCoreType(node, node.ConfigType); var coreType = _config.RunningCoreType = AppHandler.Instance.GetCoreType(node, node.ConfigType);
@@ -194,28 +169,7 @@ namespace ServiceLib.Handler
} }
} }
private async Task<int> CoreStartSpeedtest(string configPath, ECoreType coreType) private void UpdateFunc(bool notify, string msg)
{
try
{
var coreInfo = CoreInfoHandler.Instance.GetCoreInfo(coreType);
var proc = await RunProcess(coreInfo, Global.CoreSpeedtestConfigFileName, true, false);
if (proc is null)
{
return -1;
}
return proc.Id;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
ShowMsg(false, ex.Message);
return -1;
}
}
private void ShowMsg(bool notify, string msg)
{ {
_updateFunc?.Invoke(notify, msg); _updateFunc?.Invoke(notify, msg);
} }
@@ -233,11 +187,12 @@ namespace ServiceLib.Handler
#region Process #region Process
private async Task<Process?> RunProcess(CoreInfo coreInfo, string configPath, bool displayLog, bool mayNeedSudo) private async Task<Process?> RunProcess(CoreInfo? coreInfo, string configPath, bool displayLog, bool mayNeedSudo)
{ {
var fileName = CoreFindExe(coreInfo); var fileName = CoreInfoHandler.Instance.GetCoreExecFile(coreInfo, out var msg);
if (Utils.IsNullOrEmpty(fileName)) if (Utils.IsNullOrEmpty(fileName))
{ {
UpdateFunc(false, msg);
return null; return null;
} }
@@ -272,12 +227,12 @@ namespace ServiceLib.Handler
proc.OutputDataReceived += (sender, e) => proc.OutputDataReceived += (sender, e) =>
{ {
if (Utils.IsNullOrEmpty(e.Data)) return; if (Utils.IsNullOrEmpty(e.Data)) return;
ShowMsg(false, e.Data + Environment.NewLine); UpdateFunc(false, e.Data + Environment.NewLine);
}; };
proc.ErrorDataReceived += (sender, e) => proc.ErrorDataReceived += (sender, e) =>
{ {
if (Utils.IsNullOrEmpty(e.Data)) return; if (Utils.IsNullOrEmpty(e.Data)) return;
ShowMsg(false, e.Data + Environment.NewLine); UpdateFunc(false, e.Data + Environment.NewLine);
if (!startUpSuccessful) if (!startUpSuccessful)
{ {
@@ -319,40 +274,11 @@ namespace ServiceLib.Handler
catch (Exception ex) catch (Exception ex)
{ {
Logging.SaveLog(_tag, ex); Logging.SaveLog(_tag, ex);
ShowMsg(true, ex.Message); UpdateFunc(true, ex.Message);
return null; return null;
} }
} }
private async Task KillProcess(Process? proc, bool review)
{
if (proc is null)
{
return;
}
var fileName = proc?.MainModule?.FileName;
var processName = proc?.ProcessName;
try { proc?.Kill(true); } catch (Exception ex) { Logging.SaveLog(_tag, ex); }
try { proc?.Kill(); } catch (Exception ex) { Logging.SaveLog(_tag, ex); }
try { proc?.Close(); } catch (Exception ex) { Logging.SaveLog(_tag, ex); }
try { proc?.Dispose(); } catch (Exception ex) { Logging.SaveLog(_tag, ex); }
await Task.Delay(500);
if (review)
{
var proc2 = Process.GetProcessesByName(processName)
.FirstOrDefault(t => t.MainModule?.FileName == fileName);
if (proc2 != null)
{
Logging.SaveLog($"{_tag}, KillProcess not completing the job");
await KillProcess(proc2, false);
proc2 = null;
}
}
}
#endregion Process #endregion Process
#region Linux #region Linux

View File

@@ -29,6 +29,27 @@
return _coreInfo ?? []; return _coreInfo ?? [];
} }
public string GetCoreExecFile(CoreInfo? coreInfo, out string msg)
{
var fileName = string.Empty;
msg = string.Empty;
foreach (var name in coreInfo?.CoreExes)
{
var vName = Utils.GetBinPath(Utils.GetExeName(name), coreInfo.CoreType.ToString());
if (File.Exists(vName))
{
fileName = vName;
break;
}
}
if (fileName.IsNullOrEmpty())
{
msg = string.Format(ResUI.NotFoundCore, Utils.GetBinPath("", coreInfo.CoreType.ToString()), string.Join(", ", coreInfo.CoreExes.ToArray()), coreInfo.Url);
Logging.SaveLog(msg);
}
return fileName;
}
private void InitCoreInfo() private void InitCoreInfo()
{ {
_coreInfo = []; _coreInfo = [];

View File

@@ -4,6 +4,15 @@
"proxy.example.com": "127.0.0.1" "proxy.example.com": "127.0.0.1"
}, },
"servers": [ "servers": [
{
"address": "1.1.1.1",
"domains": [
"geosite:geolocation-!cn"
],
"expectIPs": [
"geoip:!cn"
]
},
{ {
"address": "223.5.5.5", "address": "223.5.5.5",
"domains": [ "domains": [
@@ -13,7 +22,6 @@
"geoip:cn" "geoip:cn"
] ]
}, },
"1.1.1.1",
"8.8.8.8", "8.8.8.8",
"https://dns.google/dns-query" "https://dns.google/dns-query"
] ]

View File

@@ -4,7 +4,7 @@
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<Version>7.5.1</Version> <Version>7.5.2</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -216,7 +216,7 @@ namespace ServiceLib.Services
{ {
if (pid > 0) if (pid > 0)
{ {
await CoreHandler.Instance.CoreStopPid(pid); await ProcUtils.ProcessKill(pid);
} }
await ProfileExHandler.Instance.SaveTo(); await ProfileExHandler.Instance.SaveTo();
} }
@@ -278,7 +278,7 @@ namespace ServiceLib.Services
if (pid > 0) if (pid > 0)
{ {
await CoreHandler.Instance.CoreStopPid(pid); await ProcUtils.ProcessKill(pid);
} }
await ProfileExHandler.Instance.SaveTo(); await ProfileExHandler.Instance.SaveTo();
} }
@@ -342,7 +342,7 @@ namespace ServiceLib.Services
if (pid > 0) if (pid > 0)
{ {
await CoreHandler.Instance.CoreStopPid(pid); await ProcUtils.ProcessKill(pid);
} }
await ProfileExHandler.Instance.SaveTo(); await ProfileExHandler.Instance.SaveTo();
} }

View File

@@ -103,7 +103,7 @@ namespace ServiceLib.ViewModels
address = Utils.GetConfigPath(address); address = Utils.GetConfigPath(address);
if (File.Exists(address)) if (File.Exists(address))
{ {
Utils.ProcessStart(address); ProcUtils.ProcessStart(address);
} }
else else
{ {

View File

@@ -130,11 +130,6 @@ namespace ServiceLib.ViewModels
DisplayOperationMsg(ResUI.LocalRestoreInvalidZipTips); DisplayOperationMsg(ResUI.LocalRestoreInvalidZipTips);
return; return;
} }
if (!Utils.UpgradeAppExists(out _))
{
DisplayOperationMsg(ResUI.UpgradeAppNotExistTip);
return;
}
//backup first //backup first
var fileBackup = Utils.GetBackupPath(BackupFileName); var fileBackup = Utils.GetBackupPath(BackupFileName);
@@ -150,13 +145,17 @@ namespace ServiceLib.ViewModels
if (Utils.IsWindows()) if (Utils.IsWindows())
{ {
service?.RebootAsAdmin(false); ProcUtils.RebootAsAdmin(false);
} }
else else
{ {
service?.Shutdown(); if (Utils.UpgradeAppExists(out var upgradeFileName))
{
ProcUtils.ProcessStart(upgradeFileName, Global.RebootAs, Utils.StartupPath());
} }
} }
service?.Shutdown();
}
else else
{ {
DisplayOperationMsg(WebDavHandler.Instance.GetLastError()); DisplayOperationMsg(WebDavHandler.Instance.GetLastError());

View File

@@ -1,9 +1,7 @@
using ReactiveUI; using ReactiveUI;
using ReactiveUI.Fody.Helpers; using ReactiveUI.Fody.Helpers;
using Splat; using Splat;
using System.Diagnostics;
using System.Reactive; using System.Reactive;
using System.Reactive.Linq;
namespace ServiceLib.ViewModels namespace ServiceLib.ViewModels
{ {
@@ -312,27 +310,16 @@ namespace ServiceLib.ViewModels
public async Task UpgradeApp(string arg) public async Task UpgradeApp(string arg)
{ {
if (!Utils.UpgradeAppExists(out var fileName)) if (!Utils.UpgradeAppExists(out var upgradeFileName))
{ {
NoticeHandler.Instance.SendMessageAndEnqueue(ResUI.UpgradeAppNotExistTip); NoticeHandler.Instance.SendMessageAndEnqueue(ResUI.UpgradeAppNotExistTip);
Logging.SaveLog("UpgradeApp does not exist"); Logging.SaveLog("UpgradeApp does not exist");
return; return;
} }
Process process = new() var id = ProcUtils.ProcessStart(upgradeFileName, arg, Utils.StartupPath());
if (id > 0)
{ {
StartInfo = new ProcessStartInfo
{
UseShellExecute = true,
FileName = fileName,
Arguments = arg.AppendQuotes(),
WorkingDirectory = Utils.StartupPath()
}
};
process.Start();
if (process.Id > 0)
{
await MyAppExitAsync(false);
await MyAppExitAsync(false); await MyAppExitAsync(false);
} }
} }
@@ -513,23 +500,11 @@ namespace ServiceLib.ViewModels
} }
} }
public async Task RebootAsAdmin(bool blAdmin = true) public async Task RebootAsAdmin()
{ {
try ProcUtils.RebootAsAdmin();
{
ProcessStartInfo startInfo = new()
{
UseShellExecute = true,
Arguments = Global.RebootAs,
WorkingDirectory = Utils.StartupPath(),
FileName = Utils.GetExePath().AppendQuotes(),
Verb = blAdmin ? "runas" : null,
};
Process.Start(startInfo);
await MyAppExitAsync(false); await MyAppExitAsync(false);
} }
catch { }
}
private async Task ClearServerStatistics() private async Task ClearServerStatistics()
{ {
@@ -542,15 +517,15 @@ namespace ServiceLib.ViewModels
var path = Utils.StartupPath(); var path = Utils.StartupPath();
if (Utils.IsWindows()) if (Utils.IsWindows())
{ {
Utils.ProcessStart(path); ProcUtils.ProcessStart(path);
} }
else if (Utils.IsLinux()) else if (Utils.IsLinux())
{ {
Utils.ProcessStart("nautilus", path); ProcUtils.ProcessStart("nautilus", path);
} }
else if (Utils.IsOSX()) else if (Utils.IsOSX())
{ {
Utils.ProcessStart("open", path); ProcUtils.ProcessStart("open", path);
} }
} }

View File

@@ -65,12 +65,12 @@ namespace v2rayN.Desktop.Views
private void linkDnsObjectDoc_Click(object? sender, RoutedEventArgs e) private void linkDnsObjectDoc_Click(object? sender, RoutedEventArgs e)
{ {
Utils.ProcessStart("https://xtls.github.io/config/dns.html#dnsobject"); ProcUtils.ProcessStart("https://xtls.github.io/config/dns.html#dnsobject");
} }
private void linkDnsSingboxObjectDoc_Click(object? sender, RoutedEventArgs e) private void linkDnsSingboxObjectDoc_Click(object? sender, RoutedEventArgs e)
{ {
Utils.ProcessStart("https://sing-box.sagernet.org/zh/configuration/dns/"); ProcUtils.ProcessStart("https://sing-box.sagernet.org/zh/configuration/dns/");
} }
} }
} }

View File

@@ -330,12 +330,12 @@ namespace v2rayN.Desktop.Views
private void menuPromotion_Click(object? sender, RoutedEventArgs e) private void menuPromotion_Click(object? sender, RoutedEventArgs e)
{ {
Utils.ProcessStart($"{Utils.Base64Decode(Global.PromotionUrl)}?t={DateTime.Now.Ticks}"); ProcUtils.ProcessStart($"{Utils.Base64Decode(Global.PromotionUrl)}?t={DateTime.Now.Ticks}");
} }
private void menuSettingsSetUWP_Click(object? sender, RoutedEventArgs e) private void menuSettingsSetUWP_Click(object? sender, RoutedEventArgs e)
{ {
Utils.ProcessStart(Utils.GetBinPath("EnableLoopback.exe")); ProcUtils.ProcessStart(Utils.GetBinPath("EnableLoopback.exe"));
} }
public async Task ScanScreenTaskAsync() public async Task ScanScreenTaskAsync()
@@ -481,7 +481,7 @@ namespace v2rayN.Desktop.Views
{ {
if (sender is MenuItem item) if (sender is MenuItem item)
{ {
Utils.ProcessStart(item.Tag?.ToString()); ProcUtils.ProcessStart(item.Tag?.ToString());
} }
} }

View File

@@ -95,7 +95,7 @@ namespace v2rayN.Desktop.Views
private void linkRuleobjectDoc_Click(object? sender, RoutedEventArgs e) private void linkRuleobjectDoc_Click(object? sender, RoutedEventArgs e)
{ {
Utils.ProcessStart("https://xtls.github.io/config/routing.html#ruleobject"); ProcUtils.ProcessStart("https://xtls.github.io/config/routing.html#ruleobject");
} }
} }
} }

View File

@@ -203,7 +203,7 @@ namespace v2rayN.Desktop.Views
private void linkCustomRulesetPath4Singbox(object? sender, RoutedEventArgs e) private void linkCustomRulesetPath4Singbox(object? sender, RoutedEventArgs e)
{ {
Utils.ProcessStart("https://github.com/2dust/v2rayCustomRoutingList/blob/master/singbox_custom_ruleset_example.json"); ProcUtils.ProcessStart("https://github.com/2dust/v2rayCustomRoutingList/blob/master/singbox_custom_ruleset_example.json");
} }
} }
} }

View File

@@ -117,12 +117,12 @@ namespace v2rayN.Desktop.Views
private void linkdomainStrategy_Click(object? sender, RoutedEventArgs e) private void linkdomainStrategy_Click(object? sender, RoutedEventArgs e)
{ {
Utils.ProcessStart("https://xtls.github.io/config/routing.html"); ProcUtils.ProcessStart("https://xtls.github.io/config/routing.html");
} }
private void linkdomainStrategy4Singbox_Click(object? sender, RoutedEventArgs e) private void linkdomainStrategy4Singbox_Click(object? sender, RoutedEventArgs e)
{ {
Utils.ProcessStart("https://sing-box.sagernet.org/zh/configuration/shared/listen/#domain_strategy"); ProcUtils.ProcessStart("https://sing-box.sagernet.org/zh/configuration/shared/listen/#domain_strategy");
} }
private void btnCancel_Click(object? sender, RoutedEventArgs e) private void btnCancel_Click(object? sender, RoutedEventArgs e)

View File

@@ -48,6 +48,9 @@
<None Update="v2rayN.png"> <None Update="v2rayN.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="v2rayN2.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup> </ItemGroup>
</Project> </Project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -66,12 +66,12 @@ namespace v2rayN.Views
private void linkDnsObjectDoc_Click(object sender, RoutedEventArgs e) private void linkDnsObjectDoc_Click(object sender, RoutedEventArgs e)
{ {
Utils.ProcessStart("https://xtls.github.io/config/dns.html#dnsobject"); ProcUtils.ProcessStart("https://xtls.github.io/config/dns.html#dnsobject");
} }
private void linkDnsSingboxObjectDoc_Click(object sender, RoutedEventArgs e) private void linkDnsSingboxObjectDoc_Click(object sender, RoutedEventArgs e)
{ {
Utils.ProcessStart("https://sing-box.sagernet.org/zh/configuration/dns/"); ProcUtils.ProcessStart("https://sing-box.sagernet.org/zh/configuration/dns/");
} }
} }
} }

View File

@@ -308,12 +308,12 @@ namespace v2rayN.Views
private void menuPromotion_Click(object sender, RoutedEventArgs e) private void menuPromotion_Click(object sender, RoutedEventArgs e)
{ {
Utils.ProcessStart($"{Utils.Base64Decode(Global.PromotionUrl)}?t={DateTime.Now.Ticks}"); ProcUtils.ProcessStart($"{Utils.Base64Decode(Global.PromotionUrl)}?t={DateTime.Now.Ticks}");
} }
private void menuSettingsSetUWP_Click(object sender, RoutedEventArgs e) private void menuSettingsSetUWP_Click(object sender, RoutedEventArgs e)
{ {
Utils.ProcessStart(Utils.GetBinPath("EnableLoopback.exe")); ProcUtils.ProcessStart(Utils.GetBinPath("EnableLoopback.exe"));
} }
private async Task ScanScreenTaskAsync() private async Task ScanScreenTaskAsync()
@@ -443,7 +443,7 @@ namespace v2rayN.Views
{ {
if (sender is MenuItem item) if (sender is MenuItem item)
{ {
Utils.ProcessStart(item.Tag.ToString()); ProcUtils.ProcessStart(item.Tag.ToString());
} }
} }

View File

@@ -89,7 +89,7 @@ namespace v2rayN.Views
private void linkRuleobjectDoc_Click(object sender, RoutedEventArgs e) private void linkRuleobjectDoc_Click(object sender, RoutedEventArgs e)
{ {
Utils.ProcessStart("https://xtls.github.io/config/routing.html#ruleobject"); ProcUtils.ProcessStart("https://xtls.github.io/config/routing.html#ruleobject");
} }
} }
} }

View File

@@ -197,7 +197,7 @@ namespace v2rayN.Views
private void linkCustomRulesetPath4Singbox(object sender, RoutedEventArgs e) private void linkCustomRulesetPath4Singbox(object sender, RoutedEventArgs e)
{ {
Utils.ProcessStart("https://github.com/2dust/v2rayCustomRoutingList/blob/master/singbox_custom_ruleset_example.json"); ProcUtils.ProcessStart("https://github.com/2dust/v2rayCustomRoutingList/blob/master/singbox_custom_ruleset_example.json");
} }
} }
} }

View File

@@ -122,12 +122,12 @@ namespace v2rayN.Views
private void linkdomainStrategy_Click(object sender, System.Windows.RoutedEventArgs e) private void linkdomainStrategy_Click(object sender, System.Windows.RoutedEventArgs e)
{ {
Utils.ProcessStart("https://xtls.github.io/config/routing.html"); ProcUtils.ProcessStart("https://xtls.github.io/config/routing.html");
} }
private void linkdomainStrategy4Singbox_Click(object sender, RoutedEventArgs e) private void linkdomainStrategy4Singbox_Click(object sender, RoutedEventArgs e)
{ {
Utils.ProcessStart("https://sing-box.sagernet.org/zh/configuration/shared/listen/#domain_strategy"); ProcUtils.ProcessStart("https://sing-box.sagernet.org/zh/configuration/shared/listen/#domain_strategy");
} }
private void btnCancel_Click(object sender, System.Windows.RoutedEventArgs e) private void btnCancel_Click(object sender, System.Windows.RoutedEventArgs e)

View File

@@ -17,6 +17,8 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
submodules: 'true'
- name: Prepare build dir - name: Prepare build dir
run: | run: |
@@ -62,7 +64,6 @@ jobs:
cd ${{ github.workspace }}/build/AndroidLibV2rayLite cd ${{ github.workspace }}/build/AndroidLibV2rayLite
bash compile-tun2socks.sh bash compile-tun2socks.sh
tar -xvzf libtun2socks.so.tgz tar -xvzf libtun2socks.so.tgz
cp -r libs/* ${{ github.workspace }}/V2rayNG/app/libs/
env: env:
NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
@@ -75,7 +76,7 @@ jobs:
- name: Copy libtun2socks - name: Copy libtun2socks
run: | run: |
cp -r ${{ github.workspace }}/build/AndroidLibV2rayLite/libs/* ${{ github.workspace }}/V2rayNG/app/libs/ cp -r ${{ github.workspace }}/build/AndroidLibV2rayLite/libs ${{ github.workspace }}/V2rayNG/app
- name: Download libv2ray - name: Download libv2ray
uses: robinraju/release-downloader@v1 uses: robinraju/release-downloader@v1
@@ -85,6 +86,44 @@ jobs:
fileName: 'libv2ray.aar' fileName: 'libv2ray.aar'
out-file-path: V2rayNG/app/libs/ out-file-path: V2rayNG/app/libs/
- name: Restore cached libhysteria2
id: cache-libhysteria2-restore
uses: actions/cache/restore@v4
with:
path: ${{ github.workspace }}/hysteria/libs
key: libhysteria2-${{ runner.os }}-${{ hashFiles('.git/modules/hysteria/HEAD') }}-${{ hashFiles('libhysteria.sh') }}
- name: Fetch Go version from AndroidLibXrayLite
if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true'
run: |
GO_VERSION=$(curl -sL https://github.com/2dust/AndroidLibXrayLite/raw/refs/heads/main/go.mod | sed -n -E 's/.*go ([0-9]+\.[0-9]+\.[0-9]+).*/\1/p')
echo "Go version: $GO_VERSION"
echo "GO_VERSION=$GO_VERSION" >> $GITHUB_ENV
- name: Setup Golang
if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true'
uses: actions/setup-go@v5
with:
go-version: '${{ env.GO_VERSION }}'
- name: Build libhysteria2
if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true'
run: |
bash libhysteria2.sh
env:
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
- name: Save libhysteria2
if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: ${{ github.workspace }}/hysteria/libs
key: libhysteria2-${{ runner.os }}-${{ hashFiles('.git/modules/hysteria/HEAD') }}-${{ hashFiles('libhysteria.sh') }}
- name: Copy libhysteria2
run: |
cp -r ${{ github.workspace }}/hysteria/libs ${{ github.workspace }}/V2rayNG/app
- name: Setup Java - name: Setup Java
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:

3
v2rayng/.gitignore vendored
View File

@@ -3,5 +3,4 @@
V2rayNG/app/release/output.json V2rayNG/app/release/output.json
.idea/ .idea/
.gradle/ .gradle/
libtun2socks.so *.so
libhysteria2.so

3
v2rayng/.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "hysteria"]
path = hysteria
url = https://github.com/apernet/hysteria

1
v2rayng/hysteria/.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
custom: [ 'https://v2.hysteria.network/docs/Donation/' ]

View File

@@ -0,0 +1,26 @@
---
name: Bug report
about: Report anything you think is a bug and needs to be fixed.
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Logs**
Attach logs from the client/server when the error occurs.
**Device and Operating System**
What are you using it on.
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,26 @@
---
name: Bug 反馈
about: 反馈任何你认为是 bug 需要修复的问题。
title: ''
labels: bug
assignees: ''
---
**描述问题**
请尽量清晰精准地描述你遇到的问题。
**如何复现**
复现问题的步骤。
**预期行为**
你认为修复后的行为应该是怎样的。
**日志**
附上客户端/服务器端在错误发生前后的日志。
**设备和操作系统**
你在用什么设备和操作系统。
**额外信息**
其他你认为有助于解决问题的信息。

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project.
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -0,0 +1,20 @@
---
name: 功能请求
about: 为这个项目提出改进意见。
title: ''
labels: enhancement
assignees: ''
---
**你的功能请求是否与某个问题有关?**
请尽量清晰精准地描述你遇到的问题。例如:我家运营商限制 UDP 协议速度,导致 Hysteria 很慢,希望增加 FakeTCP 支持。
**描述你希望的解决方案**
请尽量清晰精准地描述你希望的解决方案。
**有没有其他替代方案**
请尽量清晰精准地描述你认为可能的替代方案。
**额外信息**
其他你认为有助于开发者了解你需求的信息。

10
v2rayng/hysteria/.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"

View File

@@ -0,0 +1,104 @@
name: "Create release tags for nested modules"
on:
push:
tags:
- app/v*.*.*
permissions:
contents: write
jobs:
tag:
name: "Create tags"
runs-on: ubuntu-latest
steps:
- name: "Extract tagbase"
id: extract_tagbase
uses: actions/github-script@v7
with:
script: |
const ref = context.ref;
core.info(`context.ref: ${ref}`);
const refPrefix = 'refs/tags/app/';
if (!ref.startsWith(refPrefix)) {
core.setFailed(`context.ref does not start with ${refPrefix}: ${ref}`);
return;
}
const tagbase = ref.slice(refPrefix.length);
core.info(`tagbase: ${tagbase}`);
core.setOutput('tagbase', tagbase);
- name: "Tagging core/*"
uses: actions/github-script@v7
env:
INPUT_TAGPREFIX: "core/"
INPUT_TAGBASE: ${{ steps.extract_tagbase.outputs.tagbase }}
with:
script: |
const tagbase = core.getInput('tagbase', { required: true });
const tagprefix = core.getInput('tagprefix', { required: true });
const refname = `tags/${tagprefix}${tagbase}`;
core.info(`creating ref ${refname}`);
try {
await github.rest.git.createRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `refs/${refname}`,
sha: context.sha
});
core.info(`created ref ${refname}`);
return;
} catch (error) {
core.info(`failed to create ref ${refname}: ${error}`);
}
core.info(`updating ref ${refname}`)
try {
await github.rest.git.updateRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: refname,
sha: context.sha
});
core.info(`updated ref ${refname}`);
return;
} catch (error) {
core.setFailed(`failed to update ref ${refname}: ${error}`);
}
- name: "Tagging extras/*"
uses: actions/github-script@v7
env:
INPUT_TAGPREFIX: "extras/"
INPUT_TAGBASE: ${{ steps.extract_tagbase.outputs.tagbase }}
with:
script: |
const tagbase = core.getInput('tagbase', { required: true });
const tagprefix = core.getInput('tagprefix', { required: true });
const refname = `tags/${tagprefix}${tagbase}`;
core.info(`creating ref ${refname}`);
try {
await github.rest.git.createRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `refs/${refname}`,
sha: context.sha
});
core.info(`created ref ${refname}`);
return;
} catch (error) {
core.info(`failed to create ref ${refname}: ${error}`);
}
core.info(`updating ref ${refname}`)
try {
await github.rest.git.updateRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: refname,
sha: context.sha
});
core.info(`updated ref ${refname}`);
return;
} catch (error) {
core.setFailed(`failed to update ref ${refname}: ${error}`);
}

View File

@@ -0,0 +1,44 @@
name: "Build Docker Image"
on:
push:
tags:
- app/v*.*.*
jobs:
docker:
runs-on: ubuntu-latest
env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: true
steps:
- name: Check out
uses: actions/checkout@v4
- name: Get version
id: get_version
run: echo "version=$(git describe --tags --always --match 'app/v*' | sed -n 's|app/\([^/-]*\)\(-.*\)\{0,1\}|\1|p')" >> $GITHUB_OUTPUT
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v5
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: tobyxdd/hysteria:latest,tobyxdd/hysteria:v2,tobyxdd/hysteria:${{ steps.get_version.outputs.version }}
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}

View File

@@ -0,0 +1,52 @@
name: "Build master branch"
on:
push:
branches:
- master
jobs:
build:
name: Build
runs-on: ubuntu-latest
env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: true
steps:
- name: Check out
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "1.23"
- name: Setup Python # This is for the build script
uses: actions/setup-python@v5
with:
python-version: "3.11"
- uses: nttld/setup-ndk@v1
id: setup-ndk
with:
ndk-version: r26b
add-to-path: false
- name: Run build script
env:
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
run: |
export HY_APP_PLATFORMS=$(sed 's/\r$//' platforms.txt | awk '!/^#/ && !/^$/' | paste -sd ",")
python hyperbole.py build -r
- name: Generate hashes
run: |
for file in build/*; do
sha256sum $file >> build/hashes.txt
done
- name: Archive
uses: actions/upload-artifact@v4
with:
name: hysteria-master-${{ github.sha }}
path: build

View File

@@ -0,0 +1,71 @@
name: "Build release"
on:
push:
tags:
- app/v*.*.*
jobs:
build:
name: Build
runs-on: ubuntu-latest
env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: true
steps:
- name: Check out
uses: actions/checkout@v4
- name: Get version
id: get_version
run: echo "version=$(git describe --tags --always --match 'app/v*' | sed -n 's|app/\([^/-]*\)\(-.*\)\{0,1\}|\1|p')" >> $GITHUB_OUTPUT
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "1.23"
- name: Setup Python # This is for the build script
uses: actions/setup-python@v5
with:
python-version: "3.11"
- uses: nttld/setup-ndk@v1
id: setup-ndk
with:
ndk-version: r26b
add-to-path: false
- name: Run build script
env:
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
run: |
export HY_APP_PLATFORMS=$(sed 's/\r$//' platforms.txt | awk '!/^#/ && !/^$/' | paste -sd ",")
python hyperbole.py build -r
- name: Generate hashes
run: |
for file in build/*; do
sha256sum $file >> build/hashes.txt
done
- name: Upload GitHub
uses: softprops/action-gh-release@v2
with:
files: build/*
- name: Upload CF bucket
uses: shallwefootball/upload-s3-action@v1.3.3
with:
aws_key_id: ${{ secrets.CF_KEY_ID }}
aws_secret_access_key: ${{ secrets.CF_KEY }}
aws_bucket: "hydownload"
endpoint: "https://bea223c61d5a41250d127bd67f51dfec.r2.cloudflarestorage.com/"
source_dir: "build"
destination_dir: "app/${{ steps.get_version.outputs.version }}"
- name: Publish to API
run: |
export HY_API_POST_KEY=${{ secrets.HY2_API_POST_KEY }}
pip install requests
python hyperbole.py publish

View File

@@ -0,0 +1,29 @@
name: "Publish scripts"
on:
push:
branches:
- master
paths:
- scripts/**
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
name: Publish scripts to Cloudflare Pages
steps:
- name: Check out
uses: actions/checkout@v4
- name: Publish to Cloudflare Pages
uses: cloudflare/pages-action@v1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: hy2scripts
directory: scripts
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
branch: main

470
v2rayng/hysteria/.gitignore vendored Normal file
View File

@@ -0,0 +1,470 @@
# Created by https://www.toptal.com/developers/gitignore/api/goland+all,intellij+all,go,windows,linux,macos,python,pycharm+all
# Edit at https://www.toptal.com/developers/gitignore?templates=goland+all,intellij+all,go,windows,linux,macos,python,pycharm+all
### Go ###
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
### GoLand+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### GoLand+all Patch ###
# Ignore everything but code style settings and run configurations
# that are supposed to be shared within teams.
.idea/*
!.idea/codeStyles
!.idea/runConfigurations
### Intellij+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
# AWS User-specific
# Generated files
# Sensitive or high-churn files
# Gradle
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
# Mongo Explorer plugin
# File-based project format
# IntelliJ
# mpeltonen/sbt-idea plugin
# JIRA plugin
# Cursive Clojure plugin
# SonarLint plugin
# Crashlytics plugin (for Android Studio and IntelliJ)
# Editor-based Rest Client
# Android studio 3.1+ serialized cache file
### Intellij+all Patch ###
# Ignore everything but code style settings and run configurations
# that are supposed to be shared within teams.
### Linux ###
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### macOS Patch ###
# iCloud generated files
*.icloud
### PyCharm+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
# AWS User-specific
# Generated files
# Sensitive or high-churn files
# Gradle
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
# Mongo Explorer plugin
# File-based project format
# IntelliJ
# mpeltonen/sbt-idea plugin
# JIRA plugin
# Cursive Clojure plugin
# SonarLint plugin
# Crashlytics plugin (for Android Studio and IntelliJ)
# Editor-based Rest Client
# Android studio 3.1+ serialized cache file
### PyCharm+all Patch ###
# Ignore everything but code style settings and run configurations
# that are supposed to be shared within teams.
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
### Python Patch ###
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
poetry.toml
# ruff
.ruff_cache/
# LSP config files
pyrightconfig.json
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# End of https://www.toptal.com/developers/gitignore/api/goland+all,intellij+all,go,windows,linux,macos,python,pycharm+all

View File

@@ -0,0 +1,3 @@
# Changelog
https://v2.hysteria.network/docs/Changelog/

View File

@@ -0,0 +1,39 @@
FROM golang:1-alpine AS builder
# GOPROXY is disabled by default, use:
# docker build --build-arg GOPROXY="https://goproxy.io" ...
# to enable GOPROXY.
ARG GOPROXY=""
ENV GOPROXY ${GOPROXY}
COPY . /go/src/github.com/apernet/hysteria
WORKDIR /go/src/github.com/apernet/hysteria
RUN set -ex \
&& apk add git build-base bash python3 \
&& python hyperbole.py build -r \
&& mv ./build/hysteria-* /go/bin/hysteria
# multi-stage builds to create the final image
FROM alpine AS dist
# set up nsswitch.conf for Go's "netgo" implementation
# - https://github.com/golang/go/blob/go1.9.1/src/net/conf.go#L194-L275
# - docker run --rm debian:stretch grep '^hosts:' /etc/nsswitch.conf
RUN if [ ! -e /etc/nsswitch.conf ]; then echo 'hosts: files dns' > /etc/nsswitch.conf; fi
# bash is used for debugging, tzdata is used to add timezone information.
# Install ca-certificates to ensure no CA certificate errors.
#
# Do not try to add the "--no-cache" option when there are multiple "apk"
# commands, this will cause the build process to become very slow.
RUN set -ex \
&& apk upgrade \
&& apk add bash tzdata ca-certificates \
&& rm -rf /var/cache/apk/*
COPY --from=builder /go/bin/hysteria /usr/local/bin/hysteria
ENTRYPOINT ["hysteria"]

View File

@@ -0,0 +1,7 @@
Copyright 2023 Toby
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,153 @@
# Hysteria 2 Protocol Specification
Hysteria is a TCP & UDP proxy based on QUIC, designed for speed, security and censorship resistance. This document describes the protocol used by Hysteria starting with version 2.0.0, sometimes internally referred to as the "v4" protocol. From here on, we will call it "the protocol" or "the Hysteria protocol".
## Requirements Language
The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://tools.ietf.org/html/rfc2119).
## Underlying Protocol & Wire Format
The Hysteria protocol MUST be implemented on top of the standard QUIC transport protocol [RFC 9000](https://datatracker.ietf.org/doc/html/rfc9000) with [Unreliable Datagram Extension](https://datatracker.ietf.org/doc/rfc9221/).
All multibyte numbers use Big Endian format.
All variable-length integers ("varints") are encoded/decoded as defined in QUIC (RFC 9000).
## Authentication & HTTP/3 masquerading
One of the key features of the Hysteria protocol is that to a third party without proper authentication credentials (whether it's a middleman or an active prober), a Hysteria proxy server behaves just like a standard HTTP/3 web server. Additionally, the encrypted traffic between the client and the server appears indistinguishable from normal HTTP/3 traffic.
Therefore, a Hysteria server MUST implement an HTTP/3 server (as defined by [RFC 9114](https://datatracker.ietf.org/doc/rfc9114/)) and handle HTTP requests as any standard web server would. To prevent active probers from detecting common response patterns in Hysteria servers, implementations SHOULD advise users to either host actual content or set it up as a reverse proxy for other sites.
An actual Hysteria client, upon connection, MUST send the following HTTP/3 request to the server:
```
:method: POST
:path: /auth
:host: hysteria
Hysteria-Auth: [string]
Hysteria-CC-RX: [uint]
Hysteria-Padding: [string]
```
`Hysteria-Auth`: Authentication credentials.
`Hysteria-CC-RX`: Client's maximum receive rate in bytes per second. A value of 0 indicates unknown.
`Hysteria-Padding`: A random padding string of variable length.
The Hysteria server MUST identify this special request, and, instead of attempting to serve content or forwarding it to an upstream site, it MUST authenticate the client using the provided information. If authentication is successful, the server MUST send the following response (HTTP status code 233):
```
:status: 233 HyOK
Hysteria-UDP: [true/false]
Hysteria-CC-RX: [uint/"auto"]
Hysteria-Padding: [string]
```
`Hysteria-UDP`: Whether the server supports UDP relay.
`Hysteria-CC-RX`: Server's maximum receive rate in bytes per second. A value of 0 indicates unlimited; "auto" indicates the server refuses to provide a value and ask the client to use congestion control to determine the rate on its own.
`Hysteria-Padding`: A random padding string of variable length.
See the Congestion Control section for more information on how to use the `Hysteria-CC-RX` values.
`Hysteria-Padding` is optional and is only intended to obfuscate the request/response pattern. It SHOULD be ignored by both sides.
If authentication fails, the server MUST either act like a standard web server that does not understand the request, or in the case of being a reverse proxy, forward the request to the upstream site and return the response to the client.
The client MUST check the status code to determine if the authentication was successful. If the status code is anything other than 233, the client MUST consider authentication to have failed and disconnect from the server.
After (and only after) a client passes authentication, the server MUST consider this QUIC connection to be a Hysteria proxy connection. It MUST then start processing proxy requests from the client as described in the next section.
## Proxy Requests
### TCP
For each TCP connection, the client MUST create a new QUIC bidirectional stream and send the following TCPRequest message:
```
[varint] 0x401 (TCPRequest ID)
[varint] Address length
[bytes] Address string (host:port)
[varint] Padding length
[bytes] Random padding
```
The server MUST respond with a TCPResponse message:
```
[uint8] Status (0x00 = OK, 0x01 = Error)
[varint] Message length
[bytes] Message string
[varint] Padding length
[bytes] Random padding
```
If the status is OK, the server MUST then begin forwarding data between the client and the specified TCP address until either side closes the connection. If the status is Error, the server MUST close the QUIC stream.
### UDP
UDP packets MUST be encapsulated in the following UDPMessage format and sent over QUIC's unreliable datagram (for both client-to-server and server-to-client):
```
[uint32] Session ID
[uint16] Packet ID
[uint8] Fragment ID
[uint8] Fragment count
[varint] Address length
[bytes] Address string (host:port)
[bytes] Payload
```
The client MUST use a unique Session ID for each UDP session. The server SHOULD assign a unique UDP port to each Session ID, unless it has another mechanism to differentiate packets from different sessions (e.g., symmetric NAT, varying outbound IP addresses, etc.).
The protocol does not provide an explicit way to close a UDP session. While a client can retain and reuse a Session ID indefinitely, the server SHOULD release and reassign the port associated with the Session ID after a period of inactivity or some other criteria. If the client sends a UDP packet to a Session ID that is no longer recognized by the server, the server MUST treat it as a new session and assign a new port.
If a server does not support UDP relay, it SHOULD silently discard all UDP messages received from the client.
#### Fragmentation
Due to the limit imposed by QUIC's unreliable datagram channel, any UDP packet that exceeds QUIC's maximum datagram size MUST either be fragmented or discarded.
For fragmented packets, each fragment MUST carry the same unique Packet ID. The Fragment ID, starting from 0, indicates the index out of the total Fragment Count. Both the server and client MUST wait for all fragments of a fragmented packet to arrive before processing them. If one or more fragments of a packet are lost, the entire packet MUST be discarded.
For packets that are not fragmented, the Fragment Count MUST be set to 1. In this case, the values of Packet ID and Fragment ID are irrelevant.
## Congestion Control
A unique feature of Hysteria is the ability to set the tx/rx (upload/download) rate on the client side. During authentication, the client sends its rx rate to the server via the `Hysteria-CC-RX` header. The server can use this to determine its transmission rate to the client, and vice versa by returning its rx rate to the client through the same header.
Three special cases are:
- If the client sends 0, it doesn't know its own rx rate. The server MUST use a congestion control algorithm (e.g., BBR, Cubic) to adjust its transmission rate.
- If the server responds with 0, it has no bandwidth limit. The client MAY transmit at any rate it wants.
- If the server responds with "auto", it chooses not to specify a rate. The client MUST use a congestion control algorithm to adjust its transmission rate.
## "Salamander" Obfuscation
The Hysteria protocol supports an optional obfuscation layer codenamed "Salamander".
"Salamander" encapsulates all QUIC packets in the following format:
```
[8 bytes] Salt
[bytes] Payload
```
For each QUIC packet, the obfuscator MUST calculate the BLAKE2b-256 hash of a randomly generated 8-byte salt appended to a user-provided pre-shared key.
```
hash = BLAKE2b-256(key + salt)
```
The hash is then used to obfuscate the payload using the following algorithm:
```
for i in range(0, len(payload)):
payload[i] ^= hash[i % 32]
```
The deobfuscator MUST use the same algorithms to calculate the salted hash and deobfuscate the payload. Any invalid packet MUST be discarded.

View File

@@ -0,0 +1,60 @@
# ![Hysteria 2](logo.svg)
[![License][1]][2] [![Release][3]][4] [![Telegram][5]][6] [![Discussions][7]][8]
[1]: https://img.shields.io/badge/license-MIT-blue
[2]: LICENSE.md
[3]: https://img.shields.io/github/v/release/apernet/hysteria?style=flat-square
[4]: https://github.com/apernet/hysteria/releases
[5]: https://img.shields.io/badge/chat-Telegram-blue?style=flat-square
[6]: https://t.me/hysteria_github
[7]: https://img.shields.io/github/discussions/apernet/hysteria?style=flat-square
[8]: https://github.com/apernet/hysteria/discussions
<h2 style="text-align: center;">Hysteria is a powerful, lightning fast and censorship resistant proxy.</h2>
### [Get Started](https://v2.hysteria.network/)
### [中文文档](https://v2.hysteria.network/zh/)
### [Hysteria 1.x (legacy)](https://v1.hysteria.network/)
---
<div class="feature-grid">
<div>
<h3>🛠️ Jack of all trades</h3>
<p>Wide range of modes including SOCKS5, HTTP Proxy, TCP/UDP Forwarding, Linux TProxy, TUN - with more features being added constantly.</p>
</div>
<div>
<h3>⚡ Blazing fast</h3>
<p>Powered by a customized QUIC protocol, Hysteria is designed to deliver unparalleled performance over unreliable and lossy networks.</p>
</div>
<div>
<h3>✊ Censorship resistant</h3>
<p>The protocol masquerades as standard HTTP/3 traffic, making it very difficult for censors to detect and block without widespread collateral damage.</p>
</div>
<div>
<h3>💻 Cross-platform</h3>
<p>We have builds for every major platform and architecture. Deploy anywhere & use everywhere. Not to mention the long list of 3rd party apps.</p>
</div>
<div>
<h3>🔗 Easy integration</h3>
<p>With built-in support for custom authentication, traffic statistics & access control, Hysteria is easy to integrate into your infrastructure.</p>
</div>
<div>
<h3>🤗 Chill and supportive</h3>
<p>We have well-documented specifications and code for developers to contribute and/or build their own apps. And a helpful community, too.</p>
</div>
</div>
---
**If you find Hysteria useful, consider giving it a ⭐️!**
[![Star History Chart](https://api.star-history.com/svg?repos=apernet/hysteria&type=Date)](https://star-history.com/#apernet/hysteria&Date)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,204 @@
package cmd
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/spf13/viper"
)
// TestClientConfig tests the parsing of the client config
func TestClientConfig(t *testing.T) {
viper.SetConfigFile("client_test.yaml")
err := viper.ReadInConfig()
assert.NoError(t, err)
var config clientConfig
err = viper.Unmarshal(&config)
assert.NoError(t, err)
assert.Equal(t, config, clientConfig{
Server: "example.com",
Auth: "weak_ahh_password",
Transport: clientConfigTransport{
Type: "udp",
UDP: clientConfigTransportUDP{
HopInterval: 30 * time.Second,
},
},
Obfs: clientConfigObfs{
Type: "salamander",
Salamander: clientConfigObfsSalamander{
Password: "cry_me_a_r1ver",
},
},
TLS: clientConfigTLS{
SNI: "another.example.com",
Insecure: true,
PinSHA256: "114515DEADBEEF",
CA: "custom_ca.crt",
},
QUIC: clientConfigQUIC{
InitStreamReceiveWindow: 1145141,
MaxStreamReceiveWindow: 1145142,
InitConnectionReceiveWindow: 1145143,
MaxConnectionReceiveWindow: 1145144,
MaxIdleTimeout: 10 * time.Second,
KeepAlivePeriod: 4 * time.Second,
DisablePathMTUDiscovery: true,
Sockopts: clientConfigQUICSockopts{
BindInterface: stringRef("eth0"),
FirewallMark: uint32Ref(1234),
FdControlUnixSocket: stringRef("test.sock"),
},
},
Bandwidth: clientConfigBandwidth{
Up: "200 mbps",
Down: "1 gbps",
},
FastOpen: true,
Lazy: true,
SOCKS5: &socks5Config{
Listen: "127.0.0.1:1080",
Username: "anon",
Password: "bro",
DisableUDP: true,
},
HTTP: &httpConfig{
Listen: "127.0.0.1:8080",
Username: "qqq",
Password: "bruh",
Realm: "martian",
},
TCPForwarding: []tcpForwardingEntry{
{
Listen: "127.0.0.1:8088",
Remote: "internal.example.com:80",
},
},
UDPForwarding: []udpForwardingEntry{
{
Listen: "127.0.0.1:5353",
Remote: "internal.example.com:53",
Timeout: 50 * time.Second,
},
},
TCPTProxy: &tcpTProxyConfig{
Listen: "127.0.0.1:2500",
},
UDPTProxy: &udpTProxyConfig{
Listen: "127.0.0.1:2501",
Timeout: 20 * time.Second,
},
TCPRedirect: &tcpRedirectConfig{
Listen: "127.0.0.1:3500",
},
TUN: &tunConfig{
Name: "hytun",
MTU: 1500,
Timeout: 60 * time.Second,
Address: struct {
IPv4 string `mapstructure:"ipv4"`
IPv6 string `mapstructure:"ipv6"`
}{IPv4: "100.100.100.101/30", IPv6: "2001::ffff:ffff:ffff:fff1/126"},
Route: &struct {
Strict bool `mapstructure:"strict"`
IPv4 []string `mapstructure:"ipv4"`
IPv6 []string `mapstructure:"ipv6"`
IPv4Exclude []string `mapstructure:"ipv4Exclude"`
IPv6Exclude []string `mapstructure:"ipv6Exclude"`
}{
Strict: true,
IPv4: []string{"0.0.0.0/0"},
IPv6: []string{"2000::/3"},
IPv4Exclude: []string{"192.0.2.1/32"},
IPv6Exclude: []string{"2001:db8::1/128"},
},
},
})
}
// TestClientConfigURI tests URI-related functions of clientConfig
func TestClientConfigURI(t *testing.T) {
tests := []struct {
uri string
uriOK bool
config *clientConfig
}{
{
uri: "hysteria2://god@zilla.jp/",
uriOK: true,
config: &clientConfig{
Server: "zilla.jp",
Auth: "god",
},
},
{
uri: "hysteria2://john:wick@continental.org:4443/",
uriOK: true,
config: &clientConfig{
Server: "continental.org:4443",
Auth: "john:wick",
},
},
{
uri: "hysteria2://saul@better.call:7000-10000,20000/",
uriOK: true,
config: &clientConfig{
Server: "better.call:7000-10000,20000",
Auth: "saul",
},
},
{
uri: "hysteria2://noauth.com/?insecure=1&obfs=salamander&obfs-password=66ccff&pinSHA256=deadbeef&sni=crap.cc",
uriOK: true,
config: &clientConfig{
Server: "noauth.com",
Auth: "",
Obfs: clientConfigObfs{
Type: "salamander",
Salamander: clientConfigObfsSalamander{
Password: "66ccff",
},
},
TLS: clientConfigTLS{
SNI: "crap.cc",
Insecure: true,
PinSHA256: "deadbeef",
},
},
},
{
uri: "invalid.bs",
uriOK: false,
config: nil,
},
{
uri: "https://www.google.com/search?q=test",
uriOK: false,
config: nil,
},
}
for _, test := range tests {
t.Run(test.uri, func(t *testing.T) {
// Test parseURI
nc := &clientConfig{Server: test.uri}
assert.Equal(t, nc.parseURI(), test.uriOK)
if test.uriOK {
assert.Equal(t, nc, test.config)
}
// Test URI generation
if test.config != nil {
assert.Equal(t, test.config.URI(), test.uri)
}
})
}
}
func stringRef(s string) *string {
return &s
}
func uint32Ref(i uint32) *uint32 {
return &i
}

View File

@@ -0,0 +1,85 @@
server: example.com
auth: weak_ahh_password
transport:
type: udp
udp:
hopInterval: 30s
obfs:
type: salamander
salamander:
password: cry_me_a_r1ver
tls:
sni: another.example.com
insecure: true
pinSHA256: 114515DEADBEEF
ca: custom_ca.crt
quic:
initStreamReceiveWindow: 1145141
maxStreamReceiveWindow: 1145142
initConnReceiveWindow: 1145143
maxConnReceiveWindow: 1145144
maxIdleTimeout: 10s
keepAlivePeriod: 4s
disablePathMTUDiscovery: true
sockopts:
bindInterface: eth0
fwmark: 1234
fdControlUnixSocket: test.sock
bandwidth:
up: 200 mbps
down: 1 gbps
fastOpen: true
lazy: true
socks5:
listen: 127.0.0.1:1080
username: anon
password: bro
disableUDP: true
http:
listen: 127.0.0.1:8080
username: qqq
password: bruh
realm: martian
tcpForwarding:
- listen: 127.0.0.1:8088
remote: internal.example.com:80
udpForwarding:
- listen: 127.0.0.1:5353
remote: internal.example.com:53
timeout: 50s
tcpTProxy:
listen: 127.0.0.1:2500
udpTProxy:
listen: 127.0.0.1:2501
timeout: 20s
tcpRedirect:
listen: 127.0.0.1:3500
tun:
name: "hytun"
mtu: 1500
timeout: 1m
address:
ipv4: 100.100.100.101/30
ipv6: 2001::ffff:ffff:ffff:fff1/126
route:
strict: true
ipv4: [ 0.0.0.0/0 ]
ipv6: [ "2000::/3" ]
ipv4Exclude: [ 192.0.2.1/32 ]
ipv6Exclude: [ "2001:db8::1/128" ]

View File

@@ -0,0 +1,18 @@
package cmd
import (
"fmt"
)
type configError struct {
Field string
Err error
}
func (e configError) Error() string {
return fmt.Sprintf("invalid config: %s: %s", e.Field, e.Err)
}
func (e configError) Unwrap() error {
return e.Err
}

View File

@@ -0,0 +1,63 @@
package cmd
import (
"time"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.uber.org/zap"
"github.com/apernet/hysteria/core/v2/client"
)
// pingCmd represents the ping command
var pingCmd = &cobra.Command{
Use: "ping address",
Short: "Ping mode",
Long: "Perform a TCP ping to a specified remote address through the proxy server. Can be used as a simple connectivity test.",
Run: runPing,
}
func init() {
rootCmd.AddCommand(pingCmd)
}
func runPing(cmd *cobra.Command, args []string) {
logger.Info("ping mode")
if len(args) != 1 {
logger.Fatal("must specify one and only one address")
}
addr := args[0]
if err := viper.ReadInConfig(); err != nil {
logger.Fatal("failed to read client config", zap.Error(err))
}
var config clientConfig
if err := viper.Unmarshal(&config); err != nil {
logger.Fatal("failed to parse client config", zap.Error(err))
}
hyConfig, err := config.Config()
if err != nil {
logger.Fatal("failed to load client config", zap.Error(err))
}
c, info, err := client.NewClient(hyConfig)
if err != nil {
logger.Fatal("failed to initialize client", zap.Error(err))
}
defer c.Close()
logger.Info("connected to server",
zap.Bool("udpEnabled", info.UDPEnabled),
zap.Uint64("tx", info.Tx))
logger.Info("connecting", zap.String("addr", addr))
start := time.Now()
conn, err := c.TCP(addr)
if err != nil {
logger.Fatal("failed to connect", zap.Error(err), zap.String("time", time.Since(start).String()))
}
defer conn.Close()
logger.Info("connected", zap.String("time", time.Since(start).String()))
}

View File

@@ -0,0 +1,176 @@
package cmd
import (
"fmt"
"os"
"strconv"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
const (
appLogo = `
░█░█░█░█░█▀▀░▀█▀░█▀▀░█▀▄░▀█▀░█▀█░░░▀▀▄
░█▀█░░█░░▀▀█░░█░░█▀▀░█▀▄░░█░░█▀█░░░▄▀░
░▀░▀░░▀░░▀▀▀░░▀░░▀▀▀░▀░▀░▀▀▀░▀░▀░░░▀▀▀
`
appDesc = "a powerful, lightning fast and censorship resistant proxy"
appAuthors = "Aperture Internet Laboratory <https://github.com/apernet>"
appLogLevelEnv = "HYSTERIA_LOG_LEVEL"
appLogFormatEnv = "HYSTERIA_LOG_FORMAT"
appDisableUpdateCheckEnv = "HYSTERIA_DISABLE_UPDATE_CHECK"
appACMEDirEnv = "HYSTERIA_ACME_DIR"
)
var (
// These values will be injected by the build system
appVersion = "Unknown"
appDate = "Unknown"
appType = "Unknown" // aka channel
appToolchain = "Unknown"
appCommit = "Unknown"
appPlatform = "Unknown"
appArch = "Unknown"
libVersion = "Unknown"
appVersionLong = fmt.Sprintf("Version:\t%s\n"+
"BuildDate:\t%s\n"+
"BuildType:\t%s\n"+
"Toolchain:\t%s\n"+
"CommitHash:\t%s\n"+
"Platform:\t%s\n"+
"Architecture:\t%s\n"+
"LibVersion:\t%s",
appVersion, appDate, appType, appToolchain, appCommit, appPlatform, appArch, libVersion)
appAboutLong = fmt.Sprintf("%s\n%s\n%s\n\n%s", appLogo, appDesc, appAuthors, appVersionLong)
)
var logger *zap.Logger
// Flags
var (
cfgFile string
logLevel string
logFormat string
disableUpdateCheck bool
)
var rootCmd = &cobra.Command{
Use: "hysteria",
Short: appDesc,
Long: appAboutLong,
Run: runClient, // Default to client mode
}
var logLevelMap = map[string]zapcore.Level{
"debug": zapcore.DebugLevel,
"info": zapcore.InfoLevel,
"warn": zapcore.WarnLevel,
"error": zapcore.ErrorLevel,
}
var logFormatMap = map[string]zapcore.EncoderConfig{
"console": {
TimeKey: "time",
LevelKey: "level",
NameKey: "logger",
MessageKey: "msg",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.CapitalColorLevelEncoder,
EncodeTime: zapcore.RFC3339TimeEncoder,
EncodeDuration: zapcore.SecondsDurationEncoder,
},
"json": {
TimeKey: "time",
LevelKey: "level",
NameKey: "logger",
MessageKey: "msg",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.LowercaseLevelEncoder,
EncodeTime: zapcore.EpochMillisTimeEncoder,
EncodeDuration: zapcore.SecondsDurationEncoder,
},
}
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
initFlags()
cobra.MousetrapHelpText = "" // Disable the mousetrap so Windows users can run the exe directly by double-clicking
cobra.OnInitialize(initConfig)
cobra.OnInitialize(initLogger) // initLogger must come after initConfig as it depends on config
}
func initFlags() {
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file")
rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "l", envOrDefaultString(appLogLevelEnv, "info"), "log level")
rootCmd.PersistentFlags().StringVarP(&logFormat, "log-format", "f", envOrDefaultString(appLogFormatEnv, "console"), "log format")
rootCmd.PersistentFlags().BoolVar(&disableUpdateCheck, "disable-update-check", envOrDefaultBool(appDisableUpdateCheckEnv, false), "disable update check")
}
func initConfig() {
if cfgFile != "" {
viper.SetConfigFile(cfgFile)
} else {
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.SupportedExts = append([]string{"yaml", "yml"}, viper.SupportedExts...)
viper.AddConfigPath(".")
viper.AddConfigPath("$HOME/.hysteria")
viper.AddConfigPath("/etc/hysteria/")
}
}
func initLogger() {
level, ok := logLevelMap[strings.ToLower(logLevel)]
if !ok {
fmt.Printf("unsupported log level: %s\n", logLevel)
os.Exit(1)
}
enc, ok := logFormatMap[strings.ToLower(logFormat)]
if !ok {
fmt.Printf("unsupported log format: %s\n", logFormat)
os.Exit(1)
}
c := zap.Config{
Level: zap.NewAtomicLevelAt(level),
DisableCaller: true,
DisableStacktrace: true,
Encoding: strings.ToLower(logFormat),
EncoderConfig: enc,
OutputPaths: []string{"stderr"},
ErrorOutputPaths: []string{"stderr"},
}
var err error
logger, err = c.Build()
if err != nil {
fmt.Printf("failed to initialize logger: %s\n", err)
os.Exit(1)
}
}
func envOrDefaultString(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
func envOrDefaultBool(key string, def bool) bool {
if v := os.Getenv(key); v != "" {
b, _ := strconv.ParseBool(v)
return b
}
return def
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,187 @@
package cmd
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/spf13/viper"
)
// TestServerConfig tests the parsing of the server config
func TestServerConfig(t *testing.T) {
viper.SetConfigFile("server_test.yaml")
err := viper.ReadInConfig()
assert.NoError(t, err)
var config serverConfig
err = viper.Unmarshal(&config)
assert.NoError(t, err)
assert.Equal(t, config, serverConfig{
Listen: ":8443",
Obfs: serverConfigObfs{
Type: "salamander",
Salamander: serverConfigObfsSalamander{
Password: "cry_me_a_r1ver",
},
},
TLS: &serverConfigTLS{
Cert: "some.crt",
Key: "some.key",
SNIGuard: "strict",
},
ACME: &serverConfigACME{
Domains: []string{
"sub1.example.com",
"sub2.example.com",
},
Email: "haha@cringe.net",
CA: "zero",
ListenHost: "127.0.0.9",
Dir: "random_dir",
Type: "dns",
HTTP: serverConfigACMEHTTP{
AltPort: 8888,
},
TLS: serverConfigACMETLS{
AltPort: 44333,
},
DNS: serverConfigACMEDNS{
Name: "gomommy",
Config: map[string]string{
"key1": "value1",
"key2": "value2",
},
},
DisableHTTP: true,
DisableTLSALPN: true,
AltHTTPPort: 8080,
AltTLSALPNPort: 4433,
},
QUIC: serverConfigQUIC{
InitStreamReceiveWindow: 77881,
MaxStreamReceiveWindow: 77882,
InitConnectionReceiveWindow: 77883,
MaxConnectionReceiveWindow: 77884,
MaxIdleTimeout: 999 * time.Second,
MaxIncomingStreams: 256,
DisablePathMTUDiscovery: true,
},
Bandwidth: serverConfigBandwidth{
Up: "500 mbps",
Down: "100 mbps",
},
IgnoreClientBandwidth: true,
SpeedTest: true,
DisableUDP: true,
UDPIdleTimeout: 120 * time.Second,
Auth: serverConfigAuth{
Type: "password",
Password: "goofy_ahh_password",
UserPass: map[string]string{
"yolo": "swag",
"lol": "kek",
"foo": "bar",
},
HTTP: serverConfigAuthHTTP{
URL: "http://127.0.0.1:5000/auth",
Insecure: true,
},
Command: "/etc/some_command",
},
Resolver: serverConfigResolver{
Type: "udp",
TCP: serverConfigResolverTCP{
Addr: "123.123.123.123:5353",
Timeout: 4 * time.Second,
},
UDP: serverConfigResolverUDP{
Addr: "4.6.8.0:53",
Timeout: 2 * time.Second,
},
TLS: serverConfigResolverTLS{
Addr: "dot.yolo.com:8853",
Timeout: 10 * time.Second,
SNI: "server1.yolo.net",
Insecure: true,
},
HTTPS: serverConfigResolverHTTPS{
Addr: "cringe.ahh.cc",
Timeout: 5 * time.Second,
SNI: "real.stuff.net",
Insecure: true,
},
},
Sniff: serverConfigSniff{
Enable: true,
Timeout: 1 * time.Second,
RewriteDomain: true,
TCPPorts: "80,443,1000-2000",
UDPPorts: "443",
},
ACL: serverConfigACL{
File: "chnroute.txt",
Inline: []string{
"lmao(ok)",
"kek(cringe,boba,tea)",
},
GeoIP: "some.dat",
GeoSite: "some_site.dat",
GeoUpdateInterval: 168 * time.Hour,
},
Outbounds: []serverConfigOutboundEntry{
{
Name: "goodstuff",
Type: "direct",
Direct: serverConfigOutboundDirect{
Mode: "64",
BindIPv4: "2.4.6.8",
BindIPv6: "0:0:0:0:0:ffff:0204:0608",
BindDevice: "eth233",
},
},
{
Name: "badstuff",
Type: "socks5",
SOCKS5: serverConfigOutboundSOCKS5{
Addr: "shady.proxy.ru:1080",
Username: "hackerman",
Password: "Elliot Alderson",
},
},
{
Name: "weirdstuff",
Type: "http",
HTTP: serverConfigOutboundHTTP{
URL: "https://eyy.lmao:4443/goofy",
Insecure: true,
},
},
},
TrafficStats: serverConfigTrafficStats{
Listen: ":9999",
Secret: "its_me_mario",
},
Masquerade: serverConfigMasquerade{
Type: "proxy",
File: serverConfigMasqueradeFile{
Dir: "/www/masq",
},
Proxy: serverConfigMasqueradeProxy{
URL: "https://some.site.net",
RewriteHost: true,
},
String: serverConfigMasqueradeString{
Content: "aint nothin here",
Headers: map[string]string{
"content-type": "text/plain",
"custom-haha": "lol",
},
StatusCode: 418,
},
ListenHTTP: ":80",
ListenHTTPS: ":443",
ForceHTTPS: true,
},
})
}

View File

@@ -0,0 +1,142 @@
listen: :8443
obfs:
type: salamander
salamander:
password: cry_me_a_r1ver
tls:
cert: some.crt
key: some.key
sniGuard: strict
acme:
domains:
- sub1.example.com
- sub2.example.com
email: haha@cringe.net
ca: zero
listenHost: 127.0.0.9
dir: random_dir
type: dns
http:
altPort: 8888
tls:
altPort: 44333
dns:
name: gomommy
config:
key1: value1
key2: value2
disableHTTP: true
disableTLSALPN: true
altHTTPPort: 8080
altTLSALPNPort: 4433
quic:
initStreamReceiveWindow: 77881
maxStreamReceiveWindow: 77882
initConnReceiveWindow: 77883
maxConnReceiveWindow: 77884
maxIdleTimeout: 999s
maxIncomingStreams: 256
disablePathMTUDiscovery: true
bandwidth:
up: 500 mbps
down: 100 mbps
ignoreClientBandwidth: true
speedTest: true
disableUDP: true
udpIdleTimeout: 120s
auth:
type: password
password: goofy_ahh_password
userpass:
yolo: swag
lol: kek
foo: bar
http:
url: http://127.0.0.1:5000/auth
insecure: true
command: /etc/some_command
resolver:
type: udp
tcp:
addr: 123.123.123.123:5353
timeout: 4s
udp:
addr: 4.6.8.0:53
timeout: 2s
tls:
addr: dot.yolo.com:8853
timeout: 10s
sni: server1.yolo.net
insecure: true
https:
addr: cringe.ahh.cc
timeout: 5s
sni: real.stuff.net
insecure: true
sniff:
enable: true
timeout: 1s
rewriteDomain: true
tcpPorts: 80,443,1000-2000
udpPorts: 443
acl:
file: chnroute.txt
inline:
- lmao(ok)
- kek(cringe,boba,tea)
geoip: some.dat
geosite: some_site.dat
geoUpdateInterval: 168h
outbounds:
- name: goodstuff
type: direct
direct:
mode: 64
bindIPv4: 2.4.6.8
bindIPv6: 0:0:0:0:0:ffff:0204:0608
bindDevice: eth233
- name: badstuff
type: socks5
socks5:
addr: shady.proxy.ru:1080
username: hackerman
password: Elliot Alderson
- name: weirdstuff
type: http
http:
url: https://eyy.lmao:4443/goofy
insecure: true
trafficStats:
listen: :9999
secret: its_me_mario
masquerade:
type: proxy
file:
dir: /www/masq
proxy:
url: https://some.site.net
rewriteHost: true
string:
content: aint nothin here
headers:
content-type: text/plain
custom-haha: lol
statusCode: 418
listenHTTP: :80
listenHTTPS: :443
forceHTTPS: true

View File

@@ -0,0 +1,55 @@
package cmd
import (
"fmt"
"github.com/apernet/hysteria/app/v2/internal/utils"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.uber.org/zap"
)
var (
noText bool
withQR bool
)
// shareCmd represents the share command
var shareCmd = &cobra.Command{
Use: "share",
Short: "Generate share URI",
Long: "Generate a hysteria2:// URI from a client config for sharing",
Run: runShare,
}
func init() {
initShareFlags()
rootCmd.AddCommand(shareCmd)
}
func initShareFlags() {
shareCmd.Flags().BoolVar(&noText, "notext", false, "do not show URI as text")
shareCmd.Flags().BoolVar(&withQR, "qr", false, "show URI as QR code")
}
func runShare(cmd *cobra.Command, args []string) {
if err := viper.ReadInConfig(); err != nil {
logger.Fatal("failed to read client config", zap.Error(err))
}
var config clientConfig
if err := viper.Unmarshal(&config); err != nil {
logger.Fatal("failed to parse client config", zap.Error(err))
}
if _, err := config.Config(); err != nil {
logger.Fatal("failed to load client config", zap.Error(err))
}
u := config.URI()
if !noText {
fmt.Println(u)
}
if withQR {
utils.PrintQR(u)
}
}

View File

@@ -0,0 +1,178 @@
package cmd
import (
"errors"
"fmt"
"os"
"os/signal"
"syscall"
"time"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.uber.org/zap"
"github.com/apernet/hysteria/core/v2/client"
hyErrors "github.com/apernet/hysteria/core/v2/errors"
"github.com/apernet/hysteria/extras/v2/outbounds"
"github.com/apernet/hysteria/extras/v2/outbounds/speedtest"
)
var (
skipDownload bool
skipUpload bool
dataSize uint32
useBytes bool
speedtestAddr = fmt.Sprintf("%s:%d", outbounds.SpeedtestDest, 0)
)
// speedtestCmd represents the speedtest command
var speedtestCmd = &cobra.Command{
Use: "speedtest",
Short: "Speed test mode",
Long: "Perform a speed test through the proxy server. The server must have speed test support enabled.",
Run: runSpeedtest,
}
func init() {
initSpeedtestFlags()
rootCmd.AddCommand(speedtestCmd)
}
func initSpeedtestFlags() {
speedtestCmd.Flags().BoolVar(&skipDownload, "skip-download", false, "Skip download test")
speedtestCmd.Flags().BoolVar(&skipUpload, "skip-upload", false, "Skip upload test")
speedtestCmd.Flags().Uint32Var(&dataSize, "data-size", 1024*1024*100, "Data size for download and upload tests")
speedtestCmd.Flags().BoolVar(&useBytes, "use-bytes", false, "Use bytes per second instead of bits per second")
}
func runSpeedtest(cmd *cobra.Command, args []string) {
logger.Info("speed test mode")
if err := viper.ReadInConfig(); err != nil {
logger.Fatal("failed to read client config", zap.Error(err))
}
var config clientConfig
if err := viper.Unmarshal(&config); err != nil {
logger.Fatal("failed to parse client config", zap.Error(err))
}
hyConfig, err := config.Config()
if err != nil {
logger.Fatal("failed to load client config", zap.Error(err))
}
c, info, err := client.NewClient(hyConfig)
if err != nil {
logger.Fatal("failed to initialize client", zap.Error(err))
}
defer c.Close()
logger.Info("connected to server",
zap.Bool("udpEnabled", info.UDPEnabled),
zap.Uint64("tx", info.Tx))
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
defer signal.Stop(signalChan)
runChan := make(chan struct{}, 1)
go func() {
if !skipDownload {
runDownloadTest(c)
}
if !skipUpload {
runUploadTest(c)
}
runChan <- struct{}{}
}()
select {
case <-signalChan:
logger.Info("received signal, shutting down gracefully")
case <-runChan:
logger.Info("speed test complete")
}
}
func runDownloadTest(c client.Client) {
logger.Info("performing download test")
downConn, err := c.TCP(speedtestAddr)
if err != nil {
if errors.As(err, &hyErrors.DialError{}) {
logger.Fatal("failed to connect (server may not support speed test)", zap.Error(err))
} else {
logger.Fatal("failed to connect", zap.Error(err))
}
}
defer downConn.Close()
downClient := &speedtest.Client{Conn: downConn}
currentTotal := uint32(0)
err = downClient.Download(dataSize, func(d time.Duration, b uint32, done bool) {
if !done {
currentTotal += b
logger.Info("downloading",
zap.Uint32("bytes", b),
zap.String("progress", fmt.Sprintf("%.2f%%", float64(currentTotal)/float64(dataSize)*100)),
zap.String("speed", formatSpeed(b, d, useBytes)))
} else {
logger.Info("download complete",
zap.Uint32("bytes", b),
zap.String("speed", formatSpeed(b, d, useBytes)))
}
})
if err != nil {
logger.Fatal("download test failed", zap.Error(err))
}
logger.Info("download test complete")
}
func runUploadTest(c client.Client) {
logger.Info("performing upload test")
upConn, err := c.TCP(speedtestAddr)
if err != nil {
if errors.As(err, &hyErrors.DialError{}) {
logger.Fatal("failed to connect (server may not support speed test)", zap.Error(err))
} else {
logger.Fatal("failed to connect", zap.Error(err))
}
}
defer upConn.Close()
upClient := &speedtest.Client{Conn: upConn}
currentTotal := uint32(0)
err = upClient.Upload(dataSize, func(d time.Duration, b uint32, done bool) {
if !done {
currentTotal += b
logger.Info("uploading",
zap.Uint32("bytes", b),
zap.String("progress", fmt.Sprintf("%.2f%%", float64(currentTotal)/float64(dataSize)*100)),
zap.String("speed", formatSpeed(b, d, useBytes)))
} else {
logger.Info("upload complete",
zap.Uint32("bytes", b),
zap.String("speed", formatSpeed(b, d, useBytes)))
}
})
if err != nil {
logger.Fatal("upload test failed", zap.Error(err))
}
logger.Info("upload test complete")
}
func formatSpeed(bytes uint32, duration time.Duration, useBytes bool) string {
speed := float64(bytes) / duration.Seconds()
var units []string
if useBytes {
units = []string{"B/s", "KB/s", "MB/s", "GB/s"}
} else {
units = []string{"bps", "Kbps", "Mbps", "Gbps"}
speed *= 8
}
unitIndex := 0
for speed > 1000 && unitIndex < len(units)-1 {
speed /= 1000
unitIndex++
}
return fmt.Sprintf("%.2f %s", speed, units[unitIndex])
}

View File

@@ -0,0 +1,88 @@
package cmd
import (
"time"
"github.com/spf13/cobra"
"go.uber.org/zap"
"github.com/apernet/hysteria/app/v2/internal/utils"
"github.com/apernet/hysteria/core/v2/client"
)
const (
updateCheckInterval = 24 * time.Hour
)
// checkUpdateCmd represents the checkUpdate command
var checkUpdateCmd = &cobra.Command{
Use: "check-update",
Short: "Check for updates",
Long: "Check for updates.",
Run: runCheckUpdate,
}
func init() {
rootCmd.AddCommand(checkUpdateCmd)
}
func runCheckUpdate(cmd *cobra.Command, args []string) {
logger.Info("checking for updates",
zap.String("version", appVersion),
zap.String("platform", appPlatform),
zap.String("arch", appArch),
zap.String("channel", appType),
)
checker := utils.NewServerUpdateChecker(appVersion, appPlatform, appArch, appType)
resp, err := checker.Check()
if err != nil {
logger.Fatal("failed to check for updates", zap.Error(err))
}
if resp.HasUpdate {
logger.Info("update available",
zap.String("version", resp.LatestVersion),
zap.String("url", resp.URL),
zap.Bool("urgent", resp.Urgent),
)
} else {
logger.Info("no update available")
}
}
// runCheckUpdateServer is the background update checking routine for server mode
func runCheckUpdateServer() {
checker := utils.NewServerUpdateChecker(appVersion, appPlatform, appArch, appType)
checkUpdateRoutine(checker)
}
// runCheckUpdateClient is the background update checking routine for client mode
func runCheckUpdateClient(hyClient client.Client) {
checker := utils.NewClientUpdateChecker(appVersion, appPlatform, appArch, appType, hyClient)
checkUpdateRoutine(checker)
}
func checkUpdateRoutine(checker *utils.UpdateChecker) {
ticker := time.NewTicker(updateCheckInterval)
for {
logger.Debug("checking for updates",
zap.String("version", appVersion),
zap.String("platform", appPlatform),
zap.String("arch", appArch),
zap.String("channel", appType),
)
resp, err := checker.Check()
if err != nil {
logger.Debug("failed to check for updates", zap.Error(err))
} else if resp.HasUpdate {
logger.Info("update available",
zap.String("version", resp.LatestVersion),
zap.String("url", resp.URL),
zap.Bool("urgent", resp.Urgent),
)
} else {
logger.Debug("no update available")
}
<-ticker.C
}
}

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