diff --git a/.github/update.log b/.github/update.log index 7d8989cbcb..a8f60d1406 100644 --- a/.github/update.log +++ b/.github/update.log @@ -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 diff --git a/clash-meta/go.mod b/clash-meta/go.mod index b917b3b57f..7e82371848 100644 --- a/clash-meta/go.mod +++ b/clash-meta/go.mod @@ -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 diff --git a/clash-meta/go.sum b/clash-meta/go.sum index f995c37678..91b61d786e 100644 --- a/clash-meta/go.sum +++ b/clash-meta/go.sum @@ -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= diff --git a/mieru/Makefile b/mieru/Makefile index 561594b7be..d71718395f 100644 --- a/mieru/Makefile +++ b/mieru/Makefile @@ -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. diff --git a/mieru/build/package/mieru/amd64/debian/DEBIAN/control b/mieru/build/package/mieru/amd64/debian/DEBIAN/control index 579957516f..49eb3a3eca 100755 --- a/mieru/build/package/mieru/amd64/debian/DEBIAN/control +++ b/mieru/build/package/mieru/amd64/debian/DEBIAN/control @@ -1,5 +1,5 @@ Package: mieru -Version: 3.28.0 +Version: 3.29.0 Section: net Priority: optional Architecture: amd64 diff --git a/mieru/build/package/mieru/amd64/rpm/mieru.spec b/mieru/build/package/mieru/amd64/rpm/mieru.spec index 5c97db1925..48627c41b0 100644 --- a/mieru/build/package/mieru/amd64/rpm/mieru.spec +++ b/mieru/build/package/mieru/amd64/rpm/mieru.spec @@ -1,5 +1,5 @@ Name: mieru -Version: 3.28.0 +Version: 3.29.0 Release: 1%{?dist} Summary: Mieru proxy client License: GPLv3+ diff --git a/mieru/build/package/mieru/arm64/debian/DEBIAN/control b/mieru/build/package/mieru/arm64/debian/DEBIAN/control index b4c1c24678..7aa465665b 100755 --- a/mieru/build/package/mieru/arm64/debian/DEBIAN/control +++ b/mieru/build/package/mieru/arm64/debian/DEBIAN/control @@ -1,5 +1,5 @@ Package: mieru -Version: 3.28.0 +Version: 3.29.0 Section: net Priority: optional Architecture: arm64 diff --git a/mieru/build/package/mieru/arm64/rpm/mieru.spec b/mieru/build/package/mieru/arm64/rpm/mieru.spec index 5c97db1925..48627c41b0 100644 --- a/mieru/build/package/mieru/arm64/rpm/mieru.spec +++ b/mieru/build/package/mieru/arm64/rpm/mieru.spec @@ -1,5 +1,5 @@ Name: mieru -Version: 3.28.0 +Version: 3.29.0 Release: 1%{?dist} Summary: Mieru proxy client License: GPLv3+ diff --git a/mieru/build/package/mita/amd64/debian/DEBIAN/control b/mieru/build/package/mita/amd64/debian/DEBIAN/control index 506e387d21..de22746617 100755 --- a/mieru/build/package/mita/amd64/debian/DEBIAN/control +++ b/mieru/build/package/mita/amd64/debian/DEBIAN/control @@ -1,5 +1,5 @@ Package: mita -Version: 3.28.0 +Version: 3.29.0 Section: net Priority: optional Architecture: amd64 diff --git a/mieru/build/package/mita/amd64/rpm/mita.spec b/mieru/build/package/mita/amd64/rpm/mita.spec index 6a40bda667..a8c99a8ee6 100644 --- a/mieru/build/package/mita/amd64/rpm/mita.spec +++ b/mieru/build/package/mita/amd64/rpm/mita.spec @@ -1,5 +1,5 @@ Name: mita -Version: 3.28.0 +Version: 3.29.0 Release: 1%{?dist} Summary: Mieru proxy server License: GPLv3+ diff --git a/mieru/build/package/mita/arm64/debian/DEBIAN/control b/mieru/build/package/mita/arm64/debian/DEBIAN/control index b3baa6beed..ef1b2c86aa 100755 --- a/mieru/build/package/mita/arm64/debian/DEBIAN/control +++ b/mieru/build/package/mita/arm64/debian/DEBIAN/control @@ -1,5 +1,5 @@ Package: mita -Version: 3.28.0 +Version: 3.29.0 Section: net Priority: optional Architecture: arm64 diff --git a/mieru/build/package/mita/arm64/rpm/mita.spec b/mieru/build/package/mita/arm64/rpm/mita.spec index 718d4dec9b..e820d41709 100644 --- a/mieru/build/package/mita/arm64/rpm/mita.spec +++ b/mieru/build/package/mita/arm64/rpm/mita.spec @@ -1,5 +1,5 @@ Name: mita -Version: 3.28.0 +Version: 3.29.0 Release: 1%{?dist} Summary: Mieru proxy server License: GPLv3+ diff --git a/mieru/docs/server-install.md b/mieru/docs/server-install.md index 556d02b30b..a14ee364aa 100644 --- a/mieru/docs/server-install.md +++ b/mieru/docs/server-install.md @@ -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. diff --git a/mieru/docs/server-install.zh_CN.md b/mieru/docs/server-install.zh_CN.md index b24f88741f..3d653ba126 100644 --- a/mieru/docs/server-install.zh_CN.md +++ b/mieru/docs/server-install.zh_CN.md @@ -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 软件包的版本。 diff --git a/mieru/pkg/protocol/session.go b/mieru/pkg/protocol/session.go index 4d27a35e16..390a8a1f24 100644 --- a/mieru/pkg/protocol/session.go +++ b/mieru/pkg/protocol/session.go @@ -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) { diff --git a/mieru/pkg/protocol/underlay_base.go b/mieru/pkg/protocol/underlay_base.go index 7dbc906bda..156b67973d 100644 --- a/mieru/pkg/protocol/underlay_base.go +++ b/mieru/pkg/protocol/underlay_base.go @@ -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 } diff --git a/mieru/pkg/protocol/underlay_packet.go b/mieru/pkg/protocol/underlay_packet.go index 321705b06a..bb4c85727e 100644 --- a/mieru/pkg/protocol/underlay_packet.go +++ b/mieru/pkg/protocol/underlay_packet.go @@ -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) } diff --git a/mieru/pkg/protocol/underlay_stream.go b/mieru/pkg/protocol/underlay_stream.go index 200010b871..9d344f9add 100644 --- a/mieru/pkg/protocol/underlay_stream.go +++ b/mieru/pkg/protocol/underlay_stream.go @@ -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) + } } } diff --git a/mieru/pkg/version/current.go b/mieru/pkg/version/current.go index 33b18fceef..3b830daba2 100644 --- a/mieru/pkg/version/current.go +++ b/mieru/pkg/version/current.go @@ -16,5 +16,5 @@ package version const ( - AppVersion = "3.28.0" + AppVersion = "3.29.0" ) diff --git a/mihomo/go.mod b/mihomo/go.mod index b917b3b57f..7e82371848 100644 --- a/mihomo/go.mod +++ b/mihomo/go.mod @@ -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 diff --git a/mihomo/go.sum b/mihomo/go.sum index f995c37678..91b61d786e 100644 --- a/mihomo/go.sum +++ b/mihomo/go.sum @@ -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= diff --git a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua index c3e5f09289..20aa1e20eb 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua @@ -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 diff --git a/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_xray.lua b/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_xray.lua index 49b054640f..9bcacff2f0 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_xray.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_xray.lua @@ -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 diff --git a/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/rule_update.lua b/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/rule_update.lua index 70373b5921..d1528cc44e 100755 --- a/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/rule_update.lua +++ b/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/rule_update.lua @@ -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 diff --git a/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/subscribe.lua b/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/subscribe.lua index b880031afe..7769ccbd85 100755 --- a/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/subscribe.lua +++ b/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/subscribe.lua @@ -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 diff --git a/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua b/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua index f20d83a080..aa4a4085d7 100755 --- a/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua +++ b/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua @@ -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 diff --git a/small/.github/workflows/T2 build.yml b/small/.github/workflows/T2 build.yml index 1042d08b32..ee07f0e52d 100644 --- a/small/.github/workflows/T2 build.yml +++ b/small/.github/workflows/T2 build.yml @@ -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 diff --git a/small/.github/workflows/T9 build.yml b/small/.github/workflows/T9 build.yml index 73acea0606..22b7f45b34 100644 --- a/small/.github/workflows/T9 build.yml +++ b/small/.github/workflows/T9 build.yml @@ -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 diff --git a/small/gn/Makefile b/small/gn/Makefile index 363cf2099a..2dc4765eb6 100644 --- a/small/gn/Makefile +++ b/small/gn/Makefile @@ -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 diff --git a/small/gn/src/out/last_commit_position.h b/small/gn/src/out/last_commit_position.h index 5949e552f7..1533c849a9 100644 --- a/small/gn/src/out/last_commit_position.h +++ b/small/gn/src/out/last_commit_position.h @@ -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_ diff --git a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua index c3e5f09289..20aa1e20eb 100644 --- a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua +++ b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua @@ -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 diff --git a/small/luci-app-passwall/luasrc/passwall/util_xray.lua b/small/luci-app-passwall/luasrc/passwall/util_xray.lua index 49b054640f..9bcacff2f0 100644 --- a/small/luci-app-passwall/luasrc/passwall/util_xray.lua +++ b/small/luci-app-passwall/luasrc/passwall/util_xray.lua @@ -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 diff --git a/small/luci-app-passwall/root/usr/share/passwall/rule_update.lua b/small/luci-app-passwall/root/usr/share/passwall/rule_update.lua index 70373b5921..d1528cc44e 100755 --- a/small/luci-app-passwall/root/usr/share/passwall/rule_update.lua +++ b/small/luci-app-passwall/root/usr/share/passwall/rule_update.lua @@ -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 diff --git a/small/luci-app-passwall/root/usr/share/passwall/subscribe.lua b/small/luci-app-passwall/root/usr/share/passwall/subscribe.lua index b880031afe..7769ccbd85 100755 --- a/small/luci-app-passwall/root/usr/share/passwall/subscribe.lua +++ b/small/luci-app-passwall/root/usr/share/passwall/subscribe.lua @@ -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 diff --git a/small/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua b/small/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua index f20d83a080..aa4a4085d7 100755 --- a/small/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua +++ b/small/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua @@ -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 diff --git a/small/v2ray-geodata/Makefile b/small/v2ray-geodata/Makefile index 387129f706..eda025abce 100644 --- a/small/v2ray-geodata/Makefile +++ b/small/v2ray-geodata/Makefile @@ -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 diff --git a/v2rayn/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs b/v2rayn/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs index 9a3aea8409..1cbfbf14f5 100644 --- a/v2rayn/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs +++ b/v2rayn/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs @@ -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 childItems = [prevNode?.IndexId, node.IndexId, nextNode?.IndexId]; var chainExtraItem = chainNode.GetProtocolExtra() with diff --git a/v2rayn/v2rayN/ServiceLib/Handler/ConfigHandler.cs b/v2rayn/v2rayN/ServiceLib/Handler/ConfigHandler.cs index de4c774db9..ce185fb79a 100644 --- a/v2rayn/v2rayN/ServiceLib/Handler/ConfigHandler.cs +++ b/v2rayn/v2rayN/ServiceLib/Handler/ConfigHandler.cs @@ -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(); diff --git a/v2rayn/v2rayN/ServiceLib/Models/ConfigItems.cs b/v2rayn/v2rayN/ServiceLib/Models/ConfigItems.cs index 7dc8c2669d..56e5fe47d1 100644 --- a/v2rayn/v2rayN/ServiceLib/Models/ConfigItems.cs +++ b/v2rayn/v2rayN/ServiceLib/Models/ConfigItems.cs @@ -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 ConnectionsColumnItem { get; set; } } [Serializable] diff --git a/v2rayn/v2rayN/v2rayN.Desktop/Views/ClashConnectionsView.axaml b/v2rayn/v2rayN/v2rayN.Desktop/Views/ClashConnectionsView.axaml index 21bad1386b..6a9b7088dc 100644 --- a/v2rayn/v2rayN/v2rayN.Desktop/Views/ClashConnectionsView.axaml +++ b/v2rayn/v2rayN/v2rayN.Desktop/Views/ClashConnectionsView.axaml @@ -74,23 +74,28 @@ + Header="{x:Static resx:ResUI.TbSortingHost}" + Tag="Host" /> + Header="{x:Static resx:ResUI.TbSortingChain}" + Tag="Chain" /> + Header="{x:Static resx:ResUI.TbSortingNetwork}" + Tag="Network" /> + Header="{x:Static resx:ResUI.TbSortingType}" + Tag="Type" /> + Header="{x:Static resx:ResUI.TbSortingTime}" + Tag="Elapsed" /> diff --git a/v2rayn/v2rayN/v2rayN.Desktop/Views/ClashConnectionsView.axaml.cs b/v2rayn/v2rayN/v2rayN.Desktop/Views/ClashConnectionsView.axaml.cs index 1c39df7226..23722ad040 100644 --- a/v2rayn/v2rayN/v2rayN.Desktop/Views/ClashConnectionsView.axaml.cs +++ b/v2rayn/v2rayN/v2rayN.Desktop/Views/ClashConnectionsView.axaml.cs @@ -2,9 +2,15 @@ namespace v2rayN.Desktop.Views; public partial class ClashConnectionsView : ReactiveUserControl { + 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 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 UpdateViewHandler(EViewAction action, object? obj) @@ -51,4 +65,74 @@ public partial class ClashConnectionsView : ReactiveUserControl 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 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 } diff --git a/v2rayn/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml.cs b/v2rayn/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml.cs index 4ca8e7930e..40336be55c 100644 --- a/v2rayn/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml.cs +++ b/v2rayn/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml.cs @@ -7,6 +7,7 @@ public partial class ProfilesView : ReactiveUserControl { private static Config _config; private Window? _window; + private static readonly string _tag = "ProfilesView"; public ProfilesView() { @@ -381,7 +382,7 @@ public partial class ProfilesView : ReactiveUserControl } catch (Exception ex) { - Logging.SaveLog("ProfilesView", ex); + Logging.SaveLog(_tag, ex); } } @@ -399,53 +400,67 @@ public partial class ProfilesView : ReactiveUserControl 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 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 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 diff --git a/v2rayn/v2rayN/v2rayN/Views/ClashConnectionsView.xaml b/v2rayn/v2rayN/v2rayN/Views/ClashConnectionsView.xaml index 0e6ba6a720..067926ffb7 100644 --- a/v2rayn/v2rayN/v2rayN/Views/ClashConnectionsView.xaml +++ b/v2rayn/v2rayN/v2rayN/Views/ClashConnectionsView.xaml @@ -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 @@ - - - - - diff --git a/v2rayn/v2rayN/v2rayN/Views/ClashConnectionsView.xaml.cs b/v2rayn/v2rayN/v2rayN/Views/ClashConnectionsView.xaml.cs index 0a6245a985..47482b8edf 100644 --- a/v2rayn/v2rayN/v2rayN/Views/ClashConnectionsView.xaml.cs +++ b/v2rayn/v2rayN/v2rayN/Views/ClashConnectionsView.xaml.cs @@ -1,4 +1,5 @@ using System.Windows.Controls; +using v2rayN.Base; namespace v2rayN.Views; @@ -7,9 +8,14 @@ namespace v2rayN.Views; /// 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 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()) + { + 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 lvColumnItem = new(); + foreach (var col in lstConnections.Columns.Cast()) + { + 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 } diff --git a/v2rayn/v2rayN/v2rayN/Views/ProfilesView.xaml.cs b/v2rayn/v2rayN/v2rayN/Views/ProfilesView.xaml.cs index 46def3e2cf..1440e6ec0a 100644 --- a/v2rayn/v2rayN/v2rayN/Views/ProfilesView.xaml.cs +++ b/v2rayn/v2rayN/v2rayN/Views/ProfilesView.xaml.cs @@ -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()) { - 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 lvColumnItem = new(); - foreach (var t in lstProfiles.Columns) + try { - var item2 = (MyDGTextColumn)t; - lvColumnItem.Add(new() + List lvColumnItem = new(); + foreach (var item2 in lstProfiles.Columns.Cast()) { - 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"; /// /// Helper to search up the VisualTree diff --git a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/BootReceiver.kt b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/BootReceiver.kt index 3b47b634aa..c4af8b6c13 100644 --- a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/BootReceiver.kt +++ b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/BootReceiver.kt @@ -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) } } diff --git a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayVpnService.kt b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayVpnService.kt index 323be01d62..38af7a90ad 100644 --- a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayVpnService.kt +++ b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayVpnService.kt @@ -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) }