mirror of
https://github.com/bolucat/Archive.git
synced 2026-04-22 16:07:49 +08:00
Update On Sun Mar 8 19:47:26 CET 2026
This commit is contained in:
@@ -1292,3 +1292,4 @@ Update On Wed Mar 4 20:06:13 CET 2026
|
||||
Update On Thu Mar 5 20:25:40 CET 2026
|
||||
Update On Fri Mar 6 20:05:10 CET 2026
|
||||
Update On Sat Mar 7 19:46:34 CET 2026
|
||||
Update On Sun Mar 8 19:47:17 CET 2026
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ require (
|
||||
github.com/bahlo/generic-list-go v0.2.0
|
||||
github.com/coreos/go-iptables v0.8.0
|
||||
github.com/dlclark/regexp2 v1.11.5
|
||||
github.com/enfein/mieru/v3 v3.28.0
|
||||
github.com/enfein/mieru/v3 v3.29.0
|
||||
github.com/gobwas/ws v1.4.0
|
||||
github.com/gofrs/uuid/v5 v5.4.0
|
||||
github.com/golang/snappy v1.0.0
|
||||
|
||||
+2
-2
@@ -20,8 +20,8 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dunglas/httpsfv v1.0.2 h1:iERDp/YAfnojSDJ7PW3dj1AReJz4MrwbECSSE59JWL0=
|
||||
github.com/dunglas/httpsfv v1.0.2/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg=
|
||||
github.com/enfein/mieru/v3 v3.28.0 h1:4OsFPUIjKfQ6ymfyX1Laqz7h+zB8TxuK1m0isnYJ8ww=
|
||||
github.com/enfein/mieru/v3 v3.28.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
|
||||
github.com/enfein/mieru/v3 v3.29.0 h1:i5Hwl5spEWg4ydvYW86zWSYVJ2uGTf5sLYQmFXHdulQ=
|
||||
github.com/enfein/mieru/v3 v3.29.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
|
||||
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 h1:kXYqH/sL8dS/FdoFjr12ePjnLPorPo2FsnrHNuXSDyo=
|
||||
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
|
||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ PROJECT_NAME=$(shell basename "${ROOT}")
|
||||
# - pkg/version/current.go
|
||||
#
|
||||
# Use `tools/bump_version.sh` script to change all those files at one shot.
|
||||
VERSION="3.28.0"
|
||||
VERSION="3.29.0"
|
||||
|
||||
# With .ONESHELL, each recipe is executed in a single shell instance.
|
||||
# This allows `cd` to affect subsequent commands in the same recipe.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Package: mieru
|
||||
Version: 3.28.0
|
||||
Version: 3.29.0
|
||||
Section: net
|
||||
Priority: optional
|
||||
Architecture: amd64
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: mieru
|
||||
Version: 3.28.0
|
||||
Version: 3.29.0
|
||||
Release: 1%{?dist}
|
||||
Summary: Mieru proxy client
|
||||
License: GPLv3+
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Package: mieru
|
||||
Version: 3.28.0
|
||||
Version: 3.29.0
|
||||
Section: net
|
||||
Priority: optional
|
||||
Architecture: arm64
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: mieru
|
||||
Version: 3.28.0
|
||||
Version: 3.29.0
|
||||
Release: 1%{?dist}
|
||||
Summary: Mieru proxy client
|
||||
License: GPLv3+
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Package: mita
|
||||
Version: 3.28.0
|
||||
Version: 3.29.0
|
||||
Section: net
|
||||
Priority: optional
|
||||
Architecture: amd64
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: mita
|
||||
Version: 3.28.0
|
||||
Version: 3.29.0
|
||||
Release: 1%{?dist}
|
||||
Summary: Mieru proxy server
|
||||
License: GPLv3+
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Package: mita
|
||||
Version: 3.28.0
|
||||
Version: 3.29.0
|
||||
Section: net
|
||||
Priority: optional
|
||||
Architecture: arm64
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: mita
|
||||
Version: 3.28.0
|
||||
Version: 3.29.0
|
||||
Release: 1%{?dist}
|
||||
Summary: Mieru proxy server
|
||||
License: GPLv3+
|
||||
|
||||
@@ -18,32 +18,32 @@ Or you can manually install and configure proxy server using the steps below.
|
||||
|
||||
```sh
|
||||
# Debian / Ubuntu - X86_64
|
||||
curl -LSO https://github.com/enfein/mieru/releases/download/v3.28.0/mita_3.28.0_amd64.deb
|
||||
curl -LSO https://github.com/enfein/mieru/releases/download/v3.29.0/mita_3.29.0_amd64.deb
|
||||
|
||||
# Debian / Ubuntu - ARM 64
|
||||
curl -LSO https://github.com/enfein/mieru/releases/download/v3.28.0/mita_3.28.0_arm64.deb
|
||||
curl -LSO https://github.com/enfein/mieru/releases/download/v3.29.0/mita_3.29.0_arm64.deb
|
||||
|
||||
# RedHat / CentOS / Rocky Linux - X86_64
|
||||
curl -LSO https://github.com/enfein/mieru/releases/download/v3.28.0/mita-3.28.0-1.x86_64.rpm
|
||||
curl -LSO https://github.com/enfein/mieru/releases/download/v3.29.0/mita-3.29.0-1.x86_64.rpm
|
||||
|
||||
# RedHat / CentOS / Rocky Linux - ARM 64
|
||||
curl -LSO https://github.com/enfein/mieru/releases/download/v3.28.0/mita-3.28.0-1.aarch64.rpm
|
||||
curl -LSO https://github.com/enfein/mieru/releases/download/v3.29.0/mita-3.29.0-1.aarch64.rpm
|
||||
```
|
||||
|
||||
## Install mita package
|
||||
|
||||
```sh
|
||||
# Debian / Ubuntu - X86_64
|
||||
sudo dpkg -i mita_3.28.0_amd64.deb
|
||||
sudo dpkg -i mita_3.29.0_amd64.deb
|
||||
|
||||
# Debian / Ubuntu - ARM 64
|
||||
sudo dpkg -i mita_3.28.0_arm64.deb
|
||||
sudo dpkg -i mita_3.29.0_arm64.deb
|
||||
|
||||
# RedHat / CentOS / Rocky Linux - X86_64
|
||||
sudo rpm -Uvh --force mita-3.28.0-1.x86_64.rpm
|
||||
sudo rpm -Uvh --force mita-3.29.0-1.x86_64.rpm
|
||||
|
||||
# RedHat / CentOS / Rocky Linux - ARM 64
|
||||
sudo rpm -Uvh --force mita-3.28.0-1.aarch64.rpm
|
||||
sudo rpm -Uvh --force mita-3.29.0-1.aarch64.rpm
|
||||
```
|
||||
|
||||
Those instructions can also be used to upgrade the version of mita software package.
|
||||
|
||||
@@ -18,32 +18,32 @@ sudo python3 setup.py --lang=zh
|
||||
|
||||
```sh
|
||||
# Debian / Ubuntu - X86_64
|
||||
curl -LSO https://github.com/enfein/mieru/releases/download/v3.28.0/mita_3.28.0_amd64.deb
|
||||
curl -LSO https://github.com/enfein/mieru/releases/download/v3.29.0/mita_3.29.0_amd64.deb
|
||||
|
||||
# Debian / Ubuntu - ARM 64
|
||||
curl -LSO https://github.com/enfein/mieru/releases/download/v3.28.0/mita_3.28.0_arm64.deb
|
||||
curl -LSO https://github.com/enfein/mieru/releases/download/v3.29.0/mita_3.29.0_arm64.deb
|
||||
|
||||
# RedHat / CentOS / Rocky Linux - X86_64
|
||||
curl -LSO https://github.com/enfein/mieru/releases/download/v3.28.0/mita-3.28.0-1.x86_64.rpm
|
||||
curl -LSO https://github.com/enfein/mieru/releases/download/v3.29.0/mita-3.29.0-1.x86_64.rpm
|
||||
|
||||
# RedHat / CentOS / Rocky Linux - ARM 64
|
||||
curl -LSO https://github.com/enfein/mieru/releases/download/v3.28.0/mita-3.28.0-1.aarch64.rpm
|
||||
curl -LSO https://github.com/enfein/mieru/releases/download/v3.29.0/mita-3.29.0-1.aarch64.rpm
|
||||
```
|
||||
|
||||
## 安装 mita 软件包
|
||||
|
||||
```sh
|
||||
# Debian / Ubuntu - X86_64
|
||||
sudo dpkg -i mita_3.28.0_amd64.deb
|
||||
sudo dpkg -i mita_3.29.0_amd64.deb
|
||||
|
||||
# Debian / Ubuntu - ARM 64
|
||||
sudo dpkg -i mita_3.28.0_arm64.deb
|
||||
sudo dpkg -i mita_3.29.0_arm64.deb
|
||||
|
||||
# RedHat / CentOS / Rocky Linux - X86_64
|
||||
sudo rpm -Uvh --force mita-3.28.0-1.x86_64.rpm
|
||||
sudo rpm -Uvh --force mita-3.29.0-1.x86_64.rpm
|
||||
|
||||
# RedHat / CentOS / Rocky Linux - ARM 64
|
||||
sudo rpm -Uvh --force mita-3.28.0-1.aarch64.rpm
|
||||
sudo rpm -Uvh --force mita-3.29.0-1.aarch64.rpm
|
||||
```
|
||||
|
||||
上述指令也可以用来升级 mita 软件包的版本。
|
||||
|
||||
@@ -73,7 +73,7 @@ const (
|
||||
maxBackOffDuration = 10 * time.Second
|
||||
)
|
||||
|
||||
type sessionState byte
|
||||
type sessionState int32
|
||||
|
||||
const (
|
||||
sessionInit sessionState = 0
|
||||
@@ -106,7 +106,7 @@ type Session struct {
|
||||
mtu int // Underlay maxinum transmission unit
|
||||
transportProtocol common.TransportProtocol // transport protocol of underlay connection
|
||||
remoteAddr net.Addr // specify remote network address, used by UDP
|
||||
state sessionState // session state
|
||||
state atomic.Int32 // session state
|
||||
status statusCode // session status
|
||||
|
||||
users map[string]*appctlpb.User // all registered users, only used by server
|
||||
@@ -150,7 +150,6 @@ type Session struct {
|
||||
rLock sync.Mutex // serialize application read
|
||||
wLock sync.Mutex // serialize application write
|
||||
oLock sync.Mutex // serialize the output sequence
|
||||
sLock sync.Mutex // serialize the state transition
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -172,7 +171,6 @@ func NewSession(id uint32, isClient bool, mtu int, users map[string]*appctlpb.Us
|
||||
id: id,
|
||||
isClient: isClient,
|
||||
mtu: mtu,
|
||||
state: sessionInit,
|
||||
status: statusOK,
|
||||
users: users,
|
||||
trafficPattern: trafficPattern,
|
||||
@@ -429,7 +427,7 @@ func (s *Session) ToSessionInfo() *appctlpb.SessionInfo {
|
||||
Id: proto.String(fmt.Sprintf("%d", s.id)),
|
||||
LocalAddr: proto.String("-"),
|
||||
RemoteAddr: proto.String("-"),
|
||||
State: proto.String(s.state.String()),
|
||||
State: proto.String(sessionState(s.state.Load()).String()),
|
||||
RecvQ: proto.Uint32(uint32(s.recvQueue.Len())),
|
||||
RecvBuf: proto.Uint32(uint32(s.recvBuf.Len())),
|
||||
SendQ: proto.Uint32(uint32(s.sendQueue.Len())),
|
||||
@@ -468,41 +466,40 @@ func (s *Session) ToSessionInfo() *appctlpb.SessionInfo {
|
||||
}
|
||||
|
||||
func (s *Session) isState(target sessionState) bool {
|
||||
s.sLock.Lock()
|
||||
defer s.sLock.Unlock()
|
||||
return s.state == target
|
||||
return sessionState(s.state.Load()) == target
|
||||
}
|
||||
|
||||
func (s *Session) isStateBefore(target sessionState, include bool) bool {
|
||||
s.sLock.Lock()
|
||||
defer s.sLock.Unlock()
|
||||
state := sessionState(s.state.Load())
|
||||
if include {
|
||||
return s.state <= target
|
||||
return state <= target
|
||||
} else {
|
||||
return s.state < target
|
||||
return state < target
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) isStateAfter(target sessionState, include bool) bool {
|
||||
s.sLock.Lock()
|
||||
defer s.sLock.Unlock()
|
||||
state := sessionState(s.state.Load())
|
||||
if include {
|
||||
return s.state >= target
|
||||
return state >= target
|
||||
} else {
|
||||
return s.state > target
|
||||
return state > target
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) forwardStateTo(new sessionState) {
|
||||
s.sLock.Lock()
|
||||
defer s.sLock.Unlock()
|
||||
if new < s.state {
|
||||
panic(fmt.Sprintf("Can't move state back from %v to %v", s.state, new))
|
||||
func (s *Session) forwardStateTo(next sessionState) {
|
||||
for {
|
||||
old := s.state.Load()
|
||||
if int32(next) <= old {
|
||||
return
|
||||
}
|
||||
if s.state.CompareAndSwap(old, int32(next)) {
|
||||
if log.IsLevelEnabled(log.TraceLevel) {
|
||||
log.Tracef("%v %v => %v", s, sessionState(old), next)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
if log.IsLevelEnabled(log.TraceLevel) && s.state != new {
|
||||
log.Tracef("%v %v => %v", s, s.state, new)
|
||||
}
|
||||
s.state = new
|
||||
}
|
||||
|
||||
func (s *Session) writeChunk(b []byte) (n int, err error) {
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/enfein/mieru/v3/pkg/appctl/appctlpb"
|
||||
"github.com/enfein/mieru/v3/pkg/common"
|
||||
"github.com/enfein/mieru/v3/pkg/metrics"
|
||||
"github.com/enfein/mieru/v3/pkg/rng"
|
||||
"github.com/enfein/mieru/v3/pkg/stderror"
|
||||
)
|
||||
|
||||
@@ -37,6 +38,10 @@ const (
|
||||
sessionCleanInterval = 5 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
readOneSegmentTimeout = time.Duration(60+rng.FixedIntVH(61)) * time.Second
|
||||
)
|
||||
|
||||
// baseUnderlay contains a partial implementation of underlay.
|
||||
type baseUnderlay struct {
|
||||
isClient bool
|
||||
@@ -94,8 +99,6 @@ func (b *baseUnderlay) Close() error {
|
||||
s := v.(*Session)
|
||||
s.Close()
|
||||
s.wg.Wait()
|
||||
s.conn = nil
|
||||
s = nil
|
||||
return true
|
||||
})
|
||||
close(b.done)
|
||||
@@ -170,8 +173,6 @@ func (b *baseUnderlay) RemoveSession(s *Session) error {
|
||||
b.sessionMap.Delete(s.id)
|
||||
s.Close()
|
||||
s.wg.Wait()
|
||||
s.conn = nil
|
||||
s = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -38,8 +38,6 @@ const (
|
||||
packetNonHeaderPosition = cipher.DefaultNonceSize + MetadataLength + cipher.DefaultOverhead
|
||||
|
||||
idleSessionTimeout = time.Minute
|
||||
|
||||
readOneSegmentTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
var packetReplayCache = replay.NewCache(4*1024*1024, cipher.KeyRefreshInterval*3)
|
||||
@@ -128,8 +126,10 @@ func (u *PacketUnderlay) Close() error {
|
||||
|
||||
log.Debugf("Closing %v", u)
|
||||
u.sessionCleanTicker.Stop()
|
||||
// Unblock any pending I/O before closing sessions.
|
||||
u.conn.SetReadDeadline(time.Now())
|
||||
u.baseUnderlay.Close()
|
||||
return u.conn.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *PacketUnderlay) TransportProtocol() common.TransportProtocol {
|
||||
@@ -177,6 +177,7 @@ func (u *PacketUnderlay) RunEventLoop(ctx context.Context) error {
|
||||
if u.conn == nil {
|
||||
return stderror.ErrNullPointer
|
||||
}
|
||||
defer u.conn.Close()
|
||||
|
||||
for {
|
||||
select {
|
||||
@@ -192,7 +193,17 @@ func (u *PacketUnderlay) RunEventLoop(ctx context.Context) error {
|
||||
}
|
||||
seg, addr, err := u.readOneSegment()
|
||||
if err != nil {
|
||||
// If the underlay is closing, return gracefully.
|
||||
select {
|
||||
case <-u.done:
|
||||
u.cleanSessions()
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
return fmt.Errorf("readOneSegment() failed: %w", err)
|
||||
} else if seg == nil && addr == nil {
|
||||
// Timeout reading the segment, will try again later.
|
||||
continue
|
||||
}
|
||||
if log.IsLevelEnabled(log.TraceLevel) {
|
||||
log.Tracef("%v received %v from peer %v", u, seg, addr)
|
||||
@@ -296,8 +307,6 @@ func (u *PacketUnderlay) onCloseSession(seg *segment) error {
|
||||
}
|
||||
s := session.(*Session)
|
||||
s.recvChan <- seg
|
||||
s.wg.Wait()
|
||||
u.RemoveSession(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -316,7 +325,13 @@ func (u *PacketUnderlay) readOneSegment() (*segment, net.Addr, error) {
|
||||
n, addr, err := u.conn.ReadFrom(b)
|
||||
if err != nil {
|
||||
if stderror.IsTimeout(err) {
|
||||
continue
|
||||
// No UDP packet received. Caller will retry.
|
||||
return nil, nil, nil
|
||||
}
|
||||
select {
|
||||
case <-u.done:
|
||||
return nil, nil, io.ErrClosedPipe
|
||||
default:
|
||||
}
|
||||
return nil, nil, fmt.Errorf("ReadFrom() failed: %w", err)
|
||||
}
|
||||
|
||||
@@ -125,8 +125,10 @@ func (t *StreamUnderlay) Close() error {
|
||||
|
||||
log.Debugf("Closing %v", t)
|
||||
t.sessionCleanTicker.Stop()
|
||||
// Unblock any pending I/O before closing sessions.
|
||||
t.conn.SetDeadline(time.Now())
|
||||
t.baseUnderlay.Close()
|
||||
return t.conn.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *StreamUnderlay) Addr() net.Addr {
|
||||
@@ -181,6 +183,7 @@ func (t *StreamUnderlay) RunEventLoop(ctx context.Context) error {
|
||||
if t.conn == nil {
|
||||
return stderror.ErrNullPointer
|
||||
}
|
||||
defer t.conn.Close()
|
||||
|
||||
for {
|
||||
select {
|
||||
@@ -196,6 +199,13 @@ func (t *StreamUnderlay) RunEventLoop(ctx context.Context) error {
|
||||
}
|
||||
seg, err := t.readOneSegment()
|
||||
if err != nil {
|
||||
// If the underlay is closing, return gracefully.
|
||||
select {
|
||||
case <-t.done:
|
||||
t.cleanSessions()
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
errType := stderror.GetErrorType(err)
|
||||
if errType == stderror.NO_ERROR {
|
||||
panic(fmt.Sprintf("%v error type is NO_ERROR while error is not nil", t))
|
||||
@@ -207,6 +217,9 @@ func (t *StreamUnderlay) RunEventLoop(ctx context.Context) error {
|
||||
t.drainAfterError()
|
||||
}
|
||||
return fmt.Errorf("readOneSegment() failed: %w", err)
|
||||
} else if seg == nil {
|
||||
// Timeout reading the segment, will try again later.
|
||||
continue
|
||||
}
|
||||
if log.IsLevelEnabled(log.TraceLevel) {
|
||||
log.Tracef("%v received %v", t, seg)
|
||||
@@ -306,8 +319,6 @@ func (t *StreamUnderlay) onCloseSession(seg *segment) error {
|
||||
}
|
||||
s := session.(*Session)
|
||||
s.recvChan <- seg
|
||||
s.wg.Wait()
|
||||
t.RemoveSession(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -315,6 +326,9 @@ func (t *StreamUnderlay) readOneSegment() (*segment, error) {
|
||||
var firstRead bool
|
||||
var err error
|
||||
|
||||
common.SetReadTimeout(t.conn, readOneSegmentTimeout)
|
||||
defer common.SetReadTimeout(t.conn, 0)
|
||||
|
||||
// Read encrypted metadata.
|
||||
readLen := MetadataLength + cipher.DefaultOverhead
|
||||
if t.recv == nil {
|
||||
@@ -323,7 +337,11 @@ func (t *StreamUnderlay) readOneSegment() (*segment, error) {
|
||||
readLen += cipher.DefaultNonceSize
|
||||
}
|
||||
encryptedMeta := make([]byte, readLen)
|
||||
if _, err := io.ReadFull(t.conn, encryptedMeta); err != nil {
|
||||
if n, err := io.ReadFull(t.conn, encryptedMeta); err != nil {
|
||||
if stderror.IsTimeout(err) && n == 0 {
|
||||
// No TCP data received. Caller will retry.
|
||||
return nil, nil
|
||||
}
|
||||
err = fmt.Errorf("metadata: read %d bytes from StreamUnderlay failed: %w", readLen, err)
|
||||
return nil, stderror.WrapErrorWithType(err, stderror.NETWORK_ERROR)
|
||||
}
|
||||
@@ -695,15 +713,17 @@ func (t *StreamUnderlay) drainAfterError() {
|
||||
buf := make([]byte, bufSize)
|
||||
|
||||
// Determine the number of bytes to read.
|
||||
// Minimum 2 bytes, maximum bufSize bytes.
|
||||
minRead := rng.IntRange(2, bufSize-254)
|
||||
// Minimum 0 bytes, maximum bufSize bytes.
|
||||
minRead := rng.IntRange(0, bufSize-254)
|
||||
minRead += rng.FixedIntVH(256)
|
||||
|
||||
n, err := io.ReadAtLeast(t.conn, buf, minRead)
|
||||
if err != nil {
|
||||
log.Debugf("%v read after stream error failed to complete: %v", t, err)
|
||||
} else {
|
||||
log.Debugf("%v read at least %d bytes after stream error", t, n)
|
||||
if minRead > 0 {
|
||||
n, err := io.ReadAtLeast(t.conn, buf, minRead)
|
||||
if err != nil {
|
||||
log.Debugf("%v read after stream error failed to complete: %v", t, err)
|
||||
} else {
|
||||
log.Debugf("%v read at least %d bytes after stream error", t, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,5 +16,5 @@
|
||||
package version
|
||||
|
||||
const (
|
||||
AppVersion = "3.28.0"
|
||||
AppVersion = "3.29.0"
|
||||
)
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ require (
|
||||
github.com/bahlo/generic-list-go v0.2.0
|
||||
github.com/coreos/go-iptables v0.8.0
|
||||
github.com/dlclark/regexp2 v1.11.5
|
||||
github.com/enfein/mieru/v3 v3.28.0
|
||||
github.com/enfein/mieru/v3 v3.29.0
|
||||
github.com/gobwas/ws v1.4.0
|
||||
github.com/gofrs/uuid/v5 v5.4.0
|
||||
github.com/golang/snappy v1.0.0
|
||||
|
||||
+2
-2
@@ -20,8 +20,8 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dunglas/httpsfv v1.0.2 h1:iERDp/YAfnojSDJ7PW3dj1AReJz4MrwbECSSE59JWL0=
|
||||
github.com/dunglas/httpsfv v1.0.2/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg=
|
||||
github.com/enfein/mieru/v3 v3.28.0 h1:4OsFPUIjKfQ6ymfyX1Laqz7h+zB8TxuK1m0isnYJ8ww=
|
||||
github.com/enfein/mieru/v3 v3.28.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
|
||||
github.com/enfein/mieru/v3 v3.29.0 h1:i5Hwl5spEWg4ydvYW86zWSYVJ2uGTf5sLYQmFXHdulQ=
|
||||
github.com/enfein/mieru/v3 v3.29.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
|
||||
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 h1:kXYqH/sL8dS/FdoFjr12ePjnLPorPo2FsnrHNuXSDyo=
|
||||
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
|
||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
|
||||
|
||||
@@ -486,7 +486,7 @@ o.validate = function(self, value, t)
|
||||
end
|
||||
local _tcp_node = s.fields["tcp_node"]:formvalue(t)
|
||||
if _dns_mode and _tcp_node then
|
||||
if m:get(_tcp_node, "type"):lower() ~= _dns_mode then
|
||||
if (m:get(_tcp_node, "type") or ""):lower() ~= _dns_mode then
|
||||
return nil, translatef("TCP node must be '%s' type to use FakeDNS.", _dns_mode)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -334,6 +334,9 @@ function gen_outbound(flag, node, tag, proxy_table)
|
||||
if type(fm.tcp) == "table" then
|
||||
finalmask.tcp = fm.tcp
|
||||
end
|
||||
if type(fm.quicParams) == "table" then
|
||||
finalmask.quicParams = fm.quicParams
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -658,6 +661,9 @@ function gen_config_server(node)
|
||||
if type(fm.tcp) == "table" then
|
||||
finalmask.tcp = fm.tcp
|
||||
end
|
||||
if type(fm.quicParams) == "table" then
|
||||
finalmask.quicParams = fm.quicParams
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -367,14 +367,14 @@ local function non_file_check(file_path, header_content)
|
||||
end
|
||||
|
||||
local function GeoToRule(rule_name, rule_type, out_path)
|
||||
if not api.is_finded("geoview") then
|
||||
log(rule_name .. "生成失败,缺少 geoview 组件。")
|
||||
return false;
|
||||
local bin = api.finded_com("geoview")
|
||||
if not (bin and api.compare_versions(api.get_app_version("geoview"), ">=", "0.1.10")) then
|
||||
log("[警告] Geoview 组件缺失或版本过低,规则生成流程已被跳过。")
|
||||
return false
|
||||
end
|
||||
local geosite_path = asset_location .. "geosite.dat"
|
||||
local geoip_path = asset_location .. "geoip.dat"
|
||||
local file_path = (rule_type == "domain") and geosite_path or geoip_path
|
||||
local bin = api.get_app_path("geoview")
|
||||
local geo_arg
|
||||
if rule_type == "domain" then
|
||||
if rule_name == "gfwlist" then
|
||||
@@ -389,7 +389,13 @@ local function GeoToRule(rule_name, rule_type, out_path)
|
||||
end
|
||||
local cmd = string.format(bin .. " -input '%s' %s -lowmem=true -output '%s'", file_path, geo_arg, out_path)
|
||||
sys.exec(cmd)
|
||||
return true;
|
||||
local local_file_size = tonumber(fs.stat(out_path, "size") or 0)
|
||||
if local_file_size == 0 then
|
||||
os.remove(out_path)
|
||||
log(rule_name .. " 生成失败,请确保 Geo 文件正确且包含目标规则。")
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--fetch rule
|
||||
@@ -407,10 +413,10 @@ local function fetch_rule(rule_name, rule_type, url, exclude_domain, max_retries
|
||||
end
|
||||
|
||||
for k, v in ipairs(url) do
|
||||
local current_file = "/tmp/" .. rule_name .. "_dl" .. k
|
||||
local success = false
|
||||
local current_file = "/tmp/" .. rule_name .. "_dl" .. k
|
||||
local success = false
|
||||
|
||||
if v ~= "geo2rule" then
|
||||
if v ~= "geo2rule" then
|
||||
for i = 1, max_attempts do
|
||||
local http_code, header = curl(v, current_file)
|
||||
if http_code == 200 and not non_file_check(current_file, header) then
|
||||
|
||||
@@ -1578,7 +1578,7 @@ end
|
||||
local function curl(url, file, ua, mode)
|
||||
if not url or url == "" then return 404 end
|
||||
local curl_args = {
|
||||
"-skL", "-w %{http_code}", "--retry 3", "--connect-timeout 3"
|
||||
"-skL", "-w %{http_code}", "--retry 3", "--connect-timeout 3", "-H 'Accept-Encoding: identity'"
|
||||
}
|
||||
if ua and ua ~= "" and ua ~= "curl" then
|
||||
ua = (ua == "passwall") and ("passwall/" .. api.get_version()) or ua
|
||||
|
||||
@@ -1562,7 +1562,7 @@ end
|
||||
local function curl(url, file, ua, mode)
|
||||
if not url or url == "" then return 404 end
|
||||
local curl_args = {
|
||||
"-skL", "-w %{http_code}", "--retry 3", "--connect-timeout 3"
|
||||
"-skL", "-w %{http_code}", "--retry 3", "--connect-timeout 3", "-H 'Accept-Encoding: identity'"
|
||||
}
|
||||
if ua and ua ~= "" and ua ~= "curl" then
|
||||
ua = (ua == "passwall2") and ("passwall2/" .. api.get_version()) or ua
|
||||
|
||||
Vendored
+1
-1
@@ -29,7 +29,7 @@ jobs:
|
||||
env:
|
||||
ARCH: ${{ matrix.arch }}-${{ matrix.sdk }}
|
||||
FEEDNAME: packages_ci
|
||||
PACKAGES: luci-app-fchomo luci-app-homeproxy luci-app-momo luci-app-nikki luci-app-openclash luci-app-passwall luci-app-passwall2 luci-app-ssr-plus luci-app-bypass brook hysteria ipt2socks pdnsd-alt redsocks2 shadow-tls trojan tuic-client xray-plugin v2ray-core v2ray-geodata naiveproxy sing-box
|
||||
PACKAGES: luci-app-fchomo luci-app-homeproxy luci-app-momo luci-app-nikki luci-app-openclash luci-app-passwall luci-app-passwall2 luci-app-ssr-plus brook hysteria ipt2socks pdnsd-alt redsocks2 shadow-tls trojan tuic-client xray-plugin v2ray-core v2ray-geodata naiveproxy sing-box
|
||||
NO_REFRESH_CHECK: true
|
||||
IGNORE_ERRORS: true
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -68,7 +68,7 @@ jobs:
|
||||
env:
|
||||
ARCH: ${{ matrix.arch }}-${{ matrix.sdk }}
|
||||
FEEDNAME: packages_ci
|
||||
PACKAGES: luci-app-fchomo luci-app-homeproxy luci-app-momo luci-app-nikki luci-app-openclash luci-app-passwall luci-app-passwall2 luci-app-ssr-plus luci-app-bypass brook hysteria ipt2socks pdnsd-alt redsocks2 shadow-tls trojan tuic-client xray-plugin v2ray-core v2ray-geodata naiveproxy sing-box
|
||||
PACKAGES: luci-app-fchomo luci-app-homeproxy luci-app-momo luci-app-nikki luci-app-openclash luci-app-passwall luci-app-passwall2 luci-app-ssr-plus brook hysteria ipt2socks pdnsd-alt redsocks2 shadow-tls trojan tuic-client xray-plugin v2ray-core v2ray-geodata naiveproxy sing-box
|
||||
NO_REFRESH_CHECK: true
|
||||
IGNORE_ERRORS: true
|
||||
|
||||
|
||||
+3
-3
@@ -9,9 +9,9 @@ PKG_RELEASE:=1
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://gn.googlesource.com/gn.git
|
||||
PKG_SOURCE_DATE:=2026-02-25
|
||||
PKG_SOURCE_VERSION:=1a310e88443018837759c952b113846b0096f65b
|
||||
PKG_MIRROR_HASH:=26905bb57add5d3c9e0931eaac72510c9fcf5e8837f397594b2f6d6961f7637a
|
||||
PKG_SOURCE_DATE:=2026-03-05
|
||||
PKG_SOURCE_VERSION:=d8c2f07d653520568da7cace755a87dad241b72d
|
||||
PKG_MIRROR_HASH:=68c3f56ed9b8aa31d49c1d7620e62b66ff0496f7921e4c234a2237e676e343c7
|
||||
|
||||
PKG_LICENSE:=BSD 3-Clause
|
||||
PKG_LICENSE_FILES:=LICENSE
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#ifndef OUT_LAST_COMMIT_POSITION_H_
|
||||
#define OUT_LAST_COMMIT_POSITION_H_
|
||||
|
||||
#define LAST_COMMIT_POSITION_NUM 2338
|
||||
#define LAST_COMMIT_POSITION "2338 (1a310e884430)"
|
||||
#define LAST_COMMIT_POSITION_NUM 2342
|
||||
#define LAST_COMMIT_POSITION "2342 (d8c2f07d6535)"
|
||||
|
||||
#endif // OUT_LAST_COMMIT_POSITION_H_
|
||||
|
||||
@@ -486,7 +486,7 @@ o.validate = function(self, value, t)
|
||||
end
|
||||
local _tcp_node = s.fields["tcp_node"]:formvalue(t)
|
||||
if _dns_mode and _tcp_node then
|
||||
if m:get(_tcp_node, "type"):lower() ~= _dns_mode then
|
||||
if (m:get(_tcp_node, "type") or ""):lower() ~= _dns_mode then
|
||||
return nil, translatef("TCP node must be '%s' type to use FakeDNS.", _dns_mode)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -334,6 +334,9 @@ function gen_outbound(flag, node, tag, proxy_table)
|
||||
if type(fm.tcp) == "table" then
|
||||
finalmask.tcp = fm.tcp
|
||||
end
|
||||
if type(fm.quicParams) == "table" then
|
||||
finalmask.quicParams = fm.quicParams
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -658,6 +661,9 @@ function gen_config_server(node)
|
||||
if type(fm.tcp) == "table" then
|
||||
finalmask.tcp = fm.tcp
|
||||
end
|
||||
if type(fm.quicParams) == "table" then
|
||||
finalmask.quicParams = fm.quicParams
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -367,14 +367,14 @@ local function non_file_check(file_path, header_content)
|
||||
end
|
||||
|
||||
local function GeoToRule(rule_name, rule_type, out_path)
|
||||
if not api.is_finded("geoview") then
|
||||
log(rule_name .. "生成失败,缺少 geoview 组件。")
|
||||
return false;
|
||||
local bin = api.finded_com("geoview")
|
||||
if not (bin and api.compare_versions(api.get_app_version("geoview"), ">=", "0.1.10")) then
|
||||
log("[警告] Geoview 组件缺失或版本过低,规则生成流程已被跳过。")
|
||||
return false
|
||||
end
|
||||
local geosite_path = asset_location .. "geosite.dat"
|
||||
local geoip_path = asset_location .. "geoip.dat"
|
||||
local file_path = (rule_type == "domain") and geosite_path or geoip_path
|
||||
local bin = api.get_app_path("geoview")
|
||||
local geo_arg
|
||||
if rule_type == "domain" then
|
||||
if rule_name == "gfwlist" then
|
||||
@@ -389,7 +389,13 @@ local function GeoToRule(rule_name, rule_type, out_path)
|
||||
end
|
||||
local cmd = string.format(bin .. " -input '%s' %s -lowmem=true -output '%s'", file_path, geo_arg, out_path)
|
||||
sys.exec(cmd)
|
||||
return true;
|
||||
local local_file_size = tonumber(fs.stat(out_path, "size") or 0)
|
||||
if local_file_size == 0 then
|
||||
os.remove(out_path)
|
||||
log(rule_name .. " 生成失败,请确保 Geo 文件正确且包含目标规则。")
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--fetch rule
|
||||
@@ -407,10 +413,10 @@ local function fetch_rule(rule_name, rule_type, url, exclude_domain, max_retries
|
||||
end
|
||||
|
||||
for k, v in ipairs(url) do
|
||||
local current_file = "/tmp/" .. rule_name .. "_dl" .. k
|
||||
local success = false
|
||||
local current_file = "/tmp/" .. rule_name .. "_dl" .. k
|
||||
local success = false
|
||||
|
||||
if v ~= "geo2rule" then
|
||||
if v ~= "geo2rule" then
|
||||
for i = 1, max_attempts do
|
||||
local http_code, header = curl(v, current_file)
|
||||
if http_code == 200 and not non_file_check(current_file, header) then
|
||||
|
||||
@@ -1578,7 +1578,7 @@ end
|
||||
local function curl(url, file, ua, mode)
|
||||
if not url or url == "" then return 404 end
|
||||
local curl_args = {
|
||||
"-skL", "-w %{http_code}", "--retry 3", "--connect-timeout 3"
|
||||
"-skL", "-w %{http_code}", "--retry 3", "--connect-timeout 3", "-H 'Accept-Encoding: identity'"
|
||||
}
|
||||
if ua and ua ~= "" and ua ~= "curl" then
|
||||
ua = (ua == "passwall") and ("passwall/" .. api.get_version()) or ua
|
||||
|
||||
@@ -1562,7 +1562,7 @@ end
|
||||
local function curl(url, file, ua, mode)
|
||||
if not url or url == "" then return 404 end
|
||||
local curl_args = {
|
||||
"-skL", "-w %{http_code}", "--retry 3", "--connect-timeout 3"
|
||||
"-skL", "-w %{http_code}", "--retry 3", "--connect-timeout 3", "-H 'Accept-Encoding: identity'"
|
||||
}
|
||||
if ua and ua ~= "" and ua ~= "curl" then
|
||||
ua = (ua == "passwall2") and ("passwall2/" .. api.get_version()) or ua
|
||||
|
||||
@@ -21,13 +21,13 @@ define Download/geoip
|
||||
HASH:=c6c1d1be0d28defef55b153e87cb430f94fb480c8f523bf901c5e4ca18d58a00
|
||||
endef
|
||||
|
||||
GEOSITE_VER:=20260307033015
|
||||
GEOSITE_VER:=20260308091719
|
||||
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:=6e432a65a6deefa816555eb2c6bc83138479ead2bc88b01d2a51ccea6a9e7315
|
||||
HASH:=1d8fbd56dbb678271770306060c994b48f18615fc6ae6135f51b0e511bd388fc
|
||||
endef
|
||||
|
||||
GEOSITE_IRAN_VER:=202603020055
|
||||
|
||||
@@ -269,6 +269,7 @@ public class CoreConfigContextBuilder
|
||||
IndexId = $"inner-{Utils.GetGuid(false)}",
|
||||
ConfigType = EConfigType.ProxyChain,
|
||||
CoreType = AppManager.Instance.GetCoreType(node, node.ConfigType),
|
||||
Remarks = node.Remarks,
|
||||
};
|
||||
List<string?> childItems = [prevNode?.IndexId, node.IndexId, nextNode?.IndexId];
|
||||
var chainExtraItem = chainNode.GetProtocolExtra() with
|
||||
|
||||
@@ -154,6 +154,7 @@ public static class ConfigHandler
|
||||
DownMbps = 100
|
||||
};
|
||||
config.ClashUIItem ??= new();
|
||||
config.ClashUIItem.ConnectionsColumnItem ??= new();
|
||||
config.SystemProxyItem ??= new();
|
||||
config.WebDavItem ??= new();
|
||||
config.CheckUpdateItem ??= new();
|
||||
|
||||
@@ -208,6 +208,7 @@ public class ClashUIItem
|
||||
public int ProxiesAutoDelayTestInterval { get; set; } = 10;
|
||||
public bool ConnectionsAutoRefresh { get; set; }
|
||||
public int ConnectionsRefreshInterval { get; set; } = 2;
|
||||
public List<ColumnItem> ConnectionsColumnItem { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
|
||||
@@ -74,23 +74,28 @@
|
||||
<DataGridTextColumn
|
||||
Width="300"
|
||||
Binding="{Binding Host}"
|
||||
Header="{x:Static resx:ResUI.TbSortingHost}" />
|
||||
Header="{x:Static resx:ResUI.TbSortingHost}"
|
||||
Tag="Host" />
|
||||
<DataGridTextColumn
|
||||
Width="500"
|
||||
Binding="{Binding Chain}"
|
||||
Header="{x:Static resx:ResUI.TbSortingChain}" />
|
||||
Header="{x:Static resx:ResUI.TbSortingChain}"
|
||||
Tag="Chain" />
|
||||
<DataGridTextColumn
|
||||
Width="80"
|
||||
Binding="{Binding Network}"
|
||||
Header="{x:Static resx:ResUI.TbSortingNetwork}" />
|
||||
Header="{x:Static resx:ResUI.TbSortingNetwork}"
|
||||
Tag="Network" />
|
||||
<DataGridTextColumn
|
||||
Width="160"
|
||||
Binding="{Binding Type}"
|
||||
Header="{x:Static resx:ResUI.TbSortingType}" />
|
||||
Header="{x:Static resx:ResUI.TbSortingType}"
|
||||
Tag="Type" />
|
||||
<DataGridTextColumn
|
||||
Width="100"
|
||||
Binding="{Binding Elapsed}"
|
||||
Header="{x:Static resx:ResUI.TbSortingTime}" />
|
||||
Header="{x:Static resx:ResUI.TbSortingTime}"
|
||||
Tag="Elapsed" />
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</DockPanel>
|
||||
|
||||
@@ -2,9 +2,15 @@ namespace v2rayN.Desktop.Views;
|
||||
|
||||
public partial class ClashConnectionsView : ReactiveUserControl<ClashConnectionsViewModel>
|
||||
{
|
||||
private static Config _config;
|
||||
private static readonly string _tag = "ClashConnectionsView";
|
||||
|
||||
public ClashConnectionsView()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_config = AppManager.Instance.Config;
|
||||
|
||||
ViewModel = new ClashConnectionsViewModel(UpdateViewHandler);
|
||||
btnAutofitColumnWidth.Click += BtnAutofitColumnWidth_Click;
|
||||
|
||||
@@ -19,7 +25,15 @@ public partial class ClashConnectionsView : ReactiveUserControl<ClashConnections
|
||||
this.Bind(ViewModel, vm => vm.HostFilter, v => v.txtHostFilter.Text).DisposeWith(disposables);
|
||||
this.BindCommand(ViewModel, vm => vm.ConnectionCloseAllCmd, v => v.btnConnectionCloseAll).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables);
|
||||
|
||||
AppEvents.AppExitRequested
|
||||
.AsObservable()
|
||||
.ObserveOn(RxApp.MainThreadScheduler)
|
||||
.Subscribe(_ => StorageUI())
|
||||
.DisposeWith(disposables);
|
||||
});
|
||||
|
||||
RestoreUI();
|
||||
}
|
||||
|
||||
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
|
||||
@@ -51,4 +65,74 @@ public partial class ClashConnectionsView : ReactiveUserControl<ClashConnections
|
||||
{
|
||||
ViewModel?.ClashConnectionClose(false);
|
||||
}
|
||||
|
||||
#region UI
|
||||
|
||||
private void RestoreUI()
|
||||
{
|
||||
try
|
||||
{
|
||||
var lvColumnItem = _config.ClashUIItem?.ConnectionsColumnItem?.OrderBy(t => t.Index).ToList();
|
||||
if (lvColumnItem == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var displayIndex = 0;
|
||||
foreach (var item in lvColumnItem)
|
||||
{
|
||||
foreach (var item2 in lstConnections.Columns)
|
||||
{
|
||||
if (item2.Tag == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (item2.Tag.Equals(item.Name))
|
||||
{
|
||||
if (item.Width < 0)
|
||||
{
|
||||
item2.IsVisible = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
item2.Width = new DataGridLength(item.Width, DataGridLengthUnitType.Pixel);
|
||||
item2.DisplayIndex = displayIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void StorageUI()
|
||||
{
|
||||
try
|
||||
{
|
||||
List<ColumnItem> lvColumnItem = new();
|
||||
foreach (var item2 in lstConnections.Columns)
|
||||
{
|
||||
if (item2.Tag == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
lvColumnItem.Add(new()
|
||||
{
|
||||
Name = (string)item2.Tag,
|
||||
Width = (int)(item2.IsVisible == true ? item2.ActualWidth : -1),
|
||||
Index = item2.DisplayIndex
|
||||
});
|
||||
}
|
||||
_config.ClashUIItem.ConnectionsColumnItem = lvColumnItem;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion UI
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
|
||||
{
|
||||
private static Config _config;
|
||||
private Window? _window;
|
||||
private static readonly string _tag = "ProfilesView";
|
||||
|
||||
public ProfilesView()
|
||||
{
|
||||
@@ -381,7 +382,7 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog("ProfilesView", ex);
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,53 +400,67 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
|
||||
|
||||
private void RestoreUI()
|
||||
{
|
||||
var lvColumnItem = _config.UiItem.MainColumnItem.OrderBy(t => t.Index).ToList();
|
||||
var displayIndex = 0;
|
||||
foreach (var item in lvColumnItem)
|
||||
try
|
||||
{
|
||||
var lvColumnItem = _config.UiItem.MainColumnItem.OrderBy(t => t.Index).ToList();
|
||||
var displayIndex = 0;
|
||||
foreach (var item in lvColumnItem)
|
||||
{
|
||||
foreach (var item2 in lstProfiles.Columns)
|
||||
{
|
||||
if (item2.Tag == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (item2.Tag.Equals(item.Name))
|
||||
{
|
||||
if (item.Width < 0)
|
||||
{
|
||||
item2.IsVisible = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
item2.Width = new DataGridLength(item.Width, DataGridLengthUnitType.Pixel);
|
||||
item2.DisplayIndex = displayIndex++;
|
||||
}
|
||||
if (item.Name.ToLower().StartsWith("to"))
|
||||
{
|
||||
item2.IsVisible = _config.GuiItem.EnableStatistics;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void StorageUI()
|
||||
{
|
||||
try
|
||||
{
|
||||
List<ColumnItem> lvColumnItem = new();
|
||||
foreach (var item2 in lstProfiles.Columns)
|
||||
{
|
||||
if (item2.Tag == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (item2.Tag.Equals(item.Name))
|
||||
lvColumnItem.Add(new()
|
||||
{
|
||||
if (item.Width < 0)
|
||||
{
|
||||
item2.IsVisible = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
item2.Width = new DataGridLength(item.Width, DataGridLengthUnitType.Pixel);
|
||||
item2.DisplayIndex = displayIndex++;
|
||||
}
|
||||
if (item.Name.ToLower().StartsWith("to"))
|
||||
{
|
||||
item2.IsVisible = _config.GuiItem.EnableStatistics;
|
||||
}
|
||||
}
|
||||
Name = (string)item2.Tag,
|
||||
Width = (int)(item2.IsVisible == true ? item2.ActualWidth : -1),
|
||||
Index = item2.DisplayIndex
|
||||
});
|
||||
}
|
||||
_config.UiItem.MainColumnItem = lvColumnItem;
|
||||
}
|
||||
}
|
||||
|
||||
private void StorageUI()
|
||||
{
|
||||
List<ColumnItem> lvColumnItem = new();
|
||||
foreach (var item2 in lstProfiles.Columns)
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (item2.Tag == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
lvColumnItem.Add(new()
|
||||
{
|
||||
Name = (string)item2.Tag,
|
||||
Width = (int)(item2.IsVisible == true ? item2.ActualWidth : -1),
|
||||
Index = item2.DisplayIndex
|
||||
});
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
_config.UiItem.MainColumnItem = lvColumnItem;
|
||||
}
|
||||
|
||||
#endregion UI
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
x:Class="v2rayN.Views.ClashConnectionsView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:base="clr-namespace:v2rayN.Base"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
@@ -78,25 +79,30 @@
|
||||
</ContextMenu>
|
||||
</DataGrid.ContextMenu>
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn
|
||||
<base:MyDGTextColumn
|
||||
Width="300"
|
||||
Binding="{Binding Host}"
|
||||
ExName="Host"
|
||||
Header="{x:Static resx:ResUI.TbSortingHost}" />
|
||||
<DataGridTextColumn
|
||||
<base:MyDGTextColumn
|
||||
Width="500"
|
||||
Binding="{Binding Chain}"
|
||||
ExName="Chain"
|
||||
Header="{x:Static resx:ResUI.TbSortingChain}" />
|
||||
<DataGridTextColumn
|
||||
<base:MyDGTextColumn
|
||||
Width="80"
|
||||
Binding="{Binding Network}"
|
||||
ExName="Network"
|
||||
Header="{x:Static resx:ResUI.TbSortingNetwork}" />
|
||||
<DataGridTextColumn
|
||||
<base:MyDGTextColumn
|
||||
Width="160"
|
||||
Binding="{Binding Type}"
|
||||
ExName="Type"
|
||||
Header="{x:Static resx:ResUI.TbSortingType}" />
|
||||
<DataGridTextColumn
|
||||
<base:MyDGTextColumn
|
||||
Width="100"
|
||||
Binding="{Binding Elapsed}"
|
||||
ExName="Elapsed"
|
||||
Header="{x:Static resx:ResUI.TbSortingTime}" />
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Windows.Controls;
|
||||
using v2rayN.Base;
|
||||
|
||||
namespace v2rayN.Views;
|
||||
|
||||
@@ -7,9 +8,14 @@ namespace v2rayN.Views;
|
||||
/// </summary>
|
||||
public partial class ClashConnectionsView
|
||||
{
|
||||
private static Config _config;
|
||||
private static readonly string _tag = "ClashConnectionsView";
|
||||
|
||||
public ClashConnectionsView()
|
||||
{
|
||||
InitializeComponent();
|
||||
_config = AppManager.Instance.Config;
|
||||
|
||||
ViewModel = new ClashConnectionsViewModel(UpdateViewHandler);
|
||||
btnAutofitColumnWidth.Click += BtnAutofitColumnWidth_Click;
|
||||
|
||||
@@ -24,7 +30,15 @@ public partial class ClashConnectionsView
|
||||
this.Bind(ViewModel, vm => vm.HostFilter, v => v.txtHostFilter.Text).DisposeWith(disposables);
|
||||
this.BindCommand(ViewModel, vm => vm.ConnectionCloseAllCmd, v => v.btnConnectionCloseAll).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables);
|
||||
|
||||
AppEvents.AppExitRequested
|
||||
.AsObservable()
|
||||
.ObserveOn(RxApp.MainThreadScheduler)
|
||||
.Subscribe(_ => StorageUI())
|
||||
.DisposeWith(disposables);
|
||||
});
|
||||
|
||||
RestoreUI();
|
||||
}
|
||||
|
||||
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
|
||||
@@ -48,7 +62,7 @@ public partial class ClashConnectionsView
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog("ClashConnectionsView", ex);
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,4 +70,71 @@ public partial class ClashConnectionsView
|
||||
{
|
||||
ViewModel?.ClashConnectionClose(false);
|
||||
}
|
||||
|
||||
#region UI
|
||||
|
||||
private void RestoreUI()
|
||||
{
|
||||
try
|
||||
{
|
||||
var lvColumnItem = _config.ClashUIItem?.ConnectionsColumnItem?.OrderBy(t => t.Index).ToList();
|
||||
if (lvColumnItem == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var displayIndex = 0;
|
||||
foreach (var item in lvColumnItem)
|
||||
{
|
||||
foreach (var col in lstConnections.Columns.Cast<MyDGTextColumn>())
|
||||
{
|
||||
if (col.ExName == item.Name)
|
||||
{
|
||||
if (item.Width > 0)
|
||||
{
|
||||
col.Width = item.Width;
|
||||
}
|
||||
|
||||
col.DisplayIndex = displayIndex++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void StorageUI()
|
||||
{
|
||||
try
|
||||
{
|
||||
List<ColumnItem> lvColumnItem = new();
|
||||
foreach (var col in lstConnections.Columns.Cast<MyDGTextColumn>())
|
||||
{
|
||||
var name = col.ExName;
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
lvColumnItem.Add(new()
|
||||
{
|
||||
Name = name,
|
||||
Width = (int)col.ActualWidth,
|
||||
Index = col.DisplayIndex
|
||||
});
|
||||
}
|
||||
|
||||
_config.ClashUIItem.ConnectionsColumnItem = lvColumnItem;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion UI
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace v2rayN.Views;
|
||||
public partial class ProfilesView
|
||||
{
|
||||
private static Config _config;
|
||||
private static readonly string _tag = "ProfilesView";
|
||||
|
||||
public ProfilesView()
|
||||
{
|
||||
@@ -339,7 +340,7 @@ public partial class ProfilesView
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog("ProfilesView", ex);
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,46 +358,59 @@ public partial class ProfilesView
|
||||
|
||||
private void RestoreUI()
|
||||
{
|
||||
var lvColumnItem = _config.UiItem.MainColumnItem.OrderBy(t => t.Index).ToList();
|
||||
var displayIndex = 0;
|
||||
foreach (var item in lvColumnItem)
|
||||
try
|
||||
{
|
||||
foreach (MyDGTextColumn item2 in lstProfiles.Columns)
|
||||
var lvColumnItem = _config.UiItem.MainColumnItem.OrderBy(t => t.Index).ToList();
|
||||
var displayIndex = 0;
|
||||
foreach (var item in lvColumnItem)
|
||||
{
|
||||
if (item2.ExName == item.Name)
|
||||
foreach (var item2 in lstProfiles.Columns.Cast<MyDGTextColumn>())
|
||||
{
|
||||
if (item.Width < 0)
|
||||
if (item2.ExName == item.Name)
|
||||
{
|
||||
item2.Visibility = Visibility.Hidden;
|
||||
}
|
||||
else
|
||||
{
|
||||
item2.Width = item.Width;
|
||||
item2.DisplayIndex = displayIndex++;
|
||||
}
|
||||
if (item.Name.ToLower().StartsWith("to"))
|
||||
{
|
||||
item2.Visibility = _config.GuiItem.EnableStatistics ? Visibility.Visible : Visibility.Hidden;
|
||||
if (item.Width < 0)
|
||||
{
|
||||
item2.Visibility = Visibility.Hidden;
|
||||
}
|
||||
else
|
||||
{
|
||||
item2.Width = item.Width;
|
||||
item2.DisplayIndex = displayIndex++;
|
||||
}
|
||||
if (item.Name.ToLower().StartsWith("to"))
|
||||
{
|
||||
item2.Visibility = _config.GuiItem.EnableStatistics ? Visibility.Visible : Visibility.Hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void StorageUI()
|
||||
{
|
||||
List<ColumnItem> lvColumnItem = new();
|
||||
foreach (var t in lstProfiles.Columns)
|
||||
try
|
||||
{
|
||||
var item2 = (MyDGTextColumn)t;
|
||||
lvColumnItem.Add(new()
|
||||
List<ColumnItem> lvColumnItem = new();
|
||||
foreach (var item2 in lstProfiles.Columns.Cast<MyDGTextColumn>())
|
||||
{
|
||||
Name = item2.ExName,
|
||||
Width = (int)(item2.Visibility == Visibility.Visible ? item2.ActualWidth : -1),
|
||||
Index = item2.DisplayIndex
|
||||
});
|
||||
lvColumnItem.Add(new()
|
||||
{
|
||||
Name = item2.ExName,
|
||||
Width = (int)(item2.Visibility == Visibility.Visible ? item2.ActualWidth : -1),
|
||||
Index = item2.DisplayIndex
|
||||
});
|
||||
}
|
||||
_config.UiItem.MainColumnItem = lvColumnItem;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
_config.UiItem.MainColumnItem = lvColumnItem;
|
||||
}
|
||||
|
||||
#endregion UI
|
||||
@@ -405,7 +419,7 @@ public partial class ProfilesView
|
||||
|
||||
private Point startPoint = new();
|
||||
private int startIndex = -1;
|
||||
private string formatData = "ProfileItemModel";
|
||||
private readonly string formatData = "ProfileItemModel";
|
||||
|
||||
/// <summary>
|
||||
/// Helper to search up the VisualTree
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.v2ray.ang.receiver
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.V2RayServiceManager
|
||||
|
||||
@@ -16,8 +18,24 @@ class BootReceiver : BroadcastReceiver() {
|
||||
* @param intent The Intent being received.
|
||||
*/
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (context == null || intent?.action != Intent.ACTION_BOOT_COMPLETED) return
|
||||
if (!MmkvManager.decodeStartOnBoot() || MmkvManager.getSelectServer().isNullOrEmpty()) return
|
||||
Log.i(AppConfig.TAG, "BootReceiver received: ${intent?.action}")
|
||||
|
||||
if (context == null || intent?.action != Intent.ACTION_BOOT_COMPLETED) {
|
||||
Log.w(AppConfig.TAG, "BootReceiver: Invalid context or action")
|
||||
return
|
||||
}
|
||||
|
||||
if (!MmkvManager.decodeStartOnBoot()) {
|
||||
Log.i(AppConfig.TAG, "BootReceiver: Auto-start on boot is disabled")
|
||||
return
|
||||
}
|
||||
|
||||
if (MmkvManager.getSelectServer().isNullOrEmpty()) {
|
||||
Log.w(AppConfig.TAG, "BootReceiver: No server selected")
|
||||
return
|
||||
}
|
||||
|
||||
Log.i(AppConfig.TAG, "BootReceiver: Starting V2Ray service")
|
||||
V2RayServiceManager.startVService(context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.v2ray.ang.service
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@@ -28,6 +29,7 @@ import com.v2ray.ang.util.MyContextWrapper
|
||||
import com.v2ray.ang.util.Utils
|
||||
import java.lang.ref.SoftReference
|
||||
|
||||
@SuppressLint("VpnServicePolicy")
|
||||
class V2RayVpnService : VpnService(), ServiceControl {
|
||||
private lateinit var mInterface: ParcelFileDescriptor
|
||||
private var isRunning = false
|
||||
@@ -103,7 +105,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
}
|
||||
|
||||
override fun startService() {
|
||||
if (mInterface == null) {
|
||||
if (!::mInterface.isInitialized) {
|
||||
Log.e(AppConfig.TAG, "Failed to create VPN interface")
|
||||
return
|
||||
}
|
||||
@@ -136,7 +138,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
private fun setupVpnService() {
|
||||
val prepare = prepare(this)
|
||||
if (prepare != null) {
|
||||
Log.e(AppConfig.TAG, "VPN preparation failed")
|
||||
Log.e(AppConfig.TAG, "VPN preparation failed - VPN permission not granted")
|
||||
stopSelf()
|
||||
return
|
||||
}
|
||||
@@ -165,9 +167,11 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
|
||||
// Close the old interface since the parameters have been changed
|
||||
try {
|
||||
mInterface.close()
|
||||
} catch (ignored: Exception) {
|
||||
// ignored
|
||||
if (::mInterface.isInitialized) {
|
||||
mInterface.close()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(AppConfig.TAG, "Failed to close old interface", e)
|
||||
}
|
||||
|
||||
// Configure platform-specific features
|
||||
@@ -330,8 +334,8 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
try {
|
||||
connectivity.unregisterNetworkCallback(defaultNetworkCallback)
|
||||
} catch (ignored: Exception) {
|
||||
// ignored
|
||||
} catch (e: Exception) {
|
||||
Log.w(AppConfig.TAG, "Failed to unregister network callback", e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,7 +353,9 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
stopSelf()
|
||||
|
||||
try {
|
||||
mInterface.close()
|
||||
if (::mInterface.isInitialized) {
|
||||
mInterface.close()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Failed to close VPN interface", e)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user