Update On Mon Aug 26 20:34:39 CEST 2024

This commit is contained in:
github-action[bot]
2024-08-26 20:34:40 +02:00
parent 2e77f8eb45
commit 4afce62e47
112 changed files with 1924 additions and 896 deletions
+1
View File
@@ -745,3 +745,4 @@ Update On Thu Aug 22 20:34:16 CEST 2024
Update On Fri Aug 23 20:31:39 CEST 2024
Update On Sat Aug 24 20:31:41 CEST 2024
Update On Sun Aug 25 20:30:10 CEST 2024
Update On Mon Aug 26 20:34:29 CEST 2024
+2
View File
@@ -207,6 +207,8 @@ jobs:
if: ${{ matrix.jobs.test == 'test' }}
run: |
go test ./...
echo "---test with_gvisor---"
go test ./... -tags "with_gvisor" -count=1
- name: Update CA
run: |
+20 -4
View File
@@ -7,6 +7,7 @@ import (
"fmt"
"net"
"net/netip"
"runtime"
"strconv"
"time"
@@ -14,6 +15,7 @@ import (
"github.com/metacubex/quic-go/congestion"
M "github.com/sagernet/sing/common/metadata"
CN "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/proxydialer"
@@ -43,6 +45,8 @@ type Hysteria struct {
option *HysteriaOption
client *core.Client
closeCh chan struct{} // for test
}
func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
@@ -51,7 +55,7 @@ func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata, opts .
return nil, err
}
return NewConn(tcpConn, h), nil
return NewConn(CN.NewRefConn(tcpConn, h), h), nil
}
func (h *Hysteria) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
@@ -59,7 +63,7 @@ func (h *Hysteria) ListenPacketContext(ctx context.Context, metadata *C.Metadata
if err != nil {
return nil, err
}
return newPacketConn(&hyPacketConn{udpConn}, h), nil
return newPacketConn(CN.NewRefPacketConn(&hyPacketConn{udpConn}, h), h), nil
}
func (h *Hysteria) genHdc(ctx context.Context, opts ...dialer.Option) utils.PacketDialer {
@@ -218,7 +222,7 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) {
if err != nil {
return nil, fmt.Errorf("hysteria %s create error: %w", addr, err)
}
return &Hysteria{
outbound := &Hysteria{
Base: &Base{
name: option.Name,
addr: addr,
@@ -231,7 +235,19 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) {
},
option: &option,
client: client,
}, nil
}
runtime.SetFinalizer(outbound, closeHysteria)
return outbound, nil
}
func closeHysteria(h *Hysteria) {
if h.client != nil {
_ = h.client.Close()
}
if h.closeCh != nil {
close(h.closeCh)
}
}
type hyPacketConn struct {
+5
View File
@@ -38,6 +38,8 @@ type Hysteria2 struct {
option *Hysteria2Option
client *hysteria2.Client
dialer proxydialer.SingDialer
closeCh chan struct{} // for test
}
type Hysteria2Option struct {
@@ -89,6 +91,9 @@ func closeHysteria2(h *Hysteria2) {
if h.client != nil {
_ = h.client.CloseWithError(errors.New("proxy removed"))
}
if h.closeCh != nil {
close(h.closeCh)
}
}
func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
@@ -0,0 +1,38 @@
package outbound
import (
"context"
"runtime"
"testing"
"time"
)
func TestHysteria2GC(t *testing.T) {
option := Hysteria2Option{}
option.Server = "127.0.0.1"
option.Ports = "200,204,401-429,501-503"
option.HopInterval = 30
option.Password = "password"
option.Obfs = "salamander"
option.ObfsPassword = "password"
option.SNI = "example.com"
option.ALPN = []string{"h3"}
hy, err := NewHysteria2(option)
if err != nil {
t.Error(err)
return
}
closeCh := make(chan struct{})
hy.closeCh = closeCh
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
hy = nil
runtime.GC()
select {
case <-closeCh:
return
case <-ctx.Done():
t.Error("timeout not GC")
}
}
@@ -0,0 +1,39 @@
package outbound
import (
"context"
"runtime"
"testing"
"time"
)
func TestHysteriaGC(t *testing.T) {
option := HysteriaOption{}
option.Server = "127.0.0.1"
option.Ports = "200,204,401-429,501-503"
option.Protocol = "udp"
option.Up = "1Mbps"
option.Down = "1Mbps"
option.HopInterval = 30
option.Obfs = "salamander"
option.SNI = "example.com"
option.ALPN = []string{"h3"}
hy, err := NewHysteria(option)
if err != nil {
t.Error(err)
return
}
closeCh := make(chan struct{})
hy.closeCh = closeCh
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
hy = nil
runtime.GC()
select {
case <-closeCh:
return
case <-ctx.Done():
t.Error("timeout not GC")
}
}
-2
View File
@@ -26,7 +26,6 @@ import (
wireguard "github.com/metacubex/sing-wireguard"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/debug"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
@@ -456,7 +455,6 @@ func closeWireGuard(w *WireGuard) {
if w.device != nil {
w.device.Close()
}
_ = common.Close(w.tunDevice)
if w.closeCh != nil {
close(w.closeCh)
}
@@ -29,6 +29,7 @@ func TestWireGuardGC(t *testing.T) {
err = wg.init(ctx)
if err != nil {
t.Error(err)
return
}
// must do a small sleep before test GC
// because it maybe deadlocks if w.device.Close call too fast after w.device.Start
+1 -1
View File
@@ -27,7 +27,7 @@ require (
github.com/metacubex/sing-shadowsocks2 v0.2.2
github.com/metacubex/sing-tun v0.2.7-0.20240729131039-ed03f557dee1
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9
github.com/metacubex/sing-wireguard v0.0.0-20240618022557-a6efaa37127a
github.com/metacubex/sing-wireguard v0.0.0-20240826061955-1e4e67afe5cd
github.com/metacubex/tfo-go v0.0.0-20240821025650-e9be0afd5e7d
github.com/metacubex/utls v1.6.6
github.com/miekg/dns v1.1.62
+2 -2
View File
@@ -120,8 +120,8 @@ github.com/metacubex/sing-tun v0.2.7-0.20240729131039-ed03f557dee1 h1:ypfofGDZbP
github.com/metacubex/sing-tun v0.2.7-0.20240729131039-ed03f557dee1/go.mod h1:olbEx9yVcaw5tHTNlRamRoxmMKcvDvcVS1YLnQGzvWE=
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 h1:OAXiCosqY8xKDp3pqTW3qbrCprZ1l6WkrXSFSCwyY4I=
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY=
github.com/metacubex/sing-wireguard v0.0.0-20240618022557-a6efaa37127a h1:NpSGclHJUYndUwBmyIpFBSoBVg8PoVX7QQKhYg0DjM0=
github.com/metacubex/sing-wireguard v0.0.0-20240618022557-a6efaa37127a/go.mod h1:uY+BYb0UEknLrqvbGcwi9i++KgrKxsurysgI6G1Pveo=
github.com/metacubex/sing-wireguard v0.0.0-20240826061955-1e4e67afe5cd h1:r7alry8u4qlUFLNMwGvG1A8ZcfPM6AMSmrm6E2yKdB4=
github.com/metacubex/sing-wireguard v0.0.0-20240826061955-1e4e67afe5cd/go.mod h1:uY+BYb0UEknLrqvbGcwi9i++KgrKxsurysgI6G1Pveo=
github.com/metacubex/tfo-go v0.0.0-20240821025650-e9be0afd5e7d h1:j9LtzkYstLFoNvXW824QQeN7Y26uPL5249kzWKbzO9U=
github.com/metacubex/tfo-go v0.0.0-20240821025650-e9be0afd5e7d/go.mod h1:c7bVFM9f5+VzeZ/6Kg77T/jrg1Xp8QpqlSHvG/aXVts=
github.com/metacubex/utls v1.6.6 h1:3D12YKHTf2Z41UPhQU2dWerNWJ5TVQD9gKoQ+H+iLC8=
+4
View File
@@ -101,6 +101,7 @@ func ApplyConfig(cfg *config.Config, force bool) {
updateNTP(cfg.NTP)
updateDNS(cfg.DNS, cfg.General.IPv6)
updateListeners(cfg.General, cfg.Listeners, force)
updateTun(cfg.General) // tun should not care "force"
updateIPTables(cfg)
updateTunnels(cfg.Tunnels)
@@ -198,6 +199,9 @@ func updateListeners(general *config.General, listeners map[string]C.InboundList
listener.ReCreateShadowSocks(general.ShadowSocksConfig, tunnel.Tunnel)
listener.ReCreateVmess(general.VmessConfig, tunnel.Tunnel)
listener.ReCreateTuic(general.TuicServer, tunnel.Tunnel)
}
func updateTun(general *config.General) {
listener.ReCreateTun(general.Tun, tunnel.Tunnel)
}
+4 -1
View File
@@ -289,7 +289,10 @@ func (c *Client) DialUDP(dialer utils.PacketDialer) (UDPConn, error) {
func (c *Client) Close() error {
c.reconnectMutex.Lock()
defer c.reconnectMutex.Unlock()
err := c.quicSession.CloseWithError(closeErrorCodeGeneric, "")
var err error
if c.quicSession != nil {
err = c.quicSession.CloseWithError(closeErrorCodeGeneric, "")
}
c.closed = true
return err
}
+8 -8
View File
@@ -620,9 +620,9 @@ dependencies = [
[[package]]
name = "backon"
version = "0.5.0"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33e5b65cc81d81fbb8488f36458ab4771be35a722967bbc959df28b47397e3ff"
checksum = "274cb2897ebb0ed1d2f70adb956fb0a0384aa208eea7f2845fb7aa6cd7f39277"
dependencies = [
"fastrand 2.1.0",
"tokio",
@@ -685,7 +685,7 @@ dependencies = [
"bitflags 2.6.0",
"cexpr",
"clang-sys",
"itertools 0.11.0",
"itertools 0.12.1",
"lazy_static 1.5.0",
"lazycell",
"log 0.4.22",
@@ -3229,7 +3229,7 @@ dependencies = [
"httpdate",
"itoa 1.0.11",
"pin-project-lite",
"socket2 0.4.10",
"socket2 0.5.7",
"tokio",
"tower-service",
"tracing",
@@ -4054,7 +4054,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
dependencies = [
"cfg-if",
"windows-targets 0.48.5",
"windows-targets 0.52.6",
]
[[package]]
@@ -6033,9 +6033,9 @@ checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175"
[[package]]
name = "redb"
version = "2.1.1"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6dd20d3cdeb9c7d2366a0b16b93b35b75aec15309fbeb7ce477138c9f68c8c0"
checksum = "58323dc32ea52a8ae105ff94bc0460c5d906307533ba3401aa63db3cbe491fe5"
dependencies = [
"libc",
]
@@ -8886,7 +8886,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -18,76 +18,79 @@ pub fn register<F: FnMut(String) + Send + 'static>(schemes: &[&str], handler: F)
create_dir_all(&target)?;
for (i, scheme) in schemes.iter().enumerate() {
let exe = tauri_utils::platform::current_exe()?;
let exe = tauri_utils::platform::current_exe()?;
let file_name = format!(
"{}-handler-{}.desktop",
exe.file_name()
.ok_or_else(|| Error::new(
ErrorKind::NotFound,
"Couldn't get file name of curent executable.",
))?
.to_string_lossy(),
i
);
let file_name = format!(
"{}-handler.desktop",
exe.file_name()
.ok_or_else(|| Error::new(
ErrorKind::NotFound,
"Couldn't get file name of curent executable.",
))?
.to_string_lossy()
);
target.push(&file_name);
target.push(&file_name);
let mime_types = format!("x-scheme-handler/{};", scheme);
let mime_types = format!(
"{};",
schemes
.iter()
.map(|s| format!("x-scheme-handler/{}", s))
.collect::<Vec<String>>()
.join(";")
);
let mut file = File::create(&target)?;
file.write_all(
format!(
include_str!("template.desktop"),
name = ID
.get()
.expect("Called register() before prepare()")
.split('.')
.last()
.unwrap(),
exec = std::env::var("APPIMAGE").unwrap_or_else(|_| exe.display().to_string()),
mime_types = mime_types
)
.as_bytes(),
)?;
let mut file = File::create(&target)?;
file.write_all(
format!(
include_str!("template.desktop"),
name = ID
.get()
.expect("Called register() before prepare()")
.split('.')
.last()
.unwrap(),
exec = std::env::var("APPIMAGE").unwrap_or_else(|_| exe.display().to_string()),
mime_types = mime_types
)
.as_bytes(),
)?;
Command::new("update-desktop-database")
.arg(&target)
.status()?;
Command::new("update-desktop-database")
.arg(&target)
.status()?;
for scheme in schemes {
Command::new("xdg-mime")
.args(["default", &file_name, scheme])
.status()?;
target.pop();
}
target.pop();
Ok(())
}
pub fn unregister(schemes: &[&str]) -> Result<()> {
pub fn unregister(_schemes: &[&str]) -> Result<()> {
let mut target =
data_dir().ok_or_else(|| Error::new(ErrorKind::NotFound, "data directory not found."))?;
target.push("applications");
for (i, _) in schemes.iter().enumerate() {
target.push(format!(
"{}-handler-{}.desktop",
tauri_utils::platform::current_exe()?
.file_name()
.ok_or_else(|| Error::new(
ErrorKind::NotFound,
"Couldn't get file name of curent executable.",
))?
.to_string_lossy(),
i
));
target.push(format!(
"{}-handler.desktop",
tauri_utils::platform::current_exe()?
.file_name()
.ok_or_else(|| Error::new(
ErrorKind::NotFound,
"Couldn't get file name of current executable.",
))?
.to_string_lossy()
));
remove_file(&target)?;
target.pop();
}
remove_file(&target)?;
target.pop();
Ok(())
}
@@ -1,5 +1,4 @@
use std::{
os::windows::thread,
path::Path,
sync::atomic::{AtomicU16, Ordering},
};
+1 -1
View File
@@ -76,7 +76,7 @@ rs-snowflake = "0.6"
thiserror = { workspace = true }
simd-json = "0.13.8"
runas = "1.2.0"
backon = { version = "0.5", features = ["tokio-sleep"] }
backon = { version = "1.0.1", features = ["tokio-sleep"] }
rust-i18n = "3"
adler = "1.0.2"
rfd = "0.10" # should bump to v0.14 when clarify why the rfd v0.10 from tauri breaks build
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

@@ -73,7 +73,7 @@ impl Proxies {
#[instrument]
pub async fn fetch() -> Result<Self> {
let (inner_proxies, providers_proxies) = fetch_proxies
.retry(&*CLASH_API_DEFAULT_BACKOFF_STRATEGY)
.retry(*CLASH_API_DEFAULT_BACKOFF_STRATEGY)
.await?;
let inner_proxies = inner_proxies.proxies;
// 1. filter out the Http or File type provider proxies
@@ -48,6 +48,8 @@ ${StrLoc}
!define UNINSTALLERSIGNCOMMAND "{{uninstaller_sign_cmd}}"
!define ESTIMATEDSIZE "{{estimated_size}}"
Var ProgramDataPathVar
Name "${PRODUCTNAME}"
BrandingText "${COPYRIGHT}"
OutFile "${OUTFILE}"
@@ -381,6 +383,23 @@ FunctionEnd
${EndIf}
!macroend
!define FOLDERID_ProgramData "{62AB5D82-FDC1-4DC3-A9DD-070D1D495D97}"
!macro GetProgramDataPath
; 调用SHGetKnownFolderIDList获取PIDL
System::Call 'shell32::SHGetKnownFolderIDList(g"${FOLDERID_ProgramData}", i0x1000, i0, *i.r1)i.r0'
${If} $0 = 0
; 调用SHGetPathFromIDList将PIDL转换为路径
System::Call 'shell32::SHGetPathFromIDList(ir1,t.r0)'
StrCpy $ProgramDataPathVar $0 ; 将结果保存到变量
; DetailPrint "ProgramData Path: $ProgramDataPathVar"
; 释放PIDL内存
System::Call 'ole32::CoTaskMemFree(ir1)'
${Else}
DetailPrint "Failed to get ProgramData path, error code: $0"
${EndIf}
!macroend
Var PassiveMode
Function .onInit
${GetOptions} $CMDLINE "/P" $PassiveMode
@@ -428,6 +447,7 @@ FunctionEnd
!endif
Pop $R0
${If} $R0 = 0
DetailPrint "${Process} is running"
IfSilent kill${ID} 0
${IfThen} $PassiveMode != 1 ${|} MessageBox MB_OKCANCEL "${Process} is running, ok to kill?" IDOK kill${ID} IDCANCEL cancel${ID} ${|}
kill${ID}:
@@ -468,9 +488,9 @@ FunctionEnd
!insertmacro CheckNyanpasuProcess "mihomo-alpha.exe" "6"
!macroend
Section CheckProcesses
!insertmacro CheckAllNyanpasuProcesses
SectionEnd
; Section CheckProcesses
; !insertmacro CheckAllNyanpasuProcesses
; SectionEnd
Section EarlyChecks
; Abort silent installer if downgrades is disabled
@@ -589,9 +609,30 @@ SectionEnd
; app_check_done:
; !macroend
Section Install
SetOutPath $INSTDIR
!macro StopCoreByService
; 构建服务可执行文件的完整路径
StrCpy $1 "$ProgramDataPathVar\nyanpasu-service\data\nyanpasu-service.exe"
; 检查文件是否存在
IfFileExists "$1" 0 SkipStopCore
; 文件存在,执行停止核心服务
nsExec::ExecToLog '"$1" rpc stop-core'
Pop $0 ; 弹出命令执行的返回值
${If} $0 == "0"
DetailPrint "Core service stopped successfully."
${Else}
DetailPrint "Core stop failed with exit code $0"
${EndIf}
SkipStopCore:
; 如果文件不存在,打印错误
DetailPrint "Nyanpasu Service is not installed, skipping stop-core"
!macroend
Section Install
!insertmacro GetProgramDataPath
!insertmacro StopCoreByService
SetOutPath $INSTDIR
!insertmacro CheckAllNyanpasuProcesses
; !insertmacro CheckIfAppIsRunning
@@ -708,7 +749,36 @@ FunctionEnd
${EndIf}
!macroend
!macro StopAndRemoveServiceDirectory
; 构建服务路径
StrCpy $1 "$ProgramDataPathVar\nyanpasu-service\data\nyanpasu-service.exe"
; 检查服务可执行文件是否存在
IfFileExists "$1" 0 Skip
nsExec::ExecToLog '"$1" stop'
Pop $0
DetailPrint "Stopping service with exit code $0"
; 检查停止服务是否成功(假设0, 100, 102为成功)
IntCmp $0 0 0 StopFailed StopFailed
IntCmp $0 100 0 StopFailed StopFailed
IntCmp $0 102 0 StopFailed StopFailed
StopFailed:
Abort "Failed to stop the service. Aborting installation."
; 如果服务成功停止,继续检查目录是否存在并删除
StrCpy $2 "$ProgramDataPathVar\nyanpasu-service"
IfFileExists "$2\*" 0 Skip
RMDir /r "$2"
DetailPrint "Removed service directory successfully"
Skip:
DetailPrint "Service directory does not exist, skipping stop and remove service directory"
!macroend
Section Uninstall
!insertmacro GetProgramDataPath
!insertmacro StopAndRemoveServiceDirectory
!insertmacro CheckAllNyanpasuProcesses
; !insertmacro CheckIfAppIsRunning
@@ -0,0 +1,36 @@
<!--
Copyright (C) 2022 The Project Nyanpasu
SPDX-License-Identifier: Apache-2.0
thanks: @ReallySnow
-->
<svg width="420px" height="490px" viewBox="0 0 420 490" version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<style><![CDATA[
#element {
fill: #000000;
}
]]></style>
</defs>
<g id="nyanpasu_logo" fill-rule="nonzero">
<path
d="M349.980734,470.820835 C362.906597,470.820835 375.380055,471.040616 387.808272,470.736801 C394.678368,470.562269 398.898662,465.526692 400.333433,457.394784 C402.20122,446.832354 400.417451,436.780592 395.809381,427.284747 C390.832924,417.03906 385.255414,407.090725 380.233716,396.903216 C360.528238,356.825451 356.463054,315.532425 371.896535,273.134028 C376.646789,260.076434 384.111475,248.479739 394.781775,239.255389 C400.869857,233.993566 405.684741,233.870747 409.911498,238.718864 C414.138255,243.56698 413.285148,248.415097 407.500824,253.663991 C390.535629,269.081003 384.285974,289.34613 382.230762,311.27901 C379.516331,340.367711 385.669042,367.633519 399.499715,393.373787 C407.100123,407.523823 416.070671,421.169656 418.88851,437.433471 C420.614112,447.317165 420.484854,457.129754 417.402035,466.787202 C412.613003,481.764651 401.244707,490.012913 385.513931,490 C273.567341,489.896558 161.622905,489.814679 49.6806226,489.754347 C42.5713979,489.754347 35.4621733,489.993521 28.605003,487.73753 C11.4136053,482.081394 -0.0322464095,467.259085 0,448.855634 C0.103475152,390.930336 0.374918274,333.011502 3.10227536,275.124988 C4.89250738,237.057576 7.30964376,199.054805 11.2067914,161.148996 C16.3771366,110.612228 23.6608604,60.405131 34.5315112,10.7604159 C35.0024707,8.67513302 35.7547542,6.66358964 36.7676855,4.78107191 C38.5191399,1.50374502 41.1689418,-0.513071536 45.1694964,0.113951558 C49.170051,0.740974653 49.3768648,4.11526388 49.9326769,6.9530282 C53.9913979,27.6706469 60.3056819,47.7612426 66.8590945,67.7742684 C68.9853989,74.238424 70.1875042,82.4543658 74.7891114,86.326395 C79.26146,90.0949977 87.6051045,87.3541957 94.2425352,87.3735882 C154.302557,87.5028713 214.369043,86.8176708 274.422602,88.3755323 C279.256875,88.4983513 281.615845,87.0827012 283.509484,82.6353621 C293.203881,59.8750702 301.967616,36.8044988 308.495177,12.925908 C309.309506,9.94593225 309.942874,6.778496 313.710763,6.46175237 C317.129653,6.17732953 319.346439,8.49796139 320.858765,11.1547293 C321.981523,13.2263513 322.777248,15.4591645 323.217735,17.7740247 C332.832735,64.3247661 340.022746,111.343788 344.758685,158.640904 C354.220417,251.724745 355.745669,345.112401 356.152834,438.538842 C356.152834,444.123872 356.217463,449.695975 355.370819,455.274541 C354.569415,460.646254 352.048872,465.38448 349.980734,470.820835 Z"
id="bg" fill="#ffffff" fill-rule="evenodd"></path>
<path
d="M314.373914,51.8580359 C319.16418,79.1645649 322.474053,106.509891 325.189183,133.926344 C335.474355,237.585109 337,341.580114 337,445.633314 C337,462.193111 328.052999,470.974135 311.283837,470.980602 C222.356854,471.006466 133.429871,471.006466 44.5028879,470.980602 C27.8242301,470.980602 18.9741981,462.050856 19,445.491059 C19.0776317,397.460537 19.1422776,349.423548 20.816608,301.425356 C22.7559868,246.3726 25.665055,191.384506 31.4637977,136.538667 C34.2500385,110.221458 37.8831415,84.0335719 41.044329,57.7745583 C41.348165,55.2398296 42.0398768,52.7503639 42.8026991,49 C47.5541772,66.3034032 53.702008,81.848015 58.6926762,97.7676631 C60.6902363,104.123883 64.0001095,106.632747 70.7620769,106.632747 C141.816607,106.58964 212.868982,106.72974 283.919202,107.053047 C290.11875,107.053047 293.415694,104.679972 295.555475,99.1643504 C301.63866,83.3804912 308.652746,67.9328716 314.373914,51.8580359 Z"
id="bg" fill="#ffffff" fill-rule="evenodd"></path>
<path
d="M119.999315,204.118735 C120.083729,213.985193 112.350174,222.057169 102.850433,222 C94.0584657,221.961379 86.9547636,214.451375 87,205.249067 C87.0456702,196.148935 94.5909186,188.268541 103.493273,188.006712 C112.395627,187.744884 119.921396,195.178255 119.999315,204.118735 Z"
id="element" fill-rule="evenodd"></path>
<path
d="M236,204.861743 C236,195.671258 243.035135,188.142475 251.816026,188.001869 C261.124552,187.861264 268.993481,195.671258 269,205.104607 C269.006509,214.537955 262.179823,221.772746 253.255623,221.996436 C244.16206,222.194562 236.026056,214.103357 236,204.861743 Z"
id="element" fill-rule="evenodd"></path>
<path
d="M190.493614,267.920884 C185.555364,268.142338 181.468093,262.430145 177.097162,262.137045 C172.945422,261.850458 168.716321,267.914371 162.875519,267.999044 C153.295574,268.096744 144.921179,260.691085 144.089542,251.064378 C143.851011,248.296212 143.844564,245.541072 147.312943,245.059085 C150.362279,244.622692 151.0263,246.902358 151.638746,249.481638 C153.327808,256.646305 157.286143,260.411011 162.701456,259.935538 C168.890385,259.394931 172.371657,255.649765 173.087252,249.468612 C173.383805,247.045652 174.234782,245.104678 176.819949,245.026518 C179.869286,244.935332 180.481732,247.208485 180.855647,249.813818 C181.751752,255.988458 185.426428,259.870405 191.570229,259.889945 C197.71403,259.909485 201.433834,255.981945 202.342833,249.872438 C202.774769,246.954465 203.516151,244.433805 206.907168,245.111192 C210.130569,245.762525 210.195037,248.634905 209.872697,251.383532 C208.750953,260.769245 200.621538,268.044638 190.493614,267.920884 Z"
id="element" fill-rule="evenodd"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

@@ -1,7 +1,7 @@
import dayjs from "dayjs";
import { useSetAtom } from "jotai";
import { useAtomValue, useSetAtom } from "jotai";
import { useEffect } from "react";
import { atomLogData } from "@/store";
import { atomEnableLog, atomLogData } from "@/store";
import { LogMessage, useClashWS } from "@nyanpasu/interface";
const MAX_LOG_NUM = 1000;
@@ -15,8 +15,10 @@ export const LogProvider = () => {
const setLogData = useSetAtom(atomLogData);
const enableLog = useAtomValue(atomEnableLog);
useEffect(() => {
if (!latestMessage?.data) {
if (!latestMessage?.data || !enableLog) {
return;
}
@@ -29,7 +31,7 @@ export const LogProvider = () => {
return [...prev, { ...data, time }];
});
}, [latestMessage?.data, setLogData]);
}, [enableLog, latestMessage?.data, setLogData]);
return null;
};
@@ -19,7 +19,7 @@ import { QuickImport } from "@/components/profiles/quick-import";
import RuntimeConfigDiffDialog from "@/components/profiles/runtime-config-diff-dialog";
import { filterProfiles } from "@/components/profiles/utils";
import { Public } from "@mui/icons-material";
import { Badge, Button, IconButton } from "@mui/material";
import { Badge, Button, IconButton, useTheme } from "@mui/material";
import Grid from "@mui/material/Unstable_Grid2";
import { Profile, useClash } from "@nyanpasu/interface";
import { SidePage } from "@nyanpasu/ui";
@@ -27,6 +27,7 @@ import { SidePage } from "@nyanpasu/ui";
export const ProfilePage = () => {
const { t } = useTranslation();
const { getProfiles, getRuntimeLogs } = useClash();
const theme = useTheme();
const maxLogLevelTriggered = useMemo(() => {
const currentProfileChains =
getProfiles.data?.items?.find(
@@ -110,17 +111,24 @@ export const ProfilePage = () => {
title={t("Profiles")}
flexReverse
header={
<div>
<div className="flex items-center gap-2">
<RuntimeConfigDiffDialog
open={runtimeConfigViewerOpen}
onClose={() => setRuntimeConfigViewerOpen(false)}
/>
<IconButton
className="h-10 w-10"
color="inherit"
title="Runtime Config"
onClick={() => {
setRuntimeConfigViewerOpen(true);
}}
>
<IconMdiTextBoxCheckOutline />
<IconMdiTextBoxCheckOutline
// style={{
// color: theme.palette.text.primary,
// }}
/>
</IconButton>
<Badge
variant="dot"
@@ -12,6 +12,7 @@ import ProxyGroupName from "@/components/proxies/proxy-group-name";
import ScrollCurrentNode from "@/components/proxies/scroll-current-node";
import SortSelector from "@/components/proxies/sort-selector";
import { proxyGroupAtom } from "@/store";
import { Check } from "@mui/icons-material";
import {
alpha,
Box,
@@ -71,13 +72,14 @@ export default function ProxyPage() {
return (
<Box display="flex" alignItems="center" gap={1}>
<ButtonGroup size="small">
{Object.entries(getCurrentMode).map(([key, value], index) => (
{Object.entries(getCurrentMode).map(([key, enabled]) => (
<Button
key={index}
variant={value ? "contained" : "outlined"}
key={key}
variant={enabled ? "contained" : "outlined"}
onClick={() => handleSwitch(key)}
sx={{ textTransform: "capitalize" }}
>
{enabled && <Check className="-ml-2 mr-[0.1rem] scale-75" />}
{t(key)}
</Button>
))}
@@ -1,4 +1,4 @@
import { MUI_BREAKPOINTS } from "@nyanpasu/ui";
import { MUI_BREAKPOINTS } from "@nyanpasu/ui/src/materialYou/themeConsts.mjs";
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable @typescript-eslint/no-require-imports */
+4 -2
View File
@@ -3,12 +3,14 @@
"version": "0.1.0",
"type": "module",
"exports": {
".": "./dist/index.js"
".": "./dist/index.js",
"./src/materialYou/themeConsts.mjs": "./src/materialYou/themeConsts.mjs"
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist"
"dist",
"src"
],
"scripts": {
"build": "vite build"
@@ -1,7 +1,7 @@
import { useAsyncEffect } from "ahooks";
import { useEffect, useState } from "react";
import { createBreakpoint } from "react-use";
import { MUI_BREAKPOINTS } from "../materialYou";
import { MUI_BREAKPOINTS } from "../materialYou/themeConsts.mjs";
export type Breakpoint = "xs" | "sm" | "md" | "lg" | "xl";
@@ -68,18 +68,16 @@ export const BaseDialog = ({
const clickPosition = useClickPosition();
useLayoutEffect(
() => {
if (open) {
setOffset({
x: clickPosition?.x ?? 0,
y: clickPosition?.y ?? 0,
});
}
},
// not need clickPosition
[clickPosition?.x, clickPosition?.y, open],
);
const getClickPosition = () => clickPosition;
useLayoutEffect(() => {
if (open) {
setOffset({
x: getClickPosition()?.x ?? 0,
y: getClickPosition()?.y ?? 0,
});
}
}, [open]);
const handleClose = useCallback(() => {
if (onClose) {
@@ -5,7 +5,11 @@ export interface LazyImageProps
extends React.ImgHTMLAttributes<HTMLImageElement> {
loadingClassName?: string;
}
export function LazyImage(props: LazyImageProps) {
export function LazyImage({
className,
loadingClassName,
...others
}: LazyImageProps) {
const [loading, setLoading] = useState(true);
return (
@@ -13,15 +17,15 @@ export function LazyImage(props: LazyImageProps) {
<div
className={cn(
"inline-block animate-pulse bg-slate-200 ring-1 ring-slate-200 dark:bg-slate-700 dark:ring-slate-700",
props.className,
props.loadingClassName,
className,
loadingClassName,
loading ? "inline-block" : "hidden",
)}
/>
<img
{...props}
{...others}
onLoad={() => setLoading(false)}
className={cn(props.className, loading ? "hidden" : "inline-block")}
className={cn(className, loading ? "hidden" : "inline-block")}
/>
</>
);
@@ -3,7 +3,6 @@ import {
hexFromArgb,
themeFromSourceColor,
} from "@material/material-color-utilities";
import type { BreakpointsOptions } from "@mui/material/styles";
import createPalette from "@mui/material/styles/createPalette";
import extendTheme from "@mui/material/styles/experimental_extendTheme";
import {
@@ -20,6 +19,7 @@ import {
MuiPaper,
MuiSwitch,
} from "./themeComponents";
import { MUI_BREAKPOINTS } from "./themeConsts.mjs";
interface ThemeSchema {
primary_color: string;
@@ -33,16 +33,6 @@ interface ThemeSchema {
font_family?: string;
}
export const MUI_BREAKPOINTS: BreakpointsOptions = {
values: {
xs: 0,
sm: 400,
md: 800,
lg: 1200,
xl: 1600,
},
};
export const createMDYTheme = (themeSchema: ThemeSchema) => {
const materialColor = themeFromSourceColor(
argbFromHex(themeSchema.primary_color),
@@ -1,17 +1,34 @@
import { Theme } from "@mui/material";
import { alpha, darken, Theme } from "@mui/material";
import { Components } from "@mui/material/styles/components";
export const MuiButtonGroup: Components<Theme>["MuiButtonGroup"] = {
styleOverrides: {
grouped: {
grouped: ({ theme }) => ({
fontWeight: 700,
},
height: "2.5em",
padding: "0 1.25em",
border: `1px solid ${darken(theme.palette.primary.main, 0.09)}`,
color: darken(theme.palette.primary.main, 0.2),
"&.MuiButton-containedPrimary": {
boxShadow: "none",
border: `1px solid ${theme.palette.primary.mainChannel}`,
backgroundColor: alpha(theme.palette.primary.main, 0.2),
color: theme.palette.primary.main,
"&::before": {
content: "none",
},
"&:hover": {
backgroundColor: alpha(theme.palette.primary.main, 0.3),
},
},
}),
firstButton: {
borderTopLeftRadius: 48,
borderBottomLeftRadius: 48,
"&.MuiButton-sizeSmall": {
paddingLeft: "14px",
paddingLeft: "1.5em",
},
"&.MuiButton-sizeMedium": {
@@ -27,7 +44,7 @@ export const MuiButtonGroup: Components<Theme>["MuiButtonGroup"] = {
borderBottomRightRadius: 48,
"&.MuiButton-sizeSmall": {
paddingRight: "14px",
paddingRight: "1.5em",
},
"&.MuiButton-sizeMedium": {
@@ -39,4 +56,4 @@ export const MuiButtonGroup: Components<Theme>["MuiButtonGroup"] = {
},
},
},
};
} satisfies Components<Theme>["MuiButtonGroup"];
@@ -0,0 +1,10 @@
/** @type {import("@mui/material/styles").BreakpointsOptions} */
export const MUI_BREAKPOINTS = {
values: {
xs: 0,
sm: 400,
md: 800,
lg: 1200,
xl: 1600,
},
};
+1 -1
View File
@@ -4,7 +4,7 @@
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
+2 -2
View File
@@ -2,7 +2,7 @@
"manifest_version": 1,
"latest": {
"mihomo": "v1.18.7",
"mihomo_alpha": "alpha-27bcb26",
"mihomo_alpha": "alpha-518e9bd",
"clash_rs": "v0.2.0",
"clash_premium": "2023-09-05-gdcc8d87"
},
@@ -36,5 +36,5 @@
"darwin-x64": "clash-darwin-amd64-n{}.gz"
}
},
"updated_at": "2024-08-24T22:19:55.791Z"
"updated_at": "2024-08-25T22:20:29.500Z"
}
+1 -1
View File
@@ -80,7 +80,7 @@
"eslint-plugin-react": "7.35.0",
"eslint-plugin-react-compiler": "0.0.0-experimental-72f06b2-20240822",
"eslint-plugin-react-hooks": "4.6.2",
"knip": "5.27.3",
"knip": "5.27.4",
"lint-staged": "15.2.9",
"npm-run-all2": "6.2.2",
"postcss": "8.4.41",
+5 -5
View File
@@ -98,8 +98,8 @@ importers:
specifier: 4.6.2
version: 4.6.2(eslint@8.57.0)
knip:
specifier: 5.27.3
version: 5.27.3(@types/node@22.5.0)(typescript@5.5.4)
specifier: 5.27.4
version: 5.27.4(@types/node@22.5.0)(typescript@5.5.4)
lint-staged:
specifier: 15.2.9
version: 15.2.9
@@ -4614,8 +4614,8 @@ packages:
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
engines: {node: '>=0.10.0'}
knip@5.27.3:
resolution: {integrity: sha512-X0zYs0viwENUtp+FZE2Ig6vQZYvKOz8TvuQkWSWMOXiEDoiMAF+NuDczVD9Dhupicfew0YKpYamHhKtNP+f8+g==}
knip@5.27.4:
resolution: {integrity: sha512-7t1yqIKxaVGYD1cLI4raVLWi9cNqv+JNbngc8mgvTVJbomnxOg1pjxgCGEztB7eVgD+6VEwf7Jg5WHXzk+Kbpw==}
engines: {node: '>=18.6.0'}
hasBin: true
peerDependencies:
@@ -11150,7 +11150,7 @@ snapshots:
kind-of@6.0.3: {}
knip@5.27.3(@types/node@22.5.0)(typescript@5.5.4):
knip@5.27.4(@types/node@22.5.0)(typescript@5.5.4):
dependencies:
'@nodelib/fs.walk': 1.2.8
'@snyk/github-codeowners': 1.1.0
@@ -2,5 +2,5 @@
"printWidth": 140,
"singleQuote": true,
"semi": true,
"useTabs": true
}
"useTabs": false
}
+51 -1
View File
@@ -16,6 +16,11 @@ const (
ConnectionTypeClosed = "closed"
)
type QueryNodeMetricsReq struct {
TimeRange string `json:"time_range"` // 15min/30min/1h/6h/12h/24h
Num int `json:"num"` // number of nodes to query
}
// connection manager interface/
// TODO support closed connection
type Cmgr interface {
@@ -34,17 +39,21 @@ type Cmgr interface {
// Start starts the connection manager.
Start(ctx context.Context, errCH chan error)
QueryNodeMetrics(ctx context.Context, req *QueryNodeMetricsReq) ([]metric_reader.NodeMetrics, error)
}
type cmgrImpl struct {
lock sync.RWMutex
cfg *Config
l *zap.SugaredLogger
mr metric_reader.Reader
// k: relay label, v: connection list
activeConnectionsMap map[string][]conn.RelayConn
closedConnectionsMap map[string][]conn.RelayConn
mr metric_reader.Reader
ms []*metric_reader.NodeMetrics // TODO gc this
}
func NewCmgr(cfg *Config) Cmgr {
@@ -171,6 +180,12 @@ func (cm *cmgrImpl) Start(ctx context.Context, errCH chan error) {
cm.l.Infof("Start Cmgr sync interval=%d", cm.cfg.SyncInterval)
ticker := time.NewTicker(time.Second * time.Duration(cm.cfg.SyncInterval))
defer ticker.Stop()
// sync once at the beginning
if err := cm.syncOnce(ctx); err != nil {
cm.l.Errorf("meet non retry error: %s ,exit now", err)
errCH <- err
return
}
for {
select {
@@ -185,3 +200,38 @@ func (cm *cmgrImpl) Start(ctx context.Context, errCH chan error) {
}
}
}
func (cm *cmgrImpl) QueryNodeMetrics(ctx context.Context, req *QueryNodeMetricsReq) ([]metric_reader.NodeMetrics, error) {
cm.lock.RLock()
defer cm.lock.RUnlock()
var startTime time.Time
switch req.TimeRange {
case "15min":
startTime = time.Now().Add(-15 * time.Minute)
case "30min":
startTime = time.Now().Add(-30 * time.Minute)
case "1h":
startTime = time.Now().Add(-1 * time.Hour)
case "6h":
startTime = time.Now().Add(-6 * time.Hour)
case "12h":
startTime = time.Now().Add(-12 * time.Hour)
case "24h":
startTime = time.Now().Add(-24 * time.Hour)
default:
// default to 15min
startTime = time.Now().Add(-15 * time.Minute)
}
res := []metric_reader.NodeMetrics{}
for _, metrics := range cm.ms {
if metrics.SyncTime.After(startTime) {
res = append(res, *metrics)
}
if req.Num > 0 && len(res) >= req.Num {
break
}
}
return res, nil
}
+4 -3
View File
@@ -35,13 +35,13 @@ func (cm *cmgrImpl) syncOnce(ctx context.Context) error {
// todo: opt lock
cm.lock.Lock()
shorCommit := constant.GitRevision
shortCommit := constant.GitRevision
if len(constant.GitRevision) > 7 {
shorCommit = constant.GitRevision[:7]
shortCommit = constant.GitRevision[:7]
}
req := syncReq{
Stats: []StatsPerRule{},
Version: VersionInfo{Version: constant.Version, ShortCommit: shorCommit},
Version: VersionInfo{Version: constant.Version, ShortCommit: shortCommit},
}
if cm.cfg.NeedMetrics() {
@@ -50,6 +50,7 @@ func (cm *cmgrImpl) syncOnce(ctx context.Context) error {
cm.l.Errorf("read metrics failed: %v", err)
} else {
req.Node = *metrics
cm.ms = append(cm.ms, metrics)
}
}
+1 -49
View File
@@ -11,7 +11,6 @@ import (
"github.com/Ehco1996/ehco/internal/relay/conf"
"github.com/Ehco1996/ehco/internal/tls"
myhttp "github.com/Ehco1996/ehco/pkg/http"
"github.com/Ehco1996/ehco/pkg/sub"
xConf "github.com/xtls/xray-core/infra/conf"
"go.uber.org/zap"
)
@@ -33,18 +32,15 @@ type Config struct {
RelaySyncURL string `json:"relay_sync_url,omitempty"`
RelaySyncInterval int `json:"relay_sync_interval,omitempty"`
SubConfigs []*SubConfig `json:"sub_configs,omitempty"`
XRayConfig *xConf.Config `json:"xray_config,omitempty"`
SyncTrafficEndPoint string `json:"sync_traffic_endpoint,omitempty"`
lastLoadTime time.Time
l *zap.SugaredLogger
cachedClashSubMap map[string]*sub.ClashSub // key: clash sub name
}
func NewConfig(path string) *Config {
return &Config{PATH: path, l: zap.S().Named("cfg"), cachedClashSubMap: make(map[string]*sub.ClashSub)}
return &Config{PATH: path, l: zap.S().Named("cfg")}
}
func (c *Config) NeedSyncFromServer() bool {
@@ -93,21 +89,6 @@ func (c *Config) Adjust() error {
c.WebHost = "0.0.0.0"
}
clashSubList, err := c.GetClashSubList()
if err != nil {
return err
}
for _, clashSub := range clashSubList {
if err := clashSub.Refresh(); err != nil {
return err
}
relayConfigs, err := clashSub.ToRelayConfigs(c.WebHost)
if err != nil {
return err
}
c.RelayConfigs = append(c.RelayConfigs, relayConfigs...)
}
for _, r := range c.RelayConfigs {
if err := r.Validate(); err != nil {
return err
@@ -160,32 +141,3 @@ func (c *Config) GetMetricURL() string {
}
return url
}
func (c *Config) GetClashSubList() ([]*sub.ClashSub, error) {
clashSubList := make([]*sub.ClashSub, 0, len(c.SubConfigs))
for _, subCfg := range c.SubConfigs {
clashSub, err := c.getOrCreateClashSub(subCfg)
if err != nil {
return nil, err
}
clashSubList = append(clashSubList, clashSub)
}
return clashSubList, nil
}
func (c *Config) getOrCreateClashSub(subCfg *SubConfig) (*sub.ClashSub, error) {
if clashSub, ok := c.cachedClashSubMap[subCfg.Name]; ok {
return clashSub, nil
}
clashSub, err := sub.NewClashSubByURL(subCfg.URL, subCfg.Name)
if err != nil {
return nil, err
}
c.cachedClashSubMap[subCfg.Name] = clashSub
return clashSub, nil
}
type SubConfig struct {
Name string `json:"name"`
URL string `json:"url"`
}
+1 -3
View File
@@ -21,9 +21,7 @@ const (
shortHashLength = 7
)
var (
ErrIdleTimeout = errors.New("connection closed due to idle timeout")
)
var ErrIdleTimeout = errors.New("connection closed due to idle timeout")
// RelayConn is the interface that represents a relay connection.
// it contains two connections: clientConn and remoteConn
+1 -1
View File
@@ -107,7 +107,7 @@ func (r *Config) Adjust() error {
zap.S().Debugf("label is empty, set default label:%s", r.Label)
}
if len(r.Remotes) == 0 && len(r.TCPRemotes) != 0 {
zap.S().Warnf("tcp remotes is deprecated, use remotes instead")
zap.S().Warnf("tcp remotes is deprecated, please use remotes instead")
r.Remotes = r.TCPRemotes
}
+1 -1
View File
@@ -71,7 +71,7 @@ func (s *Server) Start(ctx context.Context) error {
go s.startOneRelay(ctx, r)
}
if s.cfg.PATH != "" && (s.cfg.ReloadInterval > 0 || len(s.cfg.SubConfigs) > 0) {
if s.cfg.PATH != "" && (s.cfg.ReloadInterval > 0) {
s.l.Infof("Start to watch relay config %s ", s.cfg.PATH)
go s.WatchAndReload(ctx)
}
+18 -45
View File
@@ -6,7 +6,7 @@ import (
"net/http"
"strconv"
"github.com/Ehco1996/ehco/internal/config"
"github.com/Ehco1996/ehco/internal/cmgr"
"github.com/Ehco1996/ehco/internal/constant"
"github.com/labstack/echo/v4"
"go.uber.org/zap"
@@ -28,60 +28,16 @@ func (s *Server) index(c echo.Context) error {
GitRevision string
BuildTime string
StartTime string
SubConfigs []*config.SubConfig
}{
Version: constant.Version,
GitBranch: constant.GitBranch,
GitRevision: constant.GitRevision,
BuildTime: constant.BuildTime,
StartTime: constant.StartTime.Format("2006-01-02 15:04:05"),
SubConfigs: s.cfg.SubConfigs,
}
return c.Render(http.StatusOK, "index.html", data)
}
func (s *Server) HandleClashProxyProvider(c echo.Context) error {
subName := c.QueryParam("sub_name")
if subName == "" {
return c.String(http.StatusBadRequest, "sub_name is empty")
}
grouped, _ := strconv.ParseBool(c.QueryParam("grouped")) // defaults to false if parameter is missing or invalid
return s.handleClashProxyProvider(c, subName, grouped)
}
func (s *Server) handleClashProxyProvider(c echo.Context, subName string, grouped bool) error {
if s.Reloader != nil {
if err := s.Reloader.Reload(true); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
} else {
s.l.Debugf("Reloader is nil this should not happen")
return echo.NewHTTPError(http.StatusBadRequest, "should not happen error happen :)")
}
clashSubList, err := s.cfg.GetClashSubList()
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
for _, clashSub := range clashSubList {
if clashSub.Name == subName {
var clashCfgBuf []byte
if grouped {
clashCfgBuf, err = clashSub.ToGroupedClashConfigYaml()
} else {
clashCfgBuf, err = clashSub.ToClashConfigYaml()
}
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"message": err.Error()})
}
return c.String(http.StatusOK, string(clashCfgBuf))
}
}
msg := fmt.Sprintf("sub_name=%s not found", subName)
return c.JSON(http.StatusBadRequest, map[string]string{"message": msg})
}
func (s *Server) HandleReload(c echo.Context) error {
if s.Reloader == nil {
return echo.NewHTTPError(http.StatusBadRequest, "reload not support")
@@ -165,3 +121,20 @@ func (s *Server) ListRules(c echo.Context) error {
"Configs": s.cfg.RelayConfigs,
})
}
func (s *Server) GetNodeMetrics(c echo.Context) error {
req := &cmgr.QueryNodeMetricsReq{TimeRange: c.QueryParam("time_range")}
num := c.QueryParam("num")
if num != "" {
n, err := strconv.Atoi(num)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
req.Num = n
}
metrics, err := s.connMgr.QueryNodeMetrics(c.Request().Context(), req)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
return c.JSON(http.StatusOK, metrics)
}
+1 -1
View File
@@ -103,13 +103,13 @@ func NewServer(
e.GET("/", s.index)
e.GET("/connections/", s.ListConnections)
e.GET("/rules/", s.ListRules)
e.GET("/clash_proxy_provider/", s.HandleClashProxyProvider)
// api group
api := e.Group("/api/v1")
api.GET("/config/", s.CurrentConfig)
api.POST("/config/reload/", s.HandleReload)
api.GET("/health_check/", s.HandleHealthCheck)
api.GET("/node_metrics/", s.GetNodeMetrics)
return s, nil
}
+119 -161
View File
@@ -1,167 +1,125 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="description" content="ehco web" />
<meta name="keywords" content="ehco-relay" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/bulma/1.0.0/css/bulma.min.css"
/>
<title>Ehco</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
</head>
<body>
<section class="hero is-fullheight is-light">
<div class="hero-body">
<div class="container">
<!-- Title -->
<h1 class="title has-text-centered">
ehco is a network relay tool and a typo :)
</h1>
<!-- Build Info Card -->
<div class="card">
<header class="card-header">
<p class="card-header-title has-text-centered">
Build Info
</p>
</header>
<div class="card-content">
<div class="content">
<ul>
<li>Version: {{.Version}}</li>
<li>GitBranch: {{.GitBranch}}</li>
<li>GitRevision: {{.GitRevision}}</li>
<li>BuildTime: {{.BuildTime}}</li>
<li>StartTime: {{.StartTime}}</li>
</ul>
</div>
</div>
</div>
<!-- Stylish Links Card -->
<div class="card">
<header class="card-header">
<p class="card-header-title has-text-centered">
Quick Links
</p>
</header>
<div class="card-content">
<div class="content">
<ul>
<li>
<a
href="/metrics/"
class="button is-info is-light"
>Metrics</a
>
</li>
<li>
<a
href="/debug/pprof/"
class="button is-info is-light"
>Debug</a
>
</li>
<li>
<a
href="/connections/?conn_type=active"
class="button is-info is-light"
>Connections</a
>
</li>
<li>
<a
href="/rules/"
class="button is-info is-light"
>Rule List</a
>
</li>
<li>
<a
href="/api/v1/config/"
class="button is-info is-light"
>Config</a
>
</li>
</ul>
</div>
</div>
</div>
<!-- Clash Providers card -->
{{ if .SubConfigs }}
<div class="card">
<header class="card-header">
<p class="card-header-title has-text-centered">
Clash Providers
</p>
</header>
<div class="card-content">
<div class="content">
<ul>
{{ range .SubConfigs }}
<li>
<a
class="button is-info is-light"
href="/clash_proxy_provider/?sub_name={{.Name}}"
>{{.Name}}</a
>
</li>
<li>
<a
class="button is-info is-light"
href="/clash_proxy_provider/?sub_name={{.Name}}&grouped=true"
>{{.Name}}-lb</a
>
</li>
</ul>
{{ end }}
</div>
</div>
</div>
{{ end }}
<!-- Reload Config Button -->
<div class="has-text-centered">
<button
class="button is-danger is-outlined"
id="reloadButton"
>
Reload Config
</button>
</div>
<head>
<title>Ehco Web</title>
<meta charset="UTF-8" />
<meta name="description" content="ehco web" />
<meta name="keywords" content="ehco-relay" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/1.0.1/css/bulma.min.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
</head>
<body>
<section class="hero is-fullheight is-light">
<div class="hero-body">
<div class="container">
<h1 class="title has-text-centered">Ehco Relay</h1>
<div class="columns is-variable is-8 is-flex-grow">
<div class="column is-flex">
<!-- Build Info Card -->
<div class="card is-flex is-flex-direction-column is-flex-grow-1">
<header class="card-header">
<p class="card-header-title has-text-centered">Build Info</p>
</header>
<div class="card-content is-flex-grow-1">
<div class="content">
<ul>
<li>Version: {{.Version}}</li>
<li>GitBranch: {{.GitBranch}}</li>
<li>GitRevision: {{.GitRevision}}</li>
<li>BuildTime: {{.BuildTime}}</li>
<li>StartTime: {{.StartTime}}</li>
</ul>
</div>
</div>
</div>
<!-- Footer -->
<div class="hero-foot">
<footer class="footer">
<div class="content has-text-centered">
<a href="https://github.com/Ehco1996/ehco"
>Source code</a
>
</div>
<footer class="card-footer">
<button class="button is-danger is-outlined card-footer-item" id="reloadButton">
<span class="icon"><i class="fas fa-sync-alt"></i></span>
<span>Reload Config</span>
</button>
</footer>
</div>
</div>
</section>
<script>
$(document).ready(function () {
$("#reloadButton").click(function () {
$.ajax({
type: "POST",
url: "/api/v1/config/reload/",
success: function (response) {
alert(
"Reload config success. Response: " + response
);
},
error: function (response) {
alert(
"Failed to reload config. Response: " +
response.responseText
);
},
});
});
});
</script>
</body>
<div class="column is-flex">
<!-- Stylish Links Card -->
<div class="card is-flex is-flex-direction-column is-flex-grow-1">
<header class="card-header">
<p class="card-header-title has-text-centered">Quick Links</p>
</header>
<div class="card-content is-flex-grow-1">
<div class="content">
<ul>
<li>
<a href="/metrics/" class="button is-info is-light">
<span class="icon"><i class="fas fa-chart-bar"></i></span>
<span>Metrics</span>
</a>
</li>
<li>
<a href="/debug/pprof/" class="button is-info is-light">
<span class="icon"><i class="fas fa-bug"></i></span>
<span>Debug</span>
</a>
</li>
<li>
<a href="/connections/?conn_type=active" class="button is-info is-light">
<span class="icon"><i class="fas fa-link"></i></span>
<span>Connections</span>
</a>
</li>
<li>
<a href="/rules/" class="button is-info is-light">
<span class="icon"><i class="fas fa-list"></i></span>
<span>Rule List</span>
</a>
</li>
<li>
<a href="/api/v1/config/" class="button is-info is-light">
<span class="icon"><i class="fas fa-cog"></i></span>
<span>Config</span>
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- metrics -->
{{template "metrics.html"}}
</div>
</div>
<!-- Footer -->
<div class="hero-foot">
<footer class="footer">
<div class="content has-text-centered">
<a href="https://github.com/Ehco1996/ehco"><i class="fab fa-github"></i> Source code</a>
</div>
</footer>
</div>
</section>
<script>
$(document).ready(function () {
// Reload config button click event
$('#reloadButton').click(function () {
$.ajax({
type: 'POST',
url: '/api/v1/config/reload/',
success: function (response) {
alert('Reload config success. Response: ' + response);
},
error: function (response) {
alert('Failed to reload config. Response: ' + response.responseText);
},
});
});
});
</script>
</body>
</html>
+284
View File
@@ -0,0 +1,284 @@
<div class="card" id="metrics-card">
<header class="card-header is-flex is-flex-wrap-wrap">
<p class="card-header-title has-text-centered">Node Metrics</p>
<div class="card-header-icon is-flex-grow-1 is-flex is-justify-content-flex-end">
<div class="dropdown is-hoverable">
<div class="dropdown-trigger">
<button class="button" aria-haspopup="true" aria-controls="dropdown-menu">
<span class="icon">
<i class="fas fa-clock"></i>
</span>
<span>Time</span>
<span class="icon is-small">
<i class="fas fa-angle-down" aria-hidden="true"></i>
</span>
</button>
</div>
<div class="dropdown-menu" id="dropdown-menu" role="menu">
<div class="dropdown-content">
<a href="#" class="dropdown-item" data-time="15min">Last 15 Min</a>
<a href="#" class="dropdown-item" data-time="30min">Last 30 Min</a>
<a href="#" class="dropdown-item" data-time="1h">Last 1 hour</a>
<a href="#" class="dropdown-item" data-time="6h">Last 6 hours</a>
<a href="#" class="dropdown-item" data-time="12h">Last 12 hours</a>
<a href="#" class="dropdown-item" data-time="24h">Last 24 hours</a>
</div>
</div>
</div>
</div>
</header>
<div class="card-content">
<div class="content">
<div class="columns is-multiline">
<div class="column is-4">
<h2 class="subtitle is-5">CPU</h2>
<canvas id="cpuChart"></canvas>
</div>
<div class="column is-4">
<h2 class="subtitle is-5">Memory</h2>
<canvas id="memoryChart"></canvas>
</div>
<div class="column is-4">
<h2 class="subtitle is-5">Disk</h2>
<canvas id="diskChart"></canvas>
</div>
<div class="column is-6">
<h2 class="subtitle is-5">Network</h2>
<canvas id="networkChart"></canvas>
</div>
<div class="column is-6">
<h2 class="subtitle is-5">Ping</h2>
<canvas id="pingChart"></canvas>
</div>
</div>
</div>
</div>
<script>
// Constants
const API_BASE_URL = '/api/v1/node_metrics/';
const BYTE_TO_MB = 1024 * 1024;
const BYTE_TO_GB = BYTE_TO_MB * 1024;
// Utility functions
const handleError = (error) => {
console.error('Error:', error);
// You can add user notifications here
};
const formatDate = (dateString) => {
const date = new Date(dateString);
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
};
const formatBytes = (bytes, decimals = 2) => {
return (bytes / BYTE_TO_GB).toFixed(decimals);
};
// Chart functions
const initChart = (canvasId, type, datasets, legendPosition = '', yDisplayText = '', additionalInfo = '', unit = '') => {
const ctx = document.getElementById(canvasId).getContext('2d');
const data = {
labels: [],
datasets: Array.isArray(datasets) ? datasets.map((dataset) => ({ ...dataset, data: [] })) : [{ ...datasets, data: [] }],
};
return new Chart(ctx, {
type,
data,
options: {
responsive: true,
plugins: {
legend: { position: legendPosition },
title: {
display: !!additionalInfo,
text: additionalInfo,
position: 'bottom',
font: { size: 12 },
},
tooltip: {
callbacks: {
label: function (context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
label += context.parsed.y.toFixed(2) + ' ' + unit;
}
return label;
},
},
},
},
scales: {
y: {
beginAtZero: true,
title: { display: true, text: yDisplayText },
},
},
elements: { line: { tension: 0.2 } },
downsample: {
enabled: true,
samples: 100,
threshold: 50,
},
},
});
};
const updateChart = (chart, newData, labels) => {
if (!newData || !labels) {
console.error('Invalid data or labels provided');
return;
}
const formattedLabels = labels.map(formatDate);
if (Array.isArray(newData) && Array.isArray(newData[0])) {
chart.data.datasets.forEach((dataset, index) => {
if (newData[index]) {
dataset.data = newData[index];
}
});
} else {
chart.data.datasets[0].data = newData;
}
chart.data.labels = formattedLabels;
chart.update();
};
// Data fetching functions
const fetchLatestMetric = async () => {
try {
const response = await fetch(`${API_BASE_URL}?time_range=15min&num=1`);
if (!response.ok) throw new Error('Network response was not ok');
const data = await response.json();
return data[0];
} catch (error) {
handleError(error);
}
};
const fetchMetrics = async (timeRange) => {
try {
const response = await fetch(`${API_BASE_URL}?time_range=${timeRange}`);
if (!response.ok) throw new Error('Network response was not ok');
return await response.json();
} catch (error) {
handleError(error);
}
};
// Chart initialization
const initializeCharts = async () => {
const metric = await fetchLatestMetric();
if (!metric) return null;
const pingTargets = metric.ping_metrics.map((ping) => ping.target);
return {
cpu: initChart(
'cpuChart',
'line',
{ label: 'CPU' },
'top',
'Usage (%)',
`Load: ${metric.cpu_load_info} | Cores: ${metric.cpu_core_count}`,
'%',
),
memory: initChart(
'memoryChart',
'line',
{ label: 'Memory' },
'top',
'Usage (%)',
`Total: ${formatBytes(metric.memory_total_bytes)} GB`,
'%',
),
disk: initChart(
'diskChart',
'line',
{ label: 'Disk' },
'top',
'Usage (%)',
`Total: ${formatBytes(metric.disk_total_bytes)} GB`,
'%',
),
network: initChart('networkChart', 'line', [{ label: 'Receive' }, { label: 'Transmit' }], 'top', 'Rate (MB/s)', '', 'MB/s'),
ping: initChart(
'pingChart',
'line',
pingTargets.map((target) => ({ label: target })),
'right',
'Latency (ms)',
'',
'ms',
),
};
};
// Update functions
const updateCharts = (charts, metrics) => {
const timestamps = metrics.map((data) => data.SyncTime);
updateChart(
charts.cpu,
metrics.map((data) => data.cpu_usage_percent),
timestamps,
);
updateChart(
charts.memory,
metrics.map((data) => data.memory_usage_percent),
timestamps,
);
updateChart(
charts.disk,
metrics.map((data) => data.disk_usage_percent),
timestamps,
);
updateChart(
charts.network,
[
metrics.map((data) => data.network_receive_bytes_rate / BYTE_TO_MB),
metrics.map((data) => data.network_transmit_bytes_rate / BYTE_TO_MB),
],
timestamps,
);
const pingTargets = [...new Set(metrics.flatMap((data) => data.ping_metrics.map((ping) => ping.target)))];
const pingData = pingTargets.map((target) =>
metrics.map((data) => {
const pingMetric = data.ping_metrics.find((ping) => ping.target === target);
return pingMetric ? pingMetric.latency : null;
}),
);
updateChart(charts.ping, pingData, timestamps);
const latestMetric = metrics[metrics.length - 1];
updateAdditionalInfo(charts, latestMetric);
};
const updateAdditionalInfo = (charts, metric) => {
charts.cpu.options.plugins.title.text = `Load: ${metric.cpu_load_info} | Cores: ${metric.cpu_core_count}`;
charts.memory.options.plugins.title.text = `Total: ${formatBytes(metric.memory_total_bytes)} GB`;
charts.disk.options.plugins.title.text = `Total: ${formatBytes(metric.disk_total_bytes)} GB`;
charts.cpu.update();
charts.memory.update();
charts.disk.update();
};
// Main execution
$(document).ready(async function () {
let charts = await initializeCharts();
if (!charts) return;
$('.dropdown-item').click(async function () {
const timeRange = $(this).data('time');
const metrics = await fetchMetrics(timeRange);
if (metrics) updateCharts(charts, metrics);
});
});
</script>
</div>
+74 -81
View File
@@ -1,86 +1,79 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="ehco web" />
<meta name="keywords" content="ehco-relay" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/bulma/1.0.1/css/bulma.min.css"
/>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<title>Rules</title>
</head>
<body>
<section class="section">
<div class="container">
<h1 class="title">Rules</h1>
<table class="table is-striped is-fullwidth">
<thead>
<tr>
<th>Label</th>
<th>Listen</th>
<th>Listen Type</th>
<th>Transport Type</th>
<th>Remote</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{{range .Configs}}
<tr>
<td>{{.Label}}</td>
<td>{{.Listen}}</td>
<td>{{.ListenType}}</td>
<td>{{.TransportType}}</td>
<td>{{.GetTCPRemotes}}</td>
<td>
<button
class="button is-small is-primary health-check"
data-label="{{.Label}}"
onclick="checkHealth('{{.Label}}')"
>
Check Health
</button>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</section>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="ehco web" />
<meta name="keywords" content="ehco-relay" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/1.0.1/css/bulma.min.css" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<title>Rules</title>
</head>
<body>
<section class="section">
<div class="container">
<h1 class="title">Rules</h1>
<table class="table is-striped is-fullwidth">
<thead>
<tr>
<th>Label</th>
<th>Listen</th>
<th>Listen Type</th>
<th>Transport Type</th>
<th>Remote</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{{range .Configs}}
<tr>
<td>{{.Label}}</td>
<td>{{.Listen}}</td>
<td>{{.ListenType}}</td>
<td>{{.TransportType}}</td>
<td>{{.Remotes}}</td>
<td>
<button class="button is-small is-primary health-check" data-label="{{.Label}}" onclick="checkHealth('{{.Label}}')">
Check Health
</button>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</section>
<script>
function checkHealth(label) {
$.ajax({
url: "/api/v1/health_check/?relay_label=" + label,
method: "GET",
success: function (response) {
// Check if the response includes an error code
if (response.error_code === 0) {
// If no error, show success message with latency
alert(
"Health Check for " +
label +
": " +
response.msg + // Use 'msg' as per Go struct
" (Latency: " +
response.latency + // Ensure this matches the Go struct field name
"ms)"
);
} else {
// If error code is not 0, show error message
alert("Error for " + label + ": " + response.msg);
}
},
error: function (xhr) {
// Parse the response JSON in case of HTTP error
var response = JSON.parse(xhr.responseText);
alert("Error: " + response.msg); // Use 'msg' as per Go struct
},
});
<script>
function checkHealth(label) {
$.ajax({
url: '/api/v1/health_check/?relay_label=' + label,
method: 'GET',
success: function (response) {
// Check if the response includes an error code
if (response.error_code === 0) {
// If no error, show success message with latency
alert(
'Health Check for ' +
label +
': ' +
response.msg + // Use 'msg' as per Go struct
' (Latency: ' +
response.latency + // Ensure this matches the Go struct field name
'ms)'
);
} else {
// If error code is not 0, show error message
alert('Error for ' + label + ': ' + response.msg);
}
</script>
</body>
},
error: function (xhr) {
// Parse the response JSON in case of HTTP error
var response = JSON.parse(xhr.responseText);
alert('Error: ' + response.msg); // Use 'msg' as per Go struct
},
});
}
</script>
</body>
</html>
+2 -2
View File
@@ -249,7 +249,7 @@ func (b *readerImpl) parseNetworkInfo(metricMap map[string]*dto.MetricFamily, nm
}
if b.lastMetrics != nil {
passedTime := now.Sub(b.lastMetrics.syncTime).Seconds()
passedTime := now.Sub(b.lastMetrics.SyncTime).Seconds()
nm.NetworkReceiveBytesRate = (nm.NetworkReceiveBytesTotal - b.lastMetrics.NetworkReceiveBytesTotal) / passedTime
nm.NetworkTransmitBytesRate = (nm.NetworkTransmitBytesTotal - b.lastMetrics.NetworkTransmitBytesTotal) / passedTime
}
@@ -272,7 +272,7 @@ func (b *readerImpl) ReadOnce(ctx context.Context) (*NodeMetrics, error) {
if err != nil {
return nil, err
}
nm := &NodeMetrics{syncTime: time.Now(), PingMetrics: []PingMetric{}}
nm := &NodeMetrics{SyncTime: time.Now(), PingMetrics: []PingMetric{}}
if err := b.parseCpuInfo(parsed, nm); err != nil {
return nil, err
}
+1 -1
View File
@@ -29,7 +29,7 @@ type NodeMetrics struct {
// ping
PingMetrics []PingMetric `json:"ping_metrics"`
syncTime time.Time
SyncTime time.Time
}
type PingMetric struct {
+1 -30
View File
@@ -38,36 +38,7 @@ For an explanation of the mieru protocol, see [mieru Proxy Protocol](./docs/prot
3. [Client Installation & Configuration - OpenWrt](./docs/client-install-openwrt.md)
4. [Maintenance & Troubleshooting](./docs/operation.md)
5. [Security Guide](./docs/security.md)
## Compile
Compiling should be done in Linux. The compilation process requires downloading dependent packages, which may be blocked by the firewall.
The following softwares are required for compilation.
- curl
- env
- git
- go (version >= 1.20)
- make
- sha256sum
- tar
- zip
To build Android executable files:
- gcc
To build debian packages:
- dpkg-deb
- fakeroot
To build RPM packages:
- rpmbuild
To compile, go to the root directory of the project and invoke `make`. The compilation result will be stored in the `release` directory.
6. [Compilation](./docs/compile.md)
## Share
+1 -30
View File
@@ -36,36 +36,7 @@ mieru 的翻墙原理与 shadowsocks / v2ray 等软件类似,在客户端和
3. [客户端安装与配置 - OpenWrt](./docs/client-install-openwrt.zh_CN.md)
4. [运营维护与故障排查](./docs/operation.zh_CN.md)
5. [翻墙安全指南](./docs/security.zh_CN.md)
## 编译
编译 mieru 的客户端和服务器软件,建议在 Linux 系统中进行。编译过程可能需要翻墙下载依赖的软件包。
编译所需的软件包括:
- curl
- env
- git
- go (version >= 1.20)
- make
- sha256sum
- tar
- zip
编译 Android 可执行文件需要:
- gcc
编译 debian 安装包需要:
- dpkg-deb
- fakeroot
编译 RPM 安装包需要:
- rpmbuild
编译时,进入项目根目录,调用指令 `make` 即可。编译结果会存放在项目根目录下的 `release` 文件夹。
6. [编译](./docs/compile.zh_CN.md)
## 分享
+44
View File
@@ -0,0 +1,44 @@
# Compilation
It is recommended to compile the `mieru` client and `mita` server software on Linux. The compilation process might require a proxy to download dependency packages.
The following softwares are required for compilation:
- curl
- env
- git
- go (version >= 1.20)
- make
- sha256sum
- tar
- zip
To compile Android executables, you need:
- gcc
To compile Debian packages, you need:
- dpkg-deb
- fakeroot
To compile RPM packages, you need:
- rpmbuild
To compile, navigate to the project's root directory and run the command `make`. The compilation results will be stored in the `release` folder under the project's root directory.
The `make` command will only generate the officially supported executables. If you want to compile executables for a specific CPU instruction set architecture or operating system, you can refer to the following commands:
```sh
# Compile the mita server software, which runs on a Linux system with Loongson processor
env GOOS=linux GOARCH=loong64 CGO_ENABLED=0 go build -ldflags="-s -w" -o mita cmd/mita/mita.go
# Compile the mieru client software, which runs on a FreeBSD system with x86_64 processor
env GOOS=freebsd GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w" -o mieru cmd/mieru/mieru.go
# Compile the mieru client software, which runs on an OpenWRT system with MIPS processor
env GOOS=linux GOARCH=mips CGO_ENABLED=0 go build -ldflags="-s -w" -o mieru cmd/mieru/mieru.go
```
**Note: The `mita` server software may not run on operating systems other than Linux.**
+44
View File
@@ -0,0 +1,44 @@
# 编译
编译 mieru 客户端和 mita 服务器软件,建议在 Linux 系统中进行。编译过程可能需要翻墙下载依赖的软件包。
编译所需的软件包括:
- curl
- env
- git
- go (version >= 1.20)
- make
- sha256sum
- tar
- zip
编译 Android 可执行文件需要:
- gcc
编译 Debian 安装包需要:
- dpkg-deb
- fakeroot
编译 RPM 安装包需要:
- rpmbuild
编译时,进入项目根目录,运行指令 `make` 即可。编译结果会存放在项目根目录下的 `release` 文件夹。
`make` 指令只会生成官方支持的可执行文件。如果你想要编译特定 CPU 指令集架构或操作系统的可执行文件,可以参考下面的几个指令:
```sh
# 编译可以在龙芯处理器 Linux 系统上运行的 mita 服务器软件
env GOOS=linux GOARCH=loong64 CGO_ENABLED=0 go build -ldflags="-s -w" -o mita cmd/mita/mita.go
# 编译可以在 x86_64 处理器 FreeBSD 系统上运行的 mieru 客户端软件
env GOOS=freebsd GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w" -o mieru cmd/mieru/mieru.go
# 编译可以在 MIPS 处理器 OpenWRT 系统上运行的 mieru 客户端软件
env GOOS=linux GOARCH=mips CGO_ENABLED=0 go build -ldflags="-s -w" -o mieru cmd/mieru/mieru.go
```
**注意,`mita` 服务器软件可能无法在 Linux 之外的操作系统中运行。**
+2 -55
View File
@@ -92,7 +92,7 @@ const (
// The size of the bandwidth filter window, in round-trips.
bandwidthFilterWindowSize = gainCycleLength + 2
// The time after which the current min_rtt value expires.
// The time after which the current min RTT value expires.
minRTTExpiry = 10 * time.Second
// The minimum time the connection can spend in PROBE RTT mode.
@@ -104,13 +104,6 @@ const (
startUpGrowthTarget = 1.25
roundTripsWithoutGrowthBeforeExitingStartup = 3
// Coefficient of target congestion window to use when basing PROBE RTT on BDP.
moderateProbeRttMultiplier = 0.75
// Coefficient to determine if a new RTT is sufficiently similar to min RTT that
// we don't need to enter PROBE RTT.
similarMinRttThreshold = 1.125
)
var (
@@ -282,23 +275,6 @@ type BBRSender struct {
// Used as the initial packet conservation mode when first entering recovery.
initialConservationInStartup bbrRecoveryState
// If true, will not exit low gain mode until bytesInFlight drops below BDP
// or it's time for high gain mode.
fullyDrainQueue bool
// If true, use a CWND of 0.75*BDP during PROBE RTT instead of minCongestionWindow.
probeRTTBasedOnBDP bool
// If true, skip PROBE RTT and update the timestamp of the existing minRTT to
// now if minRTT over the last cycle is within 12.5% of the current minRTT.
// Even if the minRTT is 12.5% too low, the 25% gain cycling and 2x CWND gain
// should overcome an overly small minRTT.
probeRTTSkippedIfSimilarRTT bool
// If true, disable PROBE RTT entirely as long as the connection was recently
// app limited.
probeRTTDisabledIfAppLimited bool
appLimitedSinceLastProbeRTT bool
minRTTSinceLastProbeRTT time.Duration
}
@@ -492,9 +468,6 @@ func (b *BBRSender) GetTargetCongestionWindow(gain float64) int64 {
}
func (b *BBRSender) ProbeRTTCongestionWindow() int64 {
if b.probeRTTBasedOnBDP {
return b.GetTargetCongestionWindow(moderateProbeRttMultiplier)
}
return b.minCongestionWindow
}
@@ -564,11 +537,7 @@ func (b *BBRSender) UpdateBandwidthAndMinRTT(now time.Time, ackedPackets []Acked
b.minRTTSinceLastProbeRTT = mathext.Min(b.minRTTSinceLastProbeRTT, sampleMinRTT)
minRTTExpired := b.minRTT > 0 && now.After(b.minRTTTimestamp.Add(minRTTExpiry))
if b.minRTT <= 0 || minRTTExpired || sampleMinRTT < b.minRTT {
if b.ShouldExtendMinRTTExpiry() {
minRTTExpired = false
} else {
b.minRTT = sampleMinRTT
}
b.minRTT = sampleMinRTT
b.minRTTTimestamp = now
b.minRTTSinceLastProbeRTT = infDuration
b.appLimitedSinceLastProbeRTT = false
@@ -576,21 +545,6 @@ func (b *BBRSender) UpdateBandwidthAndMinRTT(now time.Time, ackedPackets []Acked
return minRTTExpired
}
func (b *BBRSender) ShouldExtendMinRTTExpiry() bool {
if b.probeRTTDisabledIfAppLimited && b.appLimitedSinceLastProbeRTT {
// Extend the current min RTT if we've been app limited recently.
return true
}
minRTTIncreasedSinceLastProbe := b.minRTTSinceLastProbeRTT > time.Duration(float64(b.minRTT)*similarMinRttThreshold)
if b.probeRTTSkippedIfSimilarRTT && b.appLimitedSinceLastProbeRTT && !minRTTIncreasedSinceLastProbe {
// Extend the current min RTT if we've been app limited recently and an RTT
// has been measured in that time that's less than 12.5% more than the
// current min RTT.
return true
}
return false
}
func (b *BBRSender) UpdateGainCyclePhase(now time.Time, priorInFlight int64, hasLosses bool) {
// In most cases, the cycle is advanced after an RTT passes.
shouldAdvanceGainCycling := now.Sub(b.lastCycleStart) > b.GetMinRTT()
@@ -615,13 +569,6 @@ func (b *BBRSender) UpdateGainCyclePhase(now time.Time, priorInFlight int64, has
if shouldAdvanceGainCycling {
b.cycleCurrentOffset = (b.cycleCurrentOffset + 1) % gainCycleLength
b.lastCycleStart = now
// Stay in low gain mode until the target BDP is hit.
// Low gain mode will be exited immediately when the target BDP is achieved.
if b.pacingGain < 1.0 && pacingGainList[b.cycleCurrentOffset] == 1.0 && b.fullyDrainQueue && priorInFlight > b.GetTargetCongestionWindow(1.0) {
return
}
b.pacingGain = pacingGainList[b.cycleCurrentOffset]
}
}
+7
View File
@@ -21,6 +21,9 @@ import (
"github.com/enfein/mieru/pkg/mathext"
)
// Pacer limits the speed of sending packets.
// Pacer is not thread safe. The caller should provide synchronization
// to avoid race condition.
type Pacer struct {
budgetAtLastSent int64
maxBudget int64 // determine the max burst
@@ -28,6 +31,7 @@ type Pacer struct {
lastSentTime time.Time
}
// NewPacer returns a new Pacer object.
func NewPacer(initialBudget, maxBudget, minPacingRate int64) *Pacer {
if initialBudget <= 0 {
panic("initial budget must be a positive number")
@@ -48,16 +52,19 @@ func NewPacer(initialBudget, maxBudget, minPacingRate int64) *Pacer {
}
}
// OnPacketSent updates the budget and time when a packet is sent.
func (p *Pacer) OnPacketSent(sentTime time.Time, bytes, pacingRate int64) {
budget := p.Budget(sentTime, pacingRate)
p.budgetAtLastSent = mathext.Max(budget-bytes, 0)
p.lastSentTime = sentTime
}
// CanSend returns true if a packet can be sent based on the given pacing rate.
func (p *Pacer) CanSend(now time.Time, bytes, pacingRate int64) bool {
return p.Budget(now, pacingRate) >= bytes
}
// Budget returns the maximum number of bytes can be sent right now.
func (p *Pacer) Budget(now time.Time, pacingRate int64) int64 {
pacingRate = mathext.Max(pacingRate, p.minPacingRate)
if p.lastSentTime.IsZero() {
+46
View File
@@ -0,0 +1,46 @@
// 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/>.
package congestion
import (
"testing"
"time"
)
func TestPacer(t *testing.T) {
pacer := NewPacer(1000, 10000, 100)
now := time.Now().Truncate(time.Second)
pacer.OnPacketSent(now, 1000, 1000)
// Budget at now is 0.
if can := pacer.CanSend(now, 1, 1000); can {
t.Errorf("CanSend() = %v, want %v", can, false)
}
if can := pacer.CanSend(now.Add(time.Second), 999, 1000); !can {
t.Errorf("CanSend() = %v, want %v", can, true)
}
// Minimum pacing rate is 100.
if can := pacer.CanSend(now.Add(time.Second), 99, 10); !can {
t.Errorf("CanSend() = %v, want %v", can, true)
}
// Maximum budget is 10000.
if can := pacer.CanSend(now.Add(time.Second), 10001, 100000); can {
t.Errorf("CanSend() = %v, want %v", can, false)
}
}
+2
View File
@@ -681,6 +681,8 @@ func (s *Session) runOutputLoop(ctx context.Context) error {
bytesInFlight += newBytesInFlight
}
}
} else {
s.sendAlgorithm.OnApplicationLimited(bytesInFlight)
}
// Send ACK or heartbeat if needed.
+2
View File
@@ -207,6 +207,8 @@ jobs:
if: ${{ matrix.jobs.test == 'test' }}
run: |
go test ./...
echo "---test with_gvisor---"
go test ./... -tags "with_gvisor" -count=1
- name: Update CA
run: |
+20 -4
View File
@@ -7,6 +7,7 @@ import (
"fmt"
"net"
"net/netip"
"runtime"
"strconv"
"time"
@@ -14,6 +15,7 @@ import (
"github.com/metacubex/quic-go/congestion"
M "github.com/sagernet/sing/common/metadata"
CN "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/proxydialer"
@@ -43,6 +45,8 @@ type Hysteria struct {
option *HysteriaOption
client *core.Client
closeCh chan struct{} // for test
}
func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
@@ -51,7 +55,7 @@ func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata, opts .
return nil, err
}
return NewConn(tcpConn, h), nil
return NewConn(CN.NewRefConn(tcpConn, h), h), nil
}
func (h *Hysteria) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
@@ -59,7 +63,7 @@ func (h *Hysteria) ListenPacketContext(ctx context.Context, metadata *C.Metadata
if err != nil {
return nil, err
}
return newPacketConn(&hyPacketConn{udpConn}, h), nil
return newPacketConn(CN.NewRefPacketConn(&hyPacketConn{udpConn}, h), h), nil
}
func (h *Hysteria) genHdc(ctx context.Context, opts ...dialer.Option) utils.PacketDialer {
@@ -218,7 +222,7 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) {
if err != nil {
return nil, fmt.Errorf("hysteria %s create error: %w", addr, err)
}
return &Hysteria{
outbound := &Hysteria{
Base: &Base{
name: option.Name,
addr: addr,
@@ -231,7 +235,19 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) {
},
option: &option,
client: client,
}, nil
}
runtime.SetFinalizer(outbound, closeHysteria)
return outbound, nil
}
func closeHysteria(h *Hysteria) {
if h.client != nil {
_ = h.client.Close()
}
if h.closeCh != nil {
close(h.closeCh)
}
}
type hyPacketConn struct {
+5
View File
@@ -38,6 +38,8 @@ type Hysteria2 struct {
option *Hysteria2Option
client *hysteria2.Client
dialer proxydialer.SingDialer
closeCh chan struct{} // for test
}
type Hysteria2Option struct {
@@ -89,6 +91,9 @@ func closeHysteria2(h *Hysteria2) {
if h.client != nil {
_ = h.client.CloseWithError(errors.New("proxy removed"))
}
if h.closeCh != nil {
close(h.closeCh)
}
}
func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
+38
View File
@@ -0,0 +1,38 @@
package outbound
import (
"context"
"runtime"
"testing"
"time"
)
func TestHysteria2GC(t *testing.T) {
option := Hysteria2Option{}
option.Server = "127.0.0.1"
option.Ports = "200,204,401-429,501-503"
option.HopInterval = 30
option.Password = "password"
option.Obfs = "salamander"
option.ObfsPassword = "password"
option.SNI = "example.com"
option.ALPN = []string{"h3"}
hy, err := NewHysteria2(option)
if err != nil {
t.Error(err)
return
}
closeCh := make(chan struct{})
hy.closeCh = closeCh
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
hy = nil
runtime.GC()
select {
case <-closeCh:
return
case <-ctx.Done():
t.Error("timeout not GC")
}
}
+39
View File
@@ -0,0 +1,39 @@
package outbound
import (
"context"
"runtime"
"testing"
"time"
)
func TestHysteriaGC(t *testing.T) {
option := HysteriaOption{}
option.Server = "127.0.0.1"
option.Ports = "200,204,401-429,501-503"
option.Protocol = "udp"
option.Up = "1Mbps"
option.Down = "1Mbps"
option.HopInterval = 30
option.Obfs = "salamander"
option.SNI = "example.com"
option.ALPN = []string{"h3"}
hy, err := NewHysteria(option)
if err != nil {
t.Error(err)
return
}
closeCh := make(chan struct{})
hy.closeCh = closeCh
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
hy = nil
runtime.GC()
select {
case <-closeCh:
return
case <-ctx.Done():
t.Error("timeout not GC")
}
}
-2
View File
@@ -26,7 +26,6 @@ import (
wireguard "github.com/metacubex/sing-wireguard"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/debug"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
@@ -456,7 +455,6 @@ func closeWireGuard(w *WireGuard) {
if w.device != nil {
w.device.Close()
}
_ = common.Close(w.tunDevice)
if w.closeCh != nil {
close(w.closeCh)
}
@@ -29,6 +29,7 @@ func TestWireGuardGC(t *testing.T) {
err = wg.init(ctx)
if err != nil {
t.Error(err)
return
}
// must do a small sleep before test GC
// because it maybe deadlocks if w.device.Close call too fast after w.device.Start
+1 -1
View File
@@ -27,7 +27,7 @@ require (
github.com/metacubex/sing-shadowsocks2 v0.2.2
github.com/metacubex/sing-tun v0.2.7-0.20240729131039-ed03f557dee1
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9
github.com/metacubex/sing-wireguard v0.0.0-20240618022557-a6efaa37127a
github.com/metacubex/sing-wireguard v0.0.0-20240826061955-1e4e67afe5cd
github.com/metacubex/tfo-go v0.0.0-20240821025650-e9be0afd5e7d
github.com/metacubex/utls v1.6.6
github.com/miekg/dns v1.1.62
+2 -2
View File
@@ -120,8 +120,8 @@ github.com/metacubex/sing-tun v0.2.7-0.20240729131039-ed03f557dee1 h1:ypfofGDZbP
github.com/metacubex/sing-tun v0.2.7-0.20240729131039-ed03f557dee1/go.mod h1:olbEx9yVcaw5tHTNlRamRoxmMKcvDvcVS1YLnQGzvWE=
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 h1:OAXiCosqY8xKDp3pqTW3qbrCprZ1l6WkrXSFSCwyY4I=
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY=
github.com/metacubex/sing-wireguard v0.0.0-20240618022557-a6efaa37127a h1:NpSGclHJUYndUwBmyIpFBSoBVg8PoVX7QQKhYg0DjM0=
github.com/metacubex/sing-wireguard v0.0.0-20240618022557-a6efaa37127a/go.mod h1:uY+BYb0UEknLrqvbGcwi9i++KgrKxsurysgI6G1Pveo=
github.com/metacubex/sing-wireguard v0.0.0-20240826061955-1e4e67afe5cd h1:r7alry8u4qlUFLNMwGvG1A8ZcfPM6AMSmrm6E2yKdB4=
github.com/metacubex/sing-wireguard v0.0.0-20240826061955-1e4e67afe5cd/go.mod h1:uY+BYb0UEknLrqvbGcwi9i++KgrKxsurysgI6G1Pveo=
github.com/metacubex/tfo-go v0.0.0-20240821025650-e9be0afd5e7d h1:j9LtzkYstLFoNvXW824QQeN7Y26uPL5249kzWKbzO9U=
github.com/metacubex/tfo-go v0.0.0-20240821025650-e9be0afd5e7d/go.mod h1:c7bVFM9f5+VzeZ/6Kg77T/jrg1Xp8QpqlSHvG/aXVts=
github.com/metacubex/utls v1.6.6 h1:3D12YKHTf2Z41UPhQU2dWerNWJ5TVQD9gKoQ+H+iLC8=
+4
View File
@@ -101,6 +101,7 @@ func ApplyConfig(cfg *config.Config, force bool) {
updateNTP(cfg.NTP)
updateDNS(cfg.DNS, cfg.General.IPv6)
updateListeners(cfg.General, cfg.Listeners, force)
updateTun(cfg.General) // tun should not care "force"
updateIPTables(cfg)
updateTunnels(cfg.Tunnels)
@@ -198,6 +199,9 @@ func updateListeners(general *config.General, listeners map[string]C.InboundList
listener.ReCreateShadowSocks(general.ShadowSocksConfig, tunnel.Tunnel)
listener.ReCreateVmess(general.VmessConfig, tunnel.Tunnel)
listener.ReCreateTuic(general.TuicServer, tunnel.Tunnel)
}
func updateTun(general *config.General) {
listener.ReCreateTun(general.Tun, tunnel.Tunnel)
}
+4 -1
View File
@@ -289,7 +289,10 @@ func (c *Client) DialUDP(dialer utils.PacketDialer) (UDPConn, error) {
func (c *Client) Close() error {
c.reconnectMutex.Lock()
defer c.reconnectMutex.Unlock()
err := c.quicSession.CloseWithError(closeErrorCodeGeneric, "")
var err error
if c.quicSession != nil {
err = c.quicSession.CloseWithError(closeErrorCodeGeneric, "")
}
c.closed = true
return err
}
@@ -18,7 +18,7 @@ end
m = Map(appname)
-- [[ Haproxy Settings ]]--
s = m:section(TypedSection, "global_haproxy")
s = m:section(TypedSection, "global_haproxy", translate("Basic Settings"))
s.anonymous = true
s:append(Template(appname .. "/haproxy/status"))
@@ -56,6 +56,18 @@ o:value("tcp", "TCP")
o:value("passwall_logic", translate("URL Test") .. string.format("(passwall %s)", translate("Inner implement")))
o:depends("balancing_enable", true)
---- Passwall Inner implement Probe URL
o = s:option(Value, "health_probe_url", translate("Probe URL"))
o.default = "https://www.google.com/generate_204"
o:value("https://cp.cloudflare.com/", "Cloudflare")
o:value("https://www.gstatic.com/generate_204", "Gstatic")
o:value("https://www.google.com/generate_204", "Google")
o:value("https://www.youtube.com/generate_204", "YouTube")
o:value("https://connect.rom.miui.com/generate_204", "MIUI (CN)")
o:value("https://connectivitycheck.platform.hicloud.com/generate_204", "HiCloud (CN)")
o.description = translate("The URL used to detect the connection status.")
o:depends("health_check_type", "passwall_logic")
---- Health Check Inter
o = s:option(Value, "health_check_inter", translate("Health Check Inter"), translate("Units:seconds"))
o.default = "60"
@@ -69,7 +81,7 @@ end
o:depends("health_check_type", "passwall_logic")
-- [[ Balancing Settings ]]--
s = m:section(TypedSection, "haproxy_config", "",
s = m:section(TypedSection, "haproxy_config", translate("Node List"),
"<font color='red'>" ..
translate("Add a node, Export Of Multi WAN Only support Multi Wan. Load specific gravity range 1-256. Multiple primary servers can be load balanced, standby will only be enabled when the primary server is offline! Multiple groups can be set, Haproxy port same one for each group.") ..
"\n" .. translate("Note that the node configuration parameters for load balancing must be consistent when use TCP health check type, otherwise it cannot be used normally!") ..
@@ -220,3 +220,11 @@ listen console
f_out:write("\n" .. string.format(str, console_port, (console_user and console_user ~= "" and console_password and console_password ~= "") and "stats auth " .. console_user .. ":" .. console_password or ""))
f_out:close()
--passwall内置健康检查URL
if health_check_type == "passwall_logic" then
local probeUrl = uci:get(appname, "@global_haproxy[0]", "health_probe_url") or "https://www.google.com/generate_204"
local f_url = io.open(haproxy_path .. "/Probe_URL", "w")
f_url:write(probeUrl)
f_url:close()
end
@@ -4,7 +4,17 @@ listen_address=$1
listen_port=$2
server_address=$3
server_port=$4
status=$(/usr/bin/curl -I -o /dev/null -skL -x socks5h://${server_address}:${server_port} --connect-timeout 3 --retry 3 -w %{http_code} "https://www.google.com/generate_204")
probe_file="/tmp/etc/passwall/haproxy/Probe_URL"
probeUrl="https://www.google.com/generate_204"
if [ -f "$probe_file" ]; then
firstLine=$(head -n 1 "$probe_file" | tr -d ' \t')
if [ -n "$firstLine" ]; then
probeUrl="$firstLine"
fi
fi
status=$(/usr/bin/curl -I -o /dev/null -skL -x socks5h://${server_address}:${server_port} --connect-timeout 3 --retry 3 -w %{http_code} "${probeUrl}")
case "$status" in
204|\
200)
+81 -36
View File
@@ -17,6 +17,12 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "adler2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "aead"
version = "0.5.2"
@@ -241,7 +247,7 @@ dependencies = [
"cc",
"cfg-if",
"libc",
"miniz_oxide",
"miniz_oxide 0.7.4",
"object",
"rustc-demangle",
]
@@ -308,9 +314,9 @@ dependencies = [
[[package]]
name = "blake3"
version = "1.5.3"
version = "1.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9ec96fe9a81b5e365f9db71fe00edc4fe4ca2cc7dcb7861f0603012a7caa210"
checksum = "d82033247fd8e890df8f740e407ad4d038debb9eb1f40533fffb32e7d17dc6f7"
dependencies = [
"arrayref",
"arrayvec",
@@ -450,12 +456,13 @@ dependencies = [
[[package]]
name = "cc"
version = "1.1.6"
version = "1.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f"
checksum = "50d2eb3cd3d1bf4529e31c215ee6f93ec5a3d536d9f578f93d9d33ee19562932"
dependencies = [
"jobserver",
"libc",
"shlex",
]
[[package]]
@@ -947,12 +954,12 @@ dependencies = [
[[package]]
name = "flate2"
version = "1.0.31"
version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920"
checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253"
dependencies = [
"crc32fast",
"miniz_oxide",
"miniz_oxide 0.8.0",
]
[[package]]
@@ -1731,7 +1738,7 @@ dependencies = [
"socket2",
"widestring",
"windows-sys 0.48.0",
"winreg 0.50.0",
"winreg",
]
[[package]]
@@ -2019,6 +2026,15 @@ dependencies = [
"adler",
]
[[package]]
name = "miniz_oxide"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
dependencies = [
"adler2",
]
[[package]]
name = "mio"
version = "0.8.11"
@@ -2700,9 +2716,9 @@ checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
[[package]]
name = "reqwest"
version = "0.12.5"
version = "0.12.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37"
checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63"
dependencies = [
"async-compression",
"base64 0.22.1",
@@ -2747,7 +2763,7 @@ dependencies = [
"wasm-bindgen-futures",
"web-sys",
"webpki-roots 0.26.3",
"winreg 0.52.0",
"windows-registry",
]
[[package]]
@@ -3106,9 +3122,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.125"
version = "1.0.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed"
checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad"
dependencies = [
"indexmap",
"itoa",
@@ -3332,6 +3348,12 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.2"
@@ -3509,6 +3531,9 @@ name = "sync_wrapper"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
dependencies = [
"futures-core",
]
[[package]]
name = "synstructure"
@@ -3529,20 +3554,20 @@ checksum = "c4008983d29e823b1415f5f12732d5c9a44059795fb6218262cc0185668851e2"
[[package]]
name = "system-configuration"
version = "0.5.1"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
dependencies = [
"bitflags 1.3.2",
"bitflags 2.6.0",
"core-foundation",
"system-configuration-sys",
]
[[package]]
name = "system-configuration-sys"
version = "0.5.0"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
dependencies = [
"core-foundation-sys",
"libc",
@@ -3877,9 +3902,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "tun2"
version = "2.0.6"
version = "2.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5254ad58e460ff4d17b593d32f1be9ab28c80e96eec143e6e8237018b13bcae1"
checksum = "c1576993bcdccd110d21278396df090cb29219d296a8b8daa697442efdaab0c6"
dependencies = [
"bytes",
"cfg-if",
@@ -3894,7 +3919,7 @@ dependencies = [
"tokio",
"tokio-util",
"windows-sys 0.59.0",
"wintun",
"wintun-bindings",
]
[[package]]
@@ -4196,6 +4221,26 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-registry"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
dependencies = [
"windows-result",
"windows-strings",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-result"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-service"
version = "0.7.0"
@@ -4207,6 +4252,16 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "windows-strings"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
dependencies = [
"windows-result",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
@@ -4366,26 +4421,16 @@ dependencies = [
]
[[package]]
name = "winreg"
version = "0.52.0"
name = "wintun-bindings"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
dependencies = [
"cfg-if",
"windows-sys 0.48.0",
]
[[package]]
name = "wintun"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b196f9328341b035820c54beebca487823e2e20a5977f284f2af2a0ee8f04400"
checksum = "79260cdfee91a3de3a0fe0f04b81b695e69c68b170cd6a643746904a8c14da63"
dependencies = [
"c2rust-bitfields",
"libloading",
"log",
"thiserror",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -186,7 +186,7 @@ flate2 = { version = "1.0", optional = true }
brotli = { version = "6.0", optional = true }
zstd = { version = "0.13", optional = true }
tun2 = { version = "2.0.6", optional = true, default-features = false, features = [
tun2 = { version = "2.0.8", optional = true, default-features = false, features = [
"async",
] }
etherparse = { version = "0.15", optional = true }
-1
View File
@@ -180,7 +180,6 @@ func (w *WireGuard) Close() error {
if w.pauseCallback != nil {
w.pauseManager.UnregisterCallback(w.pauseCallback)
}
w.tunDevice.Close()
return nil
}
+2 -6
View File
@@ -230,17 +230,13 @@ func (w *StackDevice) Events() <-chan wgTun.Event {
}
func (w *StackDevice) Close() error {
select {
case <-w.done:
return os.ErrClosed
default:
}
close(w.done)
close(w.events)
w.stack.Close()
for _, endpoint := range w.stack.CleanupEndpoints() {
endpoint.Abort()
}
w.stack.Wait()
close(w.done)
return nil
}
+47 -44
View File
@@ -6,6 +6,7 @@ import (
"net"
"net/netip"
"os"
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
@@ -21,14 +22,16 @@ import (
var _ Device = (*SystemDevice)(nil)
type SystemDevice struct {
dialer N.Dialer
device tun.Tun
batchDevice tun.LinuxTUN
name string
mtu int
events chan wgTun.Event
addr4 netip.Addr
addr6 netip.Addr
dialer N.Dialer
device tun.Tun
batchDevice tun.LinuxTUN
name string
mtu uint32
inet4Addresses []netip.Prefix
inet6Addresses []netip.Prefix
gso bool
events chan wgTun.Event
closeOnce sync.Once
}
func NewSystemDevice(router adapter.Router, interfaceName string, localPrefixes []netip.Prefix, mtu uint32, gso bool) (*SystemDevice, error) {
@@ -44,43 +47,17 @@ func NewSystemDevice(router adapter.Router, interfaceName string, localPrefixes
if interfaceName == "" {
interfaceName = tun.CalculateInterfaceName("wg")
}
tunInterface, err := tun.New(tun.Options{
Name: interfaceName,
Inet4Address: inet4Addresses,
Inet6Address: inet6Addresses,
MTU: mtu,
GSO: gso,
})
if err != nil {
return nil, err
}
var inet4Address netip.Addr
var inet6Address netip.Addr
if len(inet4Addresses) > 0 {
inet4Address = inet4Addresses[0].Addr()
}
if len(inet6Addresses) > 0 {
inet6Address = inet6Addresses[0].Addr()
}
var batchDevice tun.LinuxTUN
if gso {
batchTUN, isBatchTUN := tunInterface.(tun.LinuxTUN)
if !isBatchTUN {
return nil, E.New("GSO is not supported on current platform")
}
batchDevice = batchTUN
}
return &SystemDevice{
dialer: common.Must1(dialer.NewDefault(router, option.DialerOptions{
BindInterface: interfaceName,
})),
device: tunInterface,
batchDevice: batchDevice,
name: interfaceName,
mtu: int(mtu),
events: make(chan wgTun.Event),
addr4: inet4Address,
addr6: inet6Address,
name: interfaceName,
mtu: mtu,
inet4Addresses: inet4Addresses,
inet6Addresses: inet6Addresses,
gso: gso,
events: make(chan wgTun.Event),
}, nil
}
@@ -93,14 +70,39 @@ func (w *SystemDevice) ListenPacket(ctx context.Context, destination M.Socksaddr
}
func (w *SystemDevice) Inet4Address() netip.Addr {
return w.addr4
if len(w.inet4Addresses) == 0 {
return netip.Addr{}
}
return w.inet4Addresses[0].Addr()
}
func (w *SystemDevice) Inet6Address() netip.Addr {
return w.addr6
if len(w.inet6Addresses) == 0 {
return netip.Addr{}
}
return w.inet6Addresses[0].Addr()
}
func (w *SystemDevice) Start() error {
tunInterface, err := tun.New(tun.Options{
Name: w.name,
Inet4Address: w.inet4Addresses,
Inet6Address: w.inet6Addresses,
MTU: w.mtu,
GSO: w.gso,
})
if err != nil {
return err
}
w.device = tunInterface
if w.gso {
batchTUN, isBatchTUN := tunInterface.(tun.LinuxTUN)
if !isBatchTUN {
tunInterface.Close()
return E.New("GSO is not supported on current platform")
}
w.batchDevice = batchTUN
}
w.events <- wgTun.EventUp
return nil
}
@@ -143,7 +145,7 @@ func (w *SystemDevice) Flush() error {
}
func (w *SystemDevice) MTU() (int, error) {
return w.mtu, nil
return int(w.mtu), nil
}
func (w *SystemDevice) Name() (string, error) {
@@ -155,6 +157,7 @@ func (w *SystemDevice) Events() <-chan wgTun.Event {
}
func (w *SystemDevice) Close() error {
close(w.events)
return w.device.Close()
}
@@ -164,14 +164,15 @@ function check_site(host, port)
end
function get_ip_geo_info()
local result = luci.sys.exec('curl --retry 3 -m 10 -LfsA "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36" https://ip-api.com/json/')
local result = luci.sys.exec('curl --retry 3 -m 10 -LfsA "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36" http://ip-api.com/json/')
local json = require "luci.jsonc"
local info = json.parse(result)
return {
flag = string.lower(info.countryCode) or "un",
country = get_country_name(info.countryCode) or "Unknown",
ip = info.query
ip = info.query,
isp = info.isp
}
end
@@ -198,6 +199,7 @@ function check_ip()
e.ip = geo_info.ip
e.flag = geo_info.flag
e.country = geo_info.country
e.isp = geo_info.isp
e.baidu = check_site('www.baidu.com', port)
e.taobao = check_site('www.taobao.com', port)
e.google = check_site('www.google.com', port)
@@ -94,7 +94,7 @@ function resize() {
function write_status(data) {
document.querySelector(".flag img").src = _ASSETS + "flags/" + data.flag + ".svg";
document.querySelector(".status-info").innerHTML = data.ip + "<br>" + data.country;
document.querySelector(".status-info").innerHTML = data.ip + "<br>" + data.country + "&nbsp;" + data.isp;
document.querySelector(".i1").src = data.baidu ? _ASSETS + "img/site_icon_01.png" : _ASSETS + "img/site_icon1_01.png";
document.querySelector(".i2").src = data.taobao ? _ASSETS + "img/site_icon_02.png" : _ASSETS + "img/site_icon1_02.png";
document.querySelector(".i3").src = data.google ? _ASSETS + "img/site_icon_03.png" : _ASSETS + "img/site_icon1_03.png";
@@ -112,7 +112,7 @@ XHR.poll(5, CHECK_IP_URL, null,
);
document.addEventListener('DOMContentLoaded', function() {
setTimeout("resize()",10)
setTimeout("resize()",100)
fetch(CHECK_IP_URL)
.then(response => response.json())
.then(data => {
@@ -18,7 +18,7 @@ end
m = Map(appname)
-- [[ Haproxy Settings ]]--
s = m:section(TypedSection, "global_haproxy")
s = m:section(TypedSection, "global_haproxy", translate("Basic Settings"))
s.anonymous = true
s:append(Template(appname .. "/haproxy/status"))
@@ -56,6 +56,18 @@ o:value("tcp", "TCP")
o:value("passwall_logic", translate("URL Test") .. string.format("(passwall %s)", translate("Inner implement")))
o:depends("balancing_enable", true)
---- Passwall Inner implement Probe URL
o = s:option(Value, "health_probe_url", translate("Probe URL"))
o.default = "https://www.google.com/generate_204"
o:value("https://cp.cloudflare.com/", "Cloudflare")
o:value("https://www.gstatic.com/generate_204", "Gstatic")
o:value("https://www.google.com/generate_204", "Google")
o:value("https://www.youtube.com/generate_204", "YouTube")
o:value("https://connect.rom.miui.com/generate_204", "MIUI (CN)")
o:value("https://connectivitycheck.platform.hicloud.com/generate_204", "HiCloud (CN)")
o.description = translate("The URL used to detect the connection status.")
o:depends("health_check_type", "passwall_logic")
---- Health Check Inter
o = s:option(Value, "health_check_inter", translate("Health Check Inter"), translate("Units:seconds"))
o.default = "60"
@@ -69,7 +81,7 @@ end
o:depends("health_check_type", "passwall_logic")
-- [[ Balancing Settings ]]--
s = m:section(TypedSection, "haproxy_config", "",
s = m:section(TypedSection, "haproxy_config", translate("Node List"),
"<font color='red'>" ..
translate("Add a node, Export Of Multi WAN Only support Multi Wan. Load specific gravity range 1-256. Multiple primary servers can be load balanced, standby will only be enabled when the primary server is offline! Multiple groups can be set, Haproxy port same one for each group.") ..
"\n" .. translate("Note that the node configuration parameters for load balancing must be consistent when use TCP health check type, otherwise it cannot be used normally!") ..
@@ -220,3 +220,11 @@ listen console
f_out:write("\n" .. string.format(str, console_port, (console_user and console_user ~= "" and console_password and console_password ~= "") and "stats auth " .. console_user .. ":" .. console_password or ""))
f_out:close()
--passwall内置健康检查URL
if health_check_type == "passwall_logic" then
local probeUrl = uci:get(appname, "@global_haproxy[0]", "health_probe_url") or "https://www.google.com/generate_204"
local f_url = io.open(haproxy_path .. "/Probe_URL", "w")
f_url:write(probeUrl)
f_url:close()
end
@@ -4,7 +4,17 @@ listen_address=$1
listen_port=$2
server_address=$3
server_port=$4
status=$(/usr/bin/curl -I -o /dev/null -skL -x socks5h://${server_address}:${server_port} --connect-timeout 3 --retry 3 -w %{http_code} "https://www.google.com/generate_204")
probe_file="/tmp/etc/passwall/haproxy/Probe_URL"
probeUrl="https://www.google.com/generate_204"
if [ -f "$probe_file" ]; then
firstLine=$(head -n 1 "$probe_file" | tr -d ' \t')
if [ -n "$firstLine" ]; then
probeUrl="$firstLine"
fi
fi
status=$(/usr/bin/curl -I -o /dev/null -skL -x socks5h://${server_address}:${server_port} --connect-timeout 3 --retry 3 -w %{http_code} "${probeUrl}")
case "$status" in
204|\
200)
+4 -4
View File
@@ -21,22 +21,22 @@ define Download/geoip
HASH:=2c6b6ee15f4593a7b54853c8db5a2b881986ae405122d9fa1427f33c26316ff3
endef
GEOSITE_VER:=20240823035651
GEOSITE_VER:=20240826041130
GEOSITE_FILE:=dlc.dat.$(GEOSITE_VER)
define Download/geosite
URL:=https://github.com/v2fly/domain-list-community/releases/download/$(GEOSITE_VER)/
URL_FILE:=dlc.dat
FILE:=$(GEOSITE_FILE)
HASH:=e9197238b1b4e6e6ae9f35dfc40484936093562bef354501981bf8d5e29950da
HASH:=f154374d3eb0cafcb1d37d15d97a8db6d57ebf3436e519a6747279a39cf4f7ab
endef
GEOSITE_IRAN_VER:=202408190030
GEOSITE_IRAN_VER:=202408260030
GEOSITE_IRAN_FILE:=iran.dat.$(GEOSITE_IRAN_VER)
define Download/geosite-ir
URL:=https://github.com/bootmortis/iran-hosted-domains/releases/download/$(GEOSITE_IRAN_VER)/
URL_FILE:=iran.dat
FILE:=$(GEOSITE_IRAN_FILE)
HASH:=43f48f650b5259f82024a995990d2c5b55e79123773ce5c4ca332d8cb24707e4
HASH:=d95bd88c33b41514400ced2ec117834dd325c24a46c04e82f8c04ef040648f14
endef
define Package/v2ray-geodata/template
+13
View File
@@ -5,6 +5,7 @@ using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
@@ -703,6 +704,18 @@ namespace ServiceLib.Common
return systemHosts;
}
public static string GetExeName(string name)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return $"{name}.exe";
}
else
{
return name;
}
}
#endregion
#region TempPath
@@ -116,7 +116,7 @@ namespace ServiceLib.Handler
foreach (Process p in existing)
{
string? path = p.MainModule?.FileName;
if (path == $"{Utils.GetBinPath(vName, it.coreType.ToString())}.exe")
if (path == Utils.GetExeName(Utils.GetBinPath(vName, it.coreType.ToString())))
{
KillProcess(p);
}
@@ -151,7 +151,7 @@ namespace ServiceLib.Handler
string fileName = string.Empty;
foreach (string name in coreInfo.coreExes)
{
string vName = $"{name}.exe";
string vName = Utils.GetExeName(name);
vName = Utils.GetBinPath(vName, coreInfo.coreType.ToString());
if (File.Exists(vName))
{
+1 -1
View File
@@ -187,7 +187,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.gotoolchain || '1.22' }}
go-version: ${{ matrix.gotoolchain || '1.23' }}
check-latest: true
- name: Get project dependencies
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
go-version: '1.23'
check-latest: true
- name: Restore Cache
uses: actions/cache/restore@v4
+2
View File
@@ -22,6 +22,8 @@
[Project X Channel](https://t.me/projectXtls)
[Project VLESS](https://t.me/projectVless) (non-Chinese)
## Installation
- Linux Script
+4 -1
View File
@@ -118,6 +118,9 @@ func (w *ServerWorker) handleStatusKeepAlive(meta *FrameMetadata, reader *buf.Bu
}
func (w *ServerWorker) handleStatusNew(ctx context.Context, meta *FrameMetadata, reader *buf.BufferedReader) error {
// deep-clone outbounds because it is going to be mutated concurrently
// (Target and OriginalTarget)
ctx = session.ContextCloneOutbounds(ctx)
errors.LogInfo(ctx, "received request for ", meta.Target)
{
msg := &log.AccessMessage{
@@ -170,7 +173,7 @@ func (w *ServerWorker) handleStatusNew(ctx context.Context, meta *FrameMetadata,
b.Release()
mb = nil
}
errors.LogInfoInner(ctx, err,"XUDP hit ", meta.GlobalID)
errors.LogInfoInner(ctx, err, "XUDP hit ", meta.GlobalID)
}
if mb != nil {
ctx = session.ContextWithTimeoutOnly(ctx, true)
+124
View File
@@ -0,0 +1,124 @@
package mux_test
import (
"context"
"testing"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/mux"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/transport"
"github.com/xtls/xray-core/transport/pipe"
)
func newLinkPair() (*transport.Link, *transport.Link) {
opt := pipe.WithoutSizeLimit()
uplinkReader, uplinkWriter := pipe.New(opt)
downlinkReader, downlinkWriter := pipe.New(opt)
uplink := &transport.Link{
Reader: uplinkReader,
Writer: downlinkWriter,
}
downlink := &transport.Link{
Reader: downlinkReader,
Writer: uplinkWriter,
}
return uplink, downlink
}
type TestDispatcher struct {
OnDispatch func(ctx context.Context, dest net.Destination) (*transport.Link, error)
}
func (d *TestDispatcher) Dispatch(ctx context.Context, dest net.Destination) (*transport.Link, error) {
return d.OnDispatch(ctx, dest)
}
func (d *TestDispatcher) DispatchLink(ctx context.Context, destination net.Destination, outbound *transport.Link) error {
return nil
}
func (d *TestDispatcher) Start() error {
return nil
}
func (d *TestDispatcher) Close() error {
return nil
}
func (*TestDispatcher) Type() interface{} {
return routing.DispatcherType()
}
func TestRegressionOutboundLeak(t *testing.T) {
originalOutbounds := []*session.Outbound{{}}
serverCtx := session.ContextWithOutbounds(context.Background(), originalOutbounds)
websiteUplink, websiteDownlink := newLinkPair()
dispatcher := TestDispatcher{
OnDispatch: func(ctx context.Context, dest net.Destination) (*transport.Link, error) {
// emulate what DefaultRouter.Dispatch does, and mutate something on the context
ob := session.OutboundsFromContext(ctx)[0]
ob.Target = dest
return websiteDownlink, nil
},
}
muxServerUplink, muxServerDownlink := newLinkPair()
_, err := mux.NewServerWorker(serverCtx, &dispatcher, muxServerUplink)
common.Must(err)
client, err := mux.NewClientWorker(*muxServerDownlink, mux.ClientStrategy{})
common.Must(err)
clientCtx := session.ContextWithOutbounds(context.Background(), []*session.Outbound{{
Target: net.TCPDestination(net.DomainAddress("www.example.com"), 80),
}})
muxClientUplink, muxClientDownlink := newLinkPair()
ok := client.Dispatch(clientCtx, muxClientUplink)
if !ok {
t.Error("failed to dispatch")
}
{
b := buf.FromBytes([]byte("hello"))
common.Must(muxClientDownlink.Writer.WriteMultiBuffer(buf.MultiBuffer{b}))
}
resMb, err := websiteUplink.Reader.ReadMultiBuffer()
common.Must(err)
res := resMb.String()
if res != "hello" {
t.Error("upload: ", res)
}
{
b := buf.FromBytes([]byte("world"))
common.Must(websiteUplink.Writer.WriteMultiBuffer(buf.MultiBuffer{b}))
}
resMb, err = muxClientDownlink.Reader.ReadMultiBuffer()
common.Must(err)
res = resMb.String()
if res != "world" {
t.Error("download: ", res)
}
outbounds := session.OutboundsFromContext(serverCtx)
if outbounds[0] != originalOutbounds[0] {
t.Error("outbound got reassigned: ", outbounds[0])
}
if outbounds[0].Target.Address != nil {
t.Error("outbound target got leaked: ", outbounds[0].Target.String())
}
}
+16
View File
@@ -40,6 +40,22 @@ func ContextWithOutbounds(ctx context.Context, outbounds []*Outbound) context.Co
return context.WithValue(ctx, outboundSessionKey, outbounds)
}
func ContextCloneOutbounds(ctx context.Context) context.Context {
outbounds := OutboundsFromContext(ctx)
newOutbounds := make([]*Outbound, len(outbounds))
for i, ob := range outbounds {
if ob == nil {
continue
}
// copy outbound by value
v := *ob
newOutbounds[i] = &v
}
return ContextWithOutbounds(ctx, newOutbounds)
}
func OutboundsFromContext(ctx context.Context) []*Outbound {
if outbounds, ok := ctx.Value(outboundSessionKey).([]*Outbound); ok {
return outbounds
+1 -1
View File
@@ -19,7 +19,7 @@ require (
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771
github.com/stretchr/testify v1.9.0
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e
github.com/vishvananda/netlink v1.2.1
github.com/vishvananda/netlink v1.3.0
github.com/xtls/reality v0.0.0-20240712055506-48f0b2d5ed6d
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.26.0
+2 -2
View File
@@ -160,8 +160,8 @@ github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU=
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
github.com/vishvananda/netlink v1.2.1 h1:pfLv/qlJUwOTPvtWREA7c3PI4u81YkqZw1DYhI2HmLA=
github.com/vishvananda/netlink v1.2.1/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/xtls/reality v0.0.0-20240712055506-48f0b2d5ed6d h1:+B97uD9uHLgAAulhigmys4BVwZZypzK7gPN3WtpgRJg=
+2 -6
View File
@@ -49,9 +49,6 @@ bool ReadConfig() {
config_impl->Read("fast_open", &FLAGS_tcp_fastopen);
config_impl->Read("fast_open_connect", &FLAGS_tcp_fastopen_connect);
#ifdef __linux__
config_impl->Read("congestion_algorithm", &FLAGS_congestion_algorithm);
#endif
config_impl->Read("doh_url", &FLAGS_doh_url);
config_impl->Read("dot_host", &FLAGS_dot_host);
config_impl->Read("connect_timeout", &FLAGS_connect_timeout);
@@ -62,6 +59,7 @@ bool ReadConfig() {
config_impl->Read("tcp_keep_alive_cnt", &FLAGS_tcp_keep_alive_cnt);
config_impl->Read("tcp_keep_alive_idle_timeout", &FLAGS_tcp_keep_alive_idle_timeout);
config_impl->Read("tcp_keep_alive_interval", &FLAGS_tcp_keep_alive_interval);
config_impl->Read("tcp_congestion_algorithm", &FLAGS_tcp_congestion_algorithm);
/* optional tls fields */
config_impl->Read("cacert", &FLAGS_cacert);
@@ -121,9 +119,6 @@ bool SaveConfig() {
all_fields_written &= config_impl->Write("fast_open", FLAGS_tcp_fastopen);
all_fields_written &= config_impl->Write("fast_open_connect", FLAGS_tcp_fastopen_connect);
static_cast<void>(config_impl->Delete("threads"));
#ifdef __linux__
all_fields_written &= config_impl->Write("congestion_algorithm", FLAGS_congestion_algorithm);
#endif
all_fields_written &= config_impl->Write("doh_url", FLAGS_doh_url);
all_fields_written &= config_impl->Write("dot_host", FLAGS_dot_host);
all_fields_written &= config_impl->Write("timeout", FLAGS_connect_timeout);
@@ -135,6 +130,7 @@ bool SaveConfig() {
all_fields_written &= config_impl->Write("tcp_keep_alive_cnt", FLAGS_tcp_keep_alive_cnt);
all_fields_written &= config_impl->Write("tcp_keep_alive_idle_timeout", FLAGS_tcp_keep_alive_idle_timeout);
all_fields_written &= config_impl->Write("tcp_keep_alive_interval", FLAGS_tcp_keep_alive_interval);
all_fields_written &= config_impl->Write("tcp_congestion_algorithm", FLAGS_tcp_congestion_algorithm);
all_fields_written &= config_impl->Write("cacert", FLAGS_cacert);
all_fields_written &= config_impl->Write("capath", FLAGS_capath);
+1 -3
View File
@@ -8,9 +8,6 @@
ABSL_FLAG(bool, ipv6_mode, true, "Resolve names to IPv6 addresses");
ABSL_FLAG(bool, reuse_port, true, "Reuse the listening port");
#ifdef __linux__
ABSL_FLAG(std::string, congestion_algorithm, "", "TCP Congestion Algorithm");
#endif
ABSL_FLAG(bool, tcp_fastopen, false, "TCP fastopen");
ABSL_FLAG(bool, tcp_fastopen_connect, false, "TCP fastopen connect");
ABSL_FLAG(int32_t, connect_timeout, 0, "Connect timeout (in seconds)");
@@ -24,6 +21,7 @@ ABSL_FLAG(int32_t,
7200,
"The number of seconds a connection needs to be idle before TCP begins sending out keep-alive probes.");
ABSL_FLAG(int32_t, tcp_keep_alive_interval, 75, "The number of seconds between TCP keep-alive probes.");
ABSL_FLAG(std::string, tcp_congestion_algorithm, "", "TCP Congestion Algorithm (Linux Only)");
ABSL_FLAG(bool, redir_mode, false, "Enable TCP Redir mode support (linux only)");
ABSL_FLAG(std::string, doh_url, "", "Resolve host names over DoH");
+1 -3
View File
@@ -10,9 +10,6 @@
ABSL_DECLARE_FLAG(bool, ipv6_mode);
ABSL_DECLARE_FLAG(bool, reuse_port);
#ifdef __linux__
ABSL_DECLARE_FLAG(std::string, congestion_algorithm);
#endif
ABSL_DECLARE_FLAG(bool, tcp_fastopen);
ABSL_DECLARE_FLAG(bool, tcp_fastopen_connect);
// same with proxy_connect_timeout no need for proxy_read_timeout
@@ -25,6 +22,7 @@ ABSL_DECLARE_FLAG(bool, tcp_keep_alive);
ABSL_DECLARE_FLAG(int32_t, tcp_keep_alive_cnt);
ABSL_DECLARE_FLAG(int32_t, tcp_keep_alive_idle_timeout);
ABSL_DECLARE_FLAG(int32_t, tcp_keep_alive_interval);
ABSL_DECLARE_FLAG(std::string, tcp_congestion_algorithm);
ABSL_DECLARE_FLAG(bool, redir_mode);
ABSL_DECLARE_FLAG(std::string, doh_url);
+33 -2
View File
@@ -10,6 +10,7 @@
#include "core/logging.hpp"
#include "core/utils.hpp"
#include "gtk/utils.hpp"
#include "net/network.hpp"
OptionDialog::OptionDialog(const std::string& title, GtkWindow* parent, bool modal)
: impl_(GTK_DIALOG(gtk_dialog_new_with_buttons(title.c_str(),
@@ -27,12 +28,14 @@ OptionDialog::OptionDialog(const std::string& title, GtkWindow* parent, bool mod
auto tcp_keep_alive_interval_label = gtk_label_new(_("TCP keep alive interval"));
auto enable_post_quantum_kyber_label = gtk_label_new(_("Kyber post-quantum key agreement for TLS"));
auto tcp_congestion_algorithm = gtk_label_new(_("TCP Congestion Algorithm"));
gtk_grid_attach(grid, GTK_WIDGET(tcp_keep_alive_label), 0, 0, 1, 1);
gtk_grid_attach(grid, GTK_WIDGET(tcp_keep_alive_cnt_label), 0, 1, 1, 1);
gtk_grid_attach(grid, GTK_WIDGET(tcp_keep_alive_idle_timeout_label), 0, 2, 1, 1);
gtk_grid_attach(grid, GTK_WIDGET(tcp_keep_alive_interval_label), 0, 3, 1, 1);
gtk_grid_attach(grid, GTK_WIDGET(enable_post_quantum_kyber_label), 0, 4, 1, 1);
gtk_grid_attach(grid, GTK_WIDGET(tcp_congestion_algorithm), 0, 5, 1, 1);
tcp_keep_alive_ = GTK_CHECK_BUTTON(gtk_check_button_new());
tcp_keep_alive_cnt_ = GTK_ENTRY(gtk_entry_new());
@@ -40,11 +43,19 @@ OptionDialog::OptionDialog(const std::string& title, GtkWindow* parent, bool mod
tcp_keep_alive_interval_ = GTK_ENTRY(gtk_entry_new());
enable_post_quantum_kyber_ = GTK_CHECK_BUTTON(gtk_check_button_new());
algorithms_ = net::GetTCPAvailableCongestionAlgorithms();
tcp_congestion_algorithm_ = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new());
for (const auto& algorithm : algorithms_) {
gtk_combo_box_text_append_text(tcp_congestion_algorithm_, algorithm.c_str());
}
gtk_grid_attach(grid, GTK_WIDGET(tcp_keep_alive_), 1, 0, 1, 1);
gtk_grid_attach(grid, GTK_WIDGET(tcp_keep_alive_cnt_), 1, 1, 1, 1);
gtk_grid_attach(grid, GTK_WIDGET(tcp_keep_alive_idle_timeout_), 1, 2, 1, 1);
gtk_grid_attach(grid, GTK_WIDGET(tcp_keep_alive_interval_), 1, 3, 1, 1);
gtk_grid_attach(grid, GTK_WIDGET(enable_post_quantum_kyber_), 1, 4, 1, 1);
gtk_grid_attach(grid, GTK_WIDGET(tcp_congestion_algorithm_), 1, 5, 1, 1);
gtk_widget_set_margin_top(GTK_WIDGET(grid), 12);
gtk_widget_set_margin_bottom(GTK_WIDGET(grid), 12);
@@ -79,8 +90,8 @@ OptionDialog::OptionDialog(const std::string& title, GtkWindow* parent, bool mod
g_signal_connect(G_OBJECT(cancel_button_), "clicked", G_CALLBACK(*cancel_callback), this);
gtk_grid_attach(grid, GTK_WIDGET(okay_button_), 0, 5, 1, 1);
gtk_grid_attach(grid, GTK_WIDGET(cancel_button_), 1, 5, 1, 1);
gtk_grid_attach(grid, GTK_WIDGET(okay_button_), 0, 6, 1, 1);
gtk_grid_attach(grid, GTK_WIDGET(cancel_button_), 1, 6, 1, 1);
gtk_container_add(GTK_CONTAINER(gtk_dialog_get_content_area(impl_)), GTK_WIDGET(grid));
@@ -120,6 +131,19 @@ void OptionDialog::LoadChanges() {
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(enable_post_quantum_kyber_),
absl::GetFlag(FLAGS_enable_post_quantum_kyber));
auto algorithm = absl::GetFlag(FLAGS_tcp_congestion_algorithm);
unsigned int i;
for (i = 0; i < std::size(algorithms_); ++i) {
if (algorithm == algorithms_[i])
break;
}
// first is unset
if (i == std::size(algorithms_)) {
i = 0;
}
gtk_combo_box_set_active(GTK_COMBO_BOX(tcp_congestion_algorithm_), i);
}
bool OptionDialog::OnSave() {
@@ -143,5 +167,12 @@ bool OptionDialog::OnSave() {
absl::SetFlag(&FLAGS_enable_post_quantum_kyber, enable_post_quantum_kyber);
gchar* algorithm_cstr = gtk_combo_box_text_get_active_text(tcp_congestion_algorithm_);
if (algorithm_cstr == nullptr || std::string_view(algorithm_cstr).empty()) {
absl::SetFlag(&FLAGS_tcp_congestion_algorithm, std::string());
} else {
absl::SetFlag(&FLAGS_tcp_congestion_algorithm, algorithm_cstr);
}
return true;
}
+3
View File
@@ -6,6 +6,7 @@
#include <gtk/gtk.h>
#include <string>
#include <vector>
class OptionDialog {
public:
@@ -26,6 +27,8 @@ class OptionDialog {
GtkEntry* tcp_keep_alive_idle_timeout_;
GtkEntry* tcp_keep_alive_interval_;
GtkCheckButton* enable_post_quantum_kyber_;
GtkComboBoxText* tcp_congestion_algorithm_;
std::vector<std::string> algorithms_;
GtkButton* okay_button_;
GtkButton* cancel_button_;
+47 -43
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 1.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-07-09 09:55+0800\n"
"POT-Creation-Date: 2024-08-26 14:19+0800\n"
"PO-Revision-Date: 2023-09-15 11:18+0800\n"
"Last-Translator: Chilledheart <keeyou-cn@outlook.com>\n"
"Language-Team: Chilledheart <keeyou-cn@outlook.com>\n"
@@ -17,170 +17,174 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: option_dialog.cpp:27
#: option_dialog.cpp:25
msgid "TCP keep alive"
msgstr "TCP keep alive"
#: option_dialog.cpp:28
#: option_dialog.cpp:26
msgid "The number of TCP keep-alive probes"
msgstr "The number of TCP keep-alive probes"
#: option_dialog.cpp:29
#: option_dialog.cpp:27
msgid "TCP keep alive after idle"
msgstr "TCP keep alive after idle"
#: option_dialog.cpp:30
#: option_dialog.cpp:28
msgid "TCP keep alive interval"
msgstr "TCP keep alive interval"
#: option_dialog.cpp:32
#: option_dialog.cpp:30
msgid "Kyber post-quantum key agreement for TLS"
msgstr "Kyber post-quantum key agreement for TLS"
#: option_dialog.cpp:53
#: option_dialog.cpp:32
msgid "TCP Congestion Algorithm"
msgstr "TCP Congestion Algorithm"
#: option_dialog.cpp:76
msgid "Okay"
msgstr "Okay"
#: option_dialog.cpp:56
#: option_dialog.cpp:79
msgid "Cancel"
msgstr "Cancel"
#: yass.cpp:234
#: yass.cpp:232
msgid "Connected with conns: "
msgstr "Connected with conns: "
#: yass.cpp:236
#: yass.cpp:234
msgid "Connecting"
msgstr "Connecting"
#: yass.cpp:238
#: yass.cpp:236
msgid "Failed to connect due to "
msgstr "Failed to connect due to "
#: yass.cpp:240
#: yass.cpp:238
msgid "Disconnecting"
msgstr "Disconnecting"
#: yass.cpp:242
#: yass.cpp:240
msgid "Disconnected with "
msgstr "Disconnected with "
#: yass_window.cpp:67
#: yass_window.cpp:64
msgid "File"
msgstr "File"
#: yass_window.cpp:68 yass_window.cpp:311 yass_window.cpp:370
#: yass_window.cpp:65 yass_window.cpp:323 yass_window.cpp:383
msgid "Option..."
msgstr "Option..."
#: yass_window.cpp:69 yass_window.cpp:312 yass_window.cpp:371
#: yass_window.cpp:66 yass_window.cpp:324 yass_window.cpp:384
msgid "Exit"
msgstr "Exit"
#: yass_window.cpp:92
#: yass_window.cpp:89
msgid "Help"
msgstr "Help"
#: yass_window.cpp:93
#: yass_window.cpp:90
msgid "About..."
msgstr "About..."
#: yass_window.cpp:108
#: yass_window.cpp:105
msgid "Start"
msgstr "Start"
#: yass_window.cpp:114
#: yass_window.cpp:109
msgid "Stop"
msgstr "Stop"
#: yass_window.cpp:147
#: yass_window.cpp:131
msgid "Server Host"
msgstr "Server Host"
#: yass_window.cpp:148
#: yass_window.cpp:132
msgid "Server SNI"
msgstr "Server SNI"
#: yass_window.cpp:149
#: yass_window.cpp:133
msgid "Server Port"
msgstr "Server Port"
#: yass_window.cpp:150
#: yass_window.cpp:134
msgid "Username"
msgstr "Username"
#: yass_window.cpp:151
#: yass_window.cpp:135
msgid "Password"
msgstr "Password"
#: yass_window.cpp:152
#: yass_window.cpp:136
msgid "Cipher/Method"
msgstr "Cipher/Method"
#: yass_window.cpp:153
#: yass_window.cpp:137
msgid "Local Host"
msgstr "Local Host"
#: yass_window.cpp:154
#: yass_window.cpp:138
msgid "Local Port"
msgstr "Local Port"
#: yass_window.cpp:155
#: yass_window.cpp:139
msgid "DNS over HTTPS URL"
msgstr "DNS over HTTPS URL"
#: yass_window.cpp:156
#: yass_window.cpp:140
msgid "DNS over TLS Host"
msgstr "DNS over TLS Host"
#: yass_window.cpp:157
#: yass_window.cpp:141
msgid "Limit Rate"
msgstr "Limit Rate"
#: yass_window.cpp:158
#: yass_window.cpp:142
msgid "Timeout"
msgstr "Timeout"
#: yass_window.cpp:159
#: yass_window.cpp:143
msgid "Auto Start"
msgstr "Auto Start"
#: yass_window.cpp:160
#: yass_window.cpp:144
msgid "System Proxy"
msgstr "System Proxy"
#: yass_window.cpp:263
#: yass_window.cpp:275
msgid "READY"
msgstr "READY"
#: yass_window.cpp:336 yass_window.cpp:369
#: yass_window.cpp:348 yass_window.cpp:382
msgid "Show"
msgstr "Show"
#: yass_window.cpp:531
#: yass_window.cpp:544
msgid " tx rate: "
msgstr " tx rate: "
#: yass_window.cpp:534
#: yass_window.cpp:547
msgid " rx rate: "
msgstr " rx rate: "
#: yass_window.cpp:645
#: yass_window.cpp:658
msgid "YASS Option"
msgstr "YASS Option"
#: yass_window.cpp:656
#: yass_window.cpp:669
msgid "Last Change: "
msgstr "Last Change: "
#: yass_window.cpp:659
#: yass_window.cpp:672
msgid "Enabled Feature: "
msgstr "Enabled Feature: "
#: yass_window.cpp:662
#: yass_window.cpp:675
msgid "GUI Variant: "
msgstr "GUI Variant: "
#: yass_window.cpp:671
#: yass_window.cpp:684
msgid "official-site"
msgstr "official-site"
+47 -43
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 1.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-07-09 09:55+0800\n"
"POT-Creation-Date: 2024-08-26 14:19+0800\n"
"PO-Revision-Date: 2023-09-15 11:26+0800\n"
"Last-Translator: Chilledheart <keeyou-cn@outlook.com>\n"
"Language-Team: Chilledheart <keeyou-cn@outlook.com>\n"
@@ -18,170 +18,174 @@ msgstr ""
"X-Generator: Poedit 2.2.4\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#: option_dialog.cpp:27
#: option_dialog.cpp:25
msgid "TCP keep alive"
msgstr "TCP 保活"
#: option_dialog.cpp:28
#: option_dialog.cpp:26
msgid "The number of TCP keep-alive probes"
msgstr "TCP 保活测量数"
#: option_dialog.cpp:29
#: option_dialog.cpp:27
msgid "TCP keep alive after idle"
msgstr "TCP 在闲置以后的保活次数"
#: option_dialog.cpp:30
#: option_dialog.cpp:28
msgid "TCP keep alive interval"
msgstr "TCP 保活间隔"
#: option_dialog.cpp:32
#: option_dialog.cpp:30
msgid "Kyber post-quantum key agreement for TLS"
msgstr "TLS的Kyber后量子密钥协商"
#: option_dialog.cpp:53
#: option_dialog.cpp:32
msgid "TCP Congestion Algorithm"
msgstr "TCP 拥塞算法"
#: option_dialog.cpp:76
msgid "Okay"
msgstr "确认"
#: option_dialog.cpp:56
#: option_dialog.cpp:79
msgid "Cancel"
msgstr "取消"
#: yass.cpp:234
#: yass.cpp:232
msgid "Connected with conns: "
msgstr "已产生连接: "
#: yass.cpp:236
#: yass.cpp:234
msgid "Connecting"
msgstr "连接中"
#: yass.cpp:238
#: yass.cpp:236
msgid "Failed to connect due to "
msgstr "无法连接因为 "
#: yass.cpp:240
#: yass.cpp:238
msgid "Disconnecting"
msgstr "断开连接中"
#: yass.cpp:242
#: yass.cpp:240
msgid "Disconnected with "
msgstr "断开连接于服务器 "
#: yass_window.cpp:67
#: yass_window.cpp:64
msgid "File"
msgstr "文件"
#: yass_window.cpp:68 yass_window.cpp:311 yass_window.cpp:370
#: yass_window.cpp:65 yass_window.cpp:323 yass_window.cpp:383
msgid "Option..."
msgstr "选项..."
#: yass_window.cpp:69 yass_window.cpp:312 yass_window.cpp:371
#: yass_window.cpp:66 yass_window.cpp:324 yass_window.cpp:384
msgid "Exit"
msgstr "退出"
#: yass_window.cpp:92
#: yass_window.cpp:89
msgid "Help"
msgstr "帮助"
#: yass_window.cpp:93
#: yass_window.cpp:90
msgid "About..."
msgstr "关于..."
#: yass_window.cpp:108
#: yass_window.cpp:105
msgid "Start"
msgstr "启动"
#: yass_window.cpp:114
#: yass_window.cpp:109
msgid "Stop"
msgstr "停止"
#: yass_window.cpp:147
#: yass_window.cpp:131
msgid "Server Host"
msgstr "服务器域名"
#: yass_window.cpp:148
#: yass_window.cpp:132
msgid "Server SNI"
msgstr "服务器名称指示"
#: yass_window.cpp:149
#: yass_window.cpp:133
msgid "Server Port"
msgstr "服务器端口号"
#: yass_window.cpp:150
#: yass_window.cpp:134
msgid "Username"
msgstr "用户名"
#: yass_window.cpp:151
#: yass_window.cpp:135
msgid "Password"
msgstr "密码"
#: yass_window.cpp:152
#: yass_window.cpp:136
msgid "Cipher/Method"
msgstr "加密方式"
#: yass_window.cpp:153
#: yass_window.cpp:137
msgid "Local Host"
msgstr "本地域名"
#: yass_window.cpp:154
#: yass_window.cpp:138
msgid "Local Port"
msgstr "本地端口号"
#: yass_window.cpp:155
#: yass_window.cpp:139
msgid "DNS over HTTPS URL"
msgstr "基于 HTTPS 的 DNS (DoH) URL"
#: yass_window.cpp:156
#: yass_window.cpp:140
msgid "DNS over TLS Host"
msgstr "基于 TLS 的 DNS (DoT) 域名"
#: yass_window.cpp:157
#: yass_window.cpp:141
msgid "Limit Rate"
msgstr "限制速率"
#: yass_window.cpp:158
#: yass_window.cpp:142
msgid "Timeout"
msgstr "超时时间"
#: yass_window.cpp:159
#: yass_window.cpp:143
msgid "Auto Start"
msgstr "随系统自启动"
#: yass_window.cpp:160
#: yass_window.cpp:144
msgid "System Proxy"
msgstr "系统代理"
#: yass_window.cpp:263
#: yass_window.cpp:275
msgid "READY"
msgstr "就绪"
#: yass_window.cpp:336 yass_window.cpp:369
#: yass_window.cpp:348 yass_window.cpp:382
msgid "Show"
msgstr "唤醒"
#: yass_window.cpp:531
#: yass_window.cpp:544
msgid " tx rate: "
msgstr " 上传速率: "
#: yass_window.cpp:534
#: yass_window.cpp:547
msgid " rx rate: "
msgstr " 下载速率: "
#: yass_window.cpp:645
#: yass_window.cpp:658
msgid "YASS Option"
msgstr "YASS 选项"
#: yass_window.cpp:656
#: yass_window.cpp:669
msgid "Last Change: "
msgstr "最后改动: "
#: yass_window.cpp:659
#: yass_window.cpp:672
msgid "Enabled Feature: "
msgstr "启用功能: "
#: yass_window.cpp:662
#: yass_window.cpp:675
msgid "GUI Variant: "
msgstr "图形版本: "
#: yass_window.cpp:671
#: yass_window.cpp:684
msgid "official-site"
msgstr "官方网站"

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