diff --git a/.github/update.log b/.github/update.log index f5400e9b71..d11fc1faa5 100644 --- a/.github/update.log +++ b/.github/update.log @@ -1230,3 +1230,4 @@ Update On Mon Dec 29 19:42:01 CET 2025 Update On Tue Dec 30 19:43:26 CET 2025 Update On Wed Dec 31 19:39:51 CET 2025 Update On Thu Jan 1 19:42:21 CET 2026 +Update On Fri Jan 2 19:40:09 CET 2026 diff --git a/clash-meta/dns/util.go b/clash-meta/dns/util.go index e960a8e198..d46cd65c7b 100644 --- a/clash-meta/dns/util.go +++ b/clash-meta/dns/util.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net/netip" + "strconv" "strings" "time" @@ -15,6 +16,7 @@ import ( D "github.com/miekg/dns" "github.com/samber/lo" + "golang.org/x/exp/slices" ) const ( @@ -108,37 +110,75 @@ func transform(servers []NameServer, resolver *Resolver) []dnsClient { } c = warpClientWithEdns0Subnet(c, s.Params) - - if s.Params["disable-ipv4"] == "true" { - c = warpClientWithDisableType(c, D.TypeA) - } - - if s.Params["disable-ipv6"] == "true" { - c = warpClientWithDisableType(c, D.TypeAAAA) - } + c = warpClientWithDisableTypes(c, s.Params) ret = append(ret, c) } return ret } -type clientWithDisableType struct { +type clientWithDisableTypes struct { dnsClient - qType uint16 + disableTypes map[uint16]struct{} } -func (c clientWithDisableType) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { - if len(m.Question) > 0 { - q := m.Question[0] - if q.Qtype == c.qType { - return handleMsgWithEmptyAnswer(m), nil +func (c clientWithDisableTypes) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { + // filter dns request + if slices.ContainsFunc(m.Question, c.inQuestion) { + // In fact, DNS requests are not allowed to contain multiple questions: + // https://stackoverflow.com/questions/4082081/requesting-a-and-aaaa-records-in-single-dns-query/4083071 + // so, when we find a question containing the type, we can simply discard the entire dns request. + return handleMsgWithEmptyAnswer(m), nil + } + + // do real exchange + msg, err = c.dnsClient.ExchangeContext(ctx, m) + if err != nil { + return + } + + // filter dns response + msg.Answer = slices.DeleteFunc(msg.Answer, c.inRR) + msg.Ns = slices.DeleteFunc(msg.Ns, c.inRR) + msg.Extra = slices.DeleteFunc(msg.Extra, c.inRR) + return +} + +func (c clientWithDisableTypes) inQuestion(q D.Question) bool { + _, ok := c.disableTypes[q.Qtype] + return ok +} + +func (c clientWithDisableTypes) inRR(rr D.RR) bool { + _, ok := c.disableTypes[rr.Header().Rrtype] + return ok +} + +func warpClientWithDisableTypes(c dnsClient, params map[string]string) dnsClient { + disableTypes := make(map[uint16]struct{}) + if params["disable-ipv4"] == "true" { + disableTypes[D.TypeA] = struct{}{} + } + if params["disable-ipv6"] == "true" { + disableTypes[D.TypeAAAA] = struct{}{} + } + for key, value := range params { + const prefix = "disable-qtype-" + if strings.HasPrefix(key, prefix) && value == "true" { // eg: disable-qtype-65=true + qType, err := strconv.ParseUint(key[len(prefix):], 10, 16) + if err != nil { + continue + } + if _, ok := D.TypeToRR[uint16(qType)]; !ok { // check valid RR_Header.Rrtype and Question.qtype + continue + } + disableTypes[uint16(qType)] = struct{}{} } } - return c.dnsClient.ExchangeContext(ctx, m) -} - -func warpClientWithDisableType(c dnsClient, qType uint16) dnsClient { - return clientWithDisableType{c, qType} + if len(disableTypes) > 0 { + return clientWithDisableTypes{c, disableTypes} + } + return c } type clientWithEdns0Subnet struct { diff --git a/clash-meta/go.mod b/clash-meta/go.mod index 6f18e9e843..c991e42116 100644 --- a/clash-meta/go.mod +++ b/clash-meta/go.mod @@ -26,7 +26,7 @@ require ( github.com/metacubex/http v0.1.0 github.com/metacubex/kcp-go v0.0.0-20251111012849-7455698490e9 github.com/metacubex/mlkem v0.1.0 - github.com/metacubex/quic-go v0.58.1-0.20251222092318-72a81ab195ec + github.com/metacubex/quic-go v0.57.1-0.20251217071004-e89f497a2e72 github.com/metacubex/randv2 v0.2.0 github.com/metacubex/restls-client-go v0.1.7 github.com/metacubex/sing v0.5.6 diff --git a/clash-meta/go.sum b/clash-meta/go.sum index 08bd736b30..4786479fe2 100644 --- a/clash-meta/go.sum +++ b/clash-meta/go.sum @@ -114,8 +114,8 @@ github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 h1:1Qpuy+sU3Dmy github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793/go.mod h1:RjRNb4G52yAgfR+Oe/kp9G4PJJ97Fnj89eY1BFO3YyA= github.com/metacubex/qpack v0.6.0 h1:YqClGIMOpiRYLjV1qOs483Od08MdPgRnHjt90FuaAKw= github.com/metacubex/qpack v0.6.0/go.mod h1:lKGSi7Xk94IMvHGOmxS9eIei3bvIqpOAImEBsaOwTkA= -github.com/metacubex/quic-go v0.58.1-0.20251222092318-72a81ab195ec h1:5ePGO2Xht06fpwjNIzfY5XS+82xwDHHx4xGbqgLbxjA= -github.com/metacubex/quic-go v0.58.1-0.20251222092318-72a81ab195ec/go.mod h1:oNzMrmylS897M3zSMuapIdwSwfq6F2qW01Z3NhVRJhk= +github.com/metacubex/quic-go v0.57.1-0.20251217071004-e89f497a2e72 h1:kNlYHZ75itJwkerDiySpixX+dKsv/K0TYQsKvuxogNM= +github.com/metacubex/quic-go v0.57.1-0.20251217071004-e89f497a2e72/go.mod h1:N071X2oW2+kIhLlHW3mfcD2QP+zWu2bEs1EEAm66bvI= github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs= github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY= github.com/metacubex/restls-client-go v0.1.7 h1:eCwiXCTQb5WJu9IlgYvDBA1OgrINv58dEe7hcN5H15k= diff --git a/lede/include/prereq-build.mk b/lede/include/prereq-build.mk index c7eae1958f..7db8309500 100644 --- a/lede/include/prereq-build.mk +++ b/lede/include/prereq-build.mk @@ -161,8 +161,8 @@ $(eval $(call SetupHostCommand,bzip2,Please install 'bzip2', \ $(eval $(call SetupHostCommand,wget,Please install GNU 'wget', \ wget --version | grep GNU)) -$(eval $(call SetupHostCommand,install,Please install GNU 'install', \ - install --version | grep GNU, \ +$(eval $(call SetupHostCommand,install,Please install 'install', \ + install --version | grep 'GNU\|uutils', \ ginstall --version | grep GNU)) $(eval $(call SetupHostCommand,perl,Please install Perl 5.x, \ diff --git a/mieru/pkg/protocol/session.go b/mieru/pkg/protocol/session.go index d1867ce918..a16ceaadc2 100644 --- a/mieru/pkg/protocol/session.go +++ b/mieru/pkg/protocol/session.go @@ -115,8 +115,8 @@ type Session struct { ready chan struct{} // indicate the session is ready to use closeRequested atomic.Bool // the session is being closed or has been closed closedChan chan struct{} // indicate the session is closed - readDeadline time.Time // read deadline - writeDeadline time.Time // write deadline + readDeadline atomic.Int64 // read deadline, in microseconds since Unix epoch + writeDeadline atomic.Int64 // write deadline, in microseconds since Unix epoch inputHasErr atomic.Bool // input has error inputErr chan error // this channel is closed when input has error outputHasErr atomic.Bool // output has error @@ -176,8 +176,6 @@ func NewSession(id uint32, isClient bool, mtu int, users map[string]*appctlpb.Us users: users, ready: make(chan struct{}), closedChan: make(chan struct{}), - readDeadline: time.Time{}, - writeDeadline: time.Time{}, inputErr: make(chan error), outputErr: make(chan error), sendQueue: newSegmentTree(segmentTreeCapacity), @@ -210,7 +208,7 @@ func (s *Session) Read(b []byte) (n int, err error) { defer s.rLock.Unlock() defer func() { - s.readDeadline = time.Time{} + s.readDeadline.Store(0) }() if log.IsLevelEnabled(log.TraceLevel) { log.Tracef("%v trying to read %d bytes", s, len(b)) @@ -235,8 +233,8 @@ func (s *Session) Read(b []byte) (n int, err error) { // Stop reading when deadline is reached. var timeC <-chan time.Time - if !s.readDeadline.IsZero() { - timeC = time.After(time.Until(s.readDeadline)) + if readDeadline := s.readDeadline.Load(); readDeadline != 0 { + timeC = time.After(time.Until(time.UnixMicro(readDeadline))) } for { @@ -304,7 +302,7 @@ func (s *Session) Write(b []byte) (n int, err error) { return 0, io.ErrClosedPipe } defer func() { - s.writeDeadline = time.Time{} + s.writeDeadline.Store(0) }() // Before the first write, client needs to send open session request. @@ -385,20 +383,32 @@ func (s *Session) RemoteAddr() net.Addr { // SetDeadline implements net.Conn. func (s *Session) SetDeadline(t time.Time) error { - s.readDeadline = t - s.writeDeadline = t + micros := t.UnixMicro() + if t.IsZero() { + micros = 0 + } + s.readDeadline.Store(micros) + s.writeDeadline.Store(micros) return nil } // SetReadDeadline implements net.Conn. func (s *Session) SetReadDeadline(t time.Time) error { - s.readDeadline = t + micros := t.UnixMicro() + if t.IsZero() { + micros = 0 + } + s.readDeadline.Store(micros) return nil } // SetWriteDeadline implements net.Conn. func (s *Session) SetWriteDeadline(t time.Time) error { - s.writeDeadline = t + micros := t.UnixMicro() + if t.IsZero() { + micros = 0 + } + s.writeDeadline.Store(micros) return nil } @@ -500,8 +510,8 @@ func (s *Session) writeChunk(b []byte) (n int, err error) { // Stop writing when deadline is reached. var timeC <-chan time.Time - if !s.writeDeadline.IsZero() { - timeC = time.After(time.Until(s.writeDeadline)) + if writeDeadline := s.writeDeadline.Load(); writeDeadline != 0 { + timeC = time.After(time.Until(time.UnixMicro(writeDeadline))) } seqBeforeWrite, _ := s.sendQueue.MinSeq() @@ -596,7 +606,7 @@ func (s *Session) writeChunk(b []byte) (n int, err error) { } if s.isClient { - s.readDeadline = time.Now().Add(serverRespTimeout) + s.readDeadline.Store(time.Now().Add(serverRespTimeout).UnixMicro()) } return len(b), nil } diff --git a/mihomo/dns/util.go b/mihomo/dns/util.go index e960a8e198..d46cd65c7b 100644 --- a/mihomo/dns/util.go +++ b/mihomo/dns/util.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net/netip" + "strconv" "strings" "time" @@ -15,6 +16,7 @@ import ( D "github.com/miekg/dns" "github.com/samber/lo" + "golang.org/x/exp/slices" ) const ( @@ -108,37 +110,75 @@ func transform(servers []NameServer, resolver *Resolver) []dnsClient { } c = warpClientWithEdns0Subnet(c, s.Params) - - if s.Params["disable-ipv4"] == "true" { - c = warpClientWithDisableType(c, D.TypeA) - } - - if s.Params["disable-ipv6"] == "true" { - c = warpClientWithDisableType(c, D.TypeAAAA) - } + c = warpClientWithDisableTypes(c, s.Params) ret = append(ret, c) } return ret } -type clientWithDisableType struct { +type clientWithDisableTypes struct { dnsClient - qType uint16 + disableTypes map[uint16]struct{} } -func (c clientWithDisableType) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { - if len(m.Question) > 0 { - q := m.Question[0] - if q.Qtype == c.qType { - return handleMsgWithEmptyAnswer(m), nil +func (c clientWithDisableTypes) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { + // filter dns request + if slices.ContainsFunc(m.Question, c.inQuestion) { + // In fact, DNS requests are not allowed to contain multiple questions: + // https://stackoverflow.com/questions/4082081/requesting-a-and-aaaa-records-in-single-dns-query/4083071 + // so, when we find a question containing the type, we can simply discard the entire dns request. + return handleMsgWithEmptyAnswer(m), nil + } + + // do real exchange + msg, err = c.dnsClient.ExchangeContext(ctx, m) + if err != nil { + return + } + + // filter dns response + msg.Answer = slices.DeleteFunc(msg.Answer, c.inRR) + msg.Ns = slices.DeleteFunc(msg.Ns, c.inRR) + msg.Extra = slices.DeleteFunc(msg.Extra, c.inRR) + return +} + +func (c clientWithDisableTypes) inQuestion(q D.Question) bool { + _, ok := c.disableTypes[q.Qtype] + return ok +} + +func (c clientWithDisableTypes) inRR(rr D.RR) bool { + _, ok := c.disableTypes[rr.Header().Rrtype] + return ok +} + +func warpClientWithDisableTypes(c dnsClient, params map[string]string) dnsClient { + disableTypes := make(map[uint16]struct{}) + if params["disable-ipv4"] == "true" { + disableTypes[D.TypeA] = struct{}{} + } + if params["disable-ipv6"] == "true" { + disableTypes[D.TypeAAAA] = struct{}{} + } + for key, value := range params { + const prefix = "disable-qtype-" + if strings.HasPrefix(key, prefix) && value == "true" { // eg: disable-qtype-65=true + qType, err := strconv.ParseUint(key[len(prefix):], 10, 16) + if err != nil { + continue + } + if _, ok := D.TypeToRR[uint16(qType)]; !ok { // check valid RR_Header.Rrtype and Question.qtype + continue + } + disableTypes[uint16(qType)] = struct{}{} } } - return c.dnsClient.ExchangeContext(ctx, m) -} - -func warpClientWithDisableType(c dnsClient, qType uint16) dnsClient { - return clientWithDisableType{c, qType} + if len(disableTypes) > 0 { + return clientWithDisableTypes{c, disableTypes} + } + return c } type clientWithEdns0Subnet struct { diff --git a/mihomo/go.mod b/mihomo/go.mod index 6f18e9e843..c991e42116 100644 --- a/mihomo/go.mod +++ b/mihomo/go.mod @@ -26,7 +26,7 @@ require ( github.com/metacubex/http v0.1.0 github.com/metacubex/kcp-go v0.0.0-20251111012849-7455698490e9 github.com/metacubex/mlkem v0.1.0 - github.com/metacubex/quic-go v0.58.1-0.20251222092318-72a81ab195ec + github.com/metacubex/quic-go v0.57.1-0.20251217071004-e89f497a2e72 github.com/metacubex/randv2 v0.2.0 github.com/metacubex/restls-client-go v0.1.7 github.com/metacubex/sing v0.5.6 diff --git a/mihomo/go.sum b/mihomo/go.sum index 08bd736b30..4786479fe2 100644 --- a/mihomo/go.sum +++ b/mihomo/go.sum @@ -114,8 +114,8 @@ github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 h1:1Qpuy+sU3Dmy github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793/go.mod h1:RjRNb4G52yAgfR+Oe/kp9G4PJJ97Fnj89eY1BFO3YyA= github.com/metacubex/qpack v0.6.0 h1:YqClGIMOpiRYLjV1qOs483Od08MdPgRnHjt90FuaAKw= github.com/metacubex/qpack v0.6.0/go.mod h1:lKGSi7Xk94IMvHGOmxS9eIei3bvIqpOAImEBsaOwTkA= -github.com/metacubex/quic-go v0.58.1-0.20251222092318-72a81ab195ec h1:5ePGO2Xht06fpwjNIzfY5XS+82xwDHHx4xGbqgLbxjA= -github.com/metacubex/quic-go v0.58.1-0.20251222092318-72a81ab195ec/go.mod h1:oNzMrmylS897M3zSMuapIdwSwfq6F2qW01Z3NhVRJhk= +github.com/metacubex/quic-go v0.57.1-0.20251217071004-e89f497a2e72 h1:kNlYHZ75itJwkerDiySpixX+dKsv/K0TYQsKvuxogNM= +github.com/metacubex/quic-go v0.57.1-0.20251217071004-e89f497a2e72/go.mod h1:N071X2oW2+kIhLlHW3mfcD2QP+zWu2bEs1EEAm66bvI= github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs= github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY= github.com/metacubex/restls-client-go v0.1.7 h1:eCwiXCTQb5WJu9IlgYvDBA1OgrINv58dEe7hcN5H15k= diff --git a/nodepass/README.md b/nodepass/README.md index da8cb3203e..5057373609 100644 --- a/nodepass/README.md +++ b/nodepass/README.md @@ -118,9 +118,15 @@ Project **NodePass** is licensed under the [BSD 3-Clause License](LICENSE). This project is provided "as is" without any warranties. Users assume all risks and must comply with local laws for legal use only. Developers are not liable for any direct, indirect, incidental, or consequential damages. Secondary development requires commitment to legal use and self-responsibility for legal compliance. Developers reserve the right to modify software features and this disclaimer at any time. Final interpretation rights belong to developers. -## 🔗 NFT Support +## 🔗 Donation -Support **NodePass** in a unique way by checking out our NFT collection on [OpenSea](https://opensea.io/collection/nodepass). +**Cryptocurrency:** + +- EVM-compatible Address: `0x2ea4Ea9425BEe897ED74fC5512bd13ABC7100000` + +**NFT collection:** + +- Support **NodePass** in a unique way by checking out our NFT collection on [OpenSea](https://opensea.io/collection/nodepass). ## 🤝 Sponsors diff --git a/nodepass/README_zh.md b/nodepass/README_zh.md index be38d594ef..03bd2c9eea 100644 --- a/nodepass/README_zh.md +++ b/nodepass/README_zh.md @@ -118,9 +118,15 @@ nodepass "master://:10101/api?log=debug&tls=1" 本项目以"现状"提供,开发者不提供任何明示或暗示的保证。用户使用风险自担,需遵守当地法律法规,仅限合法用途。开发者对任何直接、间接、偶然或后果性损害概不负责。进行二次开发须承诺合法使用并自负法律责任。开发者保留随时修改软件功能及本声明的权利。最终解释权归开发者所有。 -## 🔗 NFT 支持 +## 🔗 捐赠 -以独特方式支持 **NodePass**,查看我们在 [OpenSea](https://opensea.io/collection/nodepass) 上的 NFT 收藏。 +**加密货币:** + +- EVM 兼容地址: `0x2ea4Ea9425BEe897ED74fC5512bd13ABC7100000` + +**数字藏品:** + +- 以独特方式支持 **NodePass**,查看我们在 [OpenSea](https://opensea.io/collection/nodepass) 上的 NFT 收藏。 ## 🤝 赞助商 diff --git a/nodepass/docs/en/how-it-works.md b/nodepass/docs/en/how-it-works.md index dd579fd283..090b98eb62 100644 --- a/nodepass/docs/en/how-it-works.md +++ b/nodepass/docs/en/how-it-works.md @@ -73,8 +73,8 @@ The data flow mode is automatically determined based on tunnel address and targe ``` [Server] → [Generate Unique Connection ID] → [Signal Client via Unencrypted TCP Tunnel] ``` - - For TCP: Generates a `//#1` signal - - For UDP: Generates a `//#2` signal + - For TCP: Generates a `{"action":"tcp","remote":"target_addr","id":"connection_id"}` signal + - For UDP: Generates a `{"action":"udp","remote":"client_addr","id":"connection_id"}` signal 3. **Connection Preparation**: ``` @@ -95,7 +95,7 @@ The data flow mode is automatically determined based on tunnel address and targe ``` [Client] → [Read Signal from TCP Tunnel] → [Parse Connection ID] ``` - - Client differentiates between TCP and UDP signals based on URL scheme + - Client differentiates between signal types based on the action field 2. **Connection Establishment**: ``` @@ -163,27 +163,42 @@ The data flow mode is automatically determined based on tunnel address and targe ## Signal Communication Mechanism -NodePass uses a sophisticated URL-based signaling protocol through the TCP tunnel: +NodePass uses a JSON-based signaling protocol through the TCP tunnel: ### Signal Types -1. **Tunnel Signal**: - - Format: `#` - - Purpose: Informs the client about the tls code - - Timing: Sent on tunnel handshake +1. **Flush Signal**: + - Format: `{"action":"flush"}` + - Purpose: Flushes the connection pool and resets error count + - Timing: Sent when connection pool health check fails -2. **TCP Launch Signal**: - - Format: `//#1` +2. **PING Signal**: + - Format: `{"action":"ping"}` + - Purpose: Checks client connection status and requests PONG response + - Timing: Sent during periodic health checks + +3. **PONG Signal**: + - Format: `{"action":"pong"}` + - Purpose: Responds to PING signal and reports system status + - Timing: Sent when PING signal is received + +4. **Verify Signal**: + - Format: `{"action":"verify","id":"connection_id","fp":"tls_fingerprint"}` + - Purpose: Verifies TLS certificate fingerprint + - Timing: Sent after establishing TLS connection + +5. **TCP Launch Signal**: + - Format: `{"action":"tcp","remote":"remote_addr","id":"connection_id"}` - Purpose: Requests the client to establish a TCP connection for a specific ID - Timing: Sent when a new TCP connection to the target service is received -3. **UDP Launch Signal**: - - Format: `//#2` +6. **UDP Launch Signal**: + - Format: `{"action":"udp","remote":"remote_addr","id":"connection_id"}` - Purpose: Requests the client to handle UDP traffic for a specific ID - Timing: Sent when UDP data is received on the target port ### Signal Flow 1. **Signal Generation**: - - Server creates URL-formatted signals for specific events + - Server creates JSON-formatted signals for specific events - Signal is terminated with a newline character for proper parsing 2. **Signal Transmission**: @@ -192,7 +207,7 @@ NodePass uses a sophisticated URL-based signaling protocol through the TCP tunne 3. **Signal Reception**: - Client uses a buffered reader to read signals from the tunnel - - Signals are trimmed and parsed into URL format + - Signals are parsed into JSON format 4. **Signal Processing**: - Client places valid signals in a buffered channel (signalChan) @@ -200,8 +215,8 @@ NodePass uses a sophisticated URL-based signaling protocol through the TCP tunne - Semaphore pattern prevents signal overflow 5. **Signal Execution**: - - Remote signals update the client's remote address configuration - - Launch signals trigger the `clientOnce()` method to establish connections + - Dispatches to appropriate handling logic based on the `action` field + - Connection launch signals trigger respective methods to establish connections ### Signal Resilience - Buffered channel with configurable capacity prevents signal loss during high load diff --git a/nodepass/docs/zh/how-it-works.md b/nodepass/docs/zh/how-it-works.md index 955ad8ffa4..dd9d7bfde8 100644 --- a/nodepass/docs/zh/how-it-works.md +++ b/nodepass/docs/zh/how-it-works.md @@ -73,8 +73,8 @@ NodePass 通过其隧道架构建立双向数据流,支持 TCP 和 UDP 协议 ``` [服务端] → [生成唯一连接 ID] → [通过未加密的 TCP 隧道向客户端发送信号] ``` - - 对于 TCP:生成 `//#1` 信号 - - 对于 UDP:生成 `//#2` 信号 + - 对于 TCP:生成 `{"action":"tcp","remote":"target_addr","id":"connection_id"}` 信号 + - 对于 UDP:生成 `{"action":"udp","remote":"client_addr","id":"connection_id"}` 信号 3. **连接准备**: ``` @@ -95,7 +95,7 @@ NodePass 通过其隧道架构建立双向数据流,支持 TCP 和 UDP 协议 ``` [客户端] → [从 TCP 隧道读取信号] → [解析连接 ID] ``` - - 客户端根据 URL 方案区分 TCP 和 UDP 信号 + - 客户端根据 `action` 字段区分不同类型的信号 2. **连接建立**: ``` @@ -163,27 +163,42 @@ NodePass 通过其隧道架构建立双向数据流,支持 TCP 和 UDP 协议 ## 信号通信机制 -NodePass 通过 TCP 隧道使用复杂的基于 URL 的信号协议: +NodePass 通过 TCP 隧道使用基于 JSON 的信号协议: ### 信号类型 -1. **隧道信号**: - - 格式:`#` - - 目的:通知客户端 TLS 代号 - - 时机:在隧道握手时发送 +1. **刷新信号**: + - 格式:`{"action":"flush"}` + - 目的:刷新连接池并重置错误计数 + - 时机:当连接池健康检查失败时发送 -2. **TCP 启动信号**: - - 格式:`//#1` +2. **PING 信号**: + - 格式:`{"action":"ping"}` + - 目的:检查客户端连接状态并请求 PONG 响应 + - 时机:定期健康检查时发送 + +3. **PONG 信号**: + - 格式:`{"action":"pong"}` + - 目的:响应 PING 信号并报告系统状态 + - 时机:收到 PING 信号时发送 + +4. **验证信号**: + - 格式:`{"action":"verify","id":"connection_id","fp":"tls_fingerprint"}` + - 目的:验证 TLS 证书指纹 + - 时机:建立 TLS 连接后发送 + +5. **TCP 启动信号**: + - 格式:`{"action":"tcp","remote":"remote_addr","id":"connection_id"}` - 目的:请求客户端为特定 ID 建立 TCP 连接 - 时机:当接收到目标服务的新 TCP 连接时发送 -3. **UDP 启动信号**: - - 格式:`//#2` +6. **UDP 启动信号**: + - 格式:`{"action":"udp","remote":"remote_addr","id":"connection_id"}` - 目的:请求客户端处理特定 ID 的 UDP 流量 - 时机:当在目标端口接收到 UDP 数据时发送 ### 信号流程 1. **信号生成**: - - 服务端为特定事件创建 URL 格式的信号 + - 服务端为特定事件创建 JSON 格式的信号 - 信号以换行符终止,以便正确解析 2. **信号传输**: @@ -192,7 +207,7 @@ NodePass 通过 TCP 隧道使用复杂的基于 URL 的信号协议: 3. **信号接收**: - 客户端使用缓冲读取器从隧道读取信号 - - 信号被修剪并解析为 URL 格式 + - 信号被解析为 JSON 格式 4. **信号处理**: - 客户端将有效信号放入缓冲通道 (signalChan) @@ -200,8 +215,8 @@ NodePass 通过 TCP 隧道使用复杂的基于 URL 的信号协议: - 信号量模式防止信号溢出 5. **信号执行**: - - 远程信号更新客户端的远程地址配置 - - 启动信号触发 `clientOnce()` 方法建立连接 + - 根据 `action` 字段分派到相应的处理逻辑 + - 连接启动信号触发相应的处理方法建立连接 ### 信号弹性 - 具有可配置容量的缓冲通道防止在高负载下信号丢失 diff --git a/nodepass/go.mod b/nodepass/go.mod index 9ac1b1e5e8..8e3d4bf9fd 100644 --- a/nodepass/go.mod +++ b/nodepass/go.mod @@ -7,14 +7,14 @@ require ( github.com/NodePassProject/conn v1.0.16 github.com/NodePassProject/logs v1.0.3 github.com/NodePassProject/nph2 v1.0.4 - github.com/NodePassProject/npws v1.0.5 + github.com/NodePassProject/npws v1.0.6 github.com/NodePassProject/pool v1.0.50 github.com/NodePassProject/quic v1.0.14 ) require ( github.com/coder/websocket v1.8.14 // indirect - github.com/quic-go/quic-go v0.57.1 // indirect + github.com/quic-go/quic-go v0.58.0 // indirect golang.org/x/crypto v0.46.0 // indirect golang.org/x/net v0.48.0 // indirect golang.org/x/sys v0.39.0 // indirect diff --git a/nodepass/go.sum b/nodepass/go.sum index c201918163..f53853416a 100644 --- a/nodepass/go.sum +++ b/nodepass/go.sum @@ -6,8 +6,8 @@ github.com/NodePassProject/logs v1.0.3 h1:CDUZVQ477vmmFQHazrQCWM0gJPNINm0C2N3FzC github.com/NodePassProject/logs v1.0.3/go.mod h1:TwtPXOzLtb8iH+fdduQjEEywICXivsM39cy9AinMSks= github.com/NodePassProject/nph2 v1.0.4 h1:szvp7hlvMRt1g/g1hF++0DfgM++5yqWWAPlfMDh226c= github.com/NodePassProject/nph2 v1.0.4/go.mod h1:fzNbTk0zh+0gXer2aKpPXF69o4/BOd15Ys2gvicwWR0= -github.com/NodePassProject/npws v1.0.5 h1:UpVm4UYytNQHq6Nkli4Qqq17wpuwvKqoCY571+vubBA= -github.com/NodePassProject/npws v1.0.5/go.mod h1:cyVS2X3/8f07yOkVEl53D+ozHLljJvHQx/s6xoLl/s8= +github.com/NodePassProject/npws v1.0.6 h1:LZ+ZqqZoZ7OnmdtEps/WHuq2S7Qda4nPtr+15tm0024= +github.com/NodePassProject/npws v1.0.6/go.mod h1:cyVS2X3/8f07yOkVEl53D+ozHLljJvHQx/s6xoLl/s8= github.com/NodePassProject/pool v1.0.50 h1:Xmvb0hSHq24U06moHICr8CkqEHB4ZHBbY9Z+a7fSejQ= github.com/NodePassProject/pool v1.0.50/go.mod h1:joQFk1oocg56QpJ1QK/2g5Jv/AyqYUQgPXMG1gWe8iA= github.com/NodePassProject/quic v1.0.14 h1:zx5/cmQ8Tp1ntwCVAmtosODfBWTXTHHuprZdAspSjc8= @@ -18,8 +18,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10= -github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= +github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug= +github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= @@ -32,7 +32,5 @@ golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= -golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= -golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/nodepass/internal/client.go b/nodepass/internal/client.go index a30e4acb11..2076a2d130 100644 --- a/nodepass/internal/client.go +++ b/nodepass/internal/client.go @@ -31,7 +31,8 @@ func NewClient(parsedURL *url.URL, logger *logs.Logger) (*Client, error) { Common: Common{ parsedURL: parsedURL, logger: logger, - signalChan: make(chan string, semaphoreLimit), + signalChan: make(chan Signal, semaphoreLimit), + writeChan: make(chan []byte, semaphoreLimit), tcpBufferPool: &sync.Pool{ New: func() any { buf := make([]byte, tcpDataBufSize) @@ -44,9 +45,6 @@ func NewClient(parsedURL *url.URL, logger *logs.Logger) (*Client, error) { return &buf }, }, - flushURL: &url.URL{Scheme: "np", Fragment: "f"}, - pingURL: &url.URL{Scheme: "np", Fragment: "i"}, - pongURL: &url.URL{Scheme: "np", Fragment: "o"}, }, } if err := client.initConfig(); err != nil { @@ -284,6 +282,9 @@ func (c *Client) tunnelHandshake() error { c.maxPoolCapacity = config.Max c.tlsCode = config.TLS c.poolType = config.Type + if c.tlsCode == "1" || c.tlsCode == "2" { + c.verifyChan = make(chan struct{}) + } c.logger.Info("Loading tunnel config: FLOW=%v|MAX=%v|TLS=%v|TYPE=%v", c.dataFlow, c.maxPoolCapacity, c.tlsCode, c.poolType) diff --git a/nodepass/internal/common.go b/nodepass/internal/common.go index d6f532cb5c..f13b881bd3 100644 --- a/nodepass/internal/common.go +++ b/nodepass/internal/common.go @@ -10,6 +10,7 @@ import ( "crypto/tls" "encoding/base64" "encoding/hex" + "encoding/json" "fmt" "hash/fnv" "io" @@ -28,7 +29,6 @@ import ( // Common 包含所有模式共享的核心功能 type Common struct { - mu sync.Mutex // 互斥锁 parsedURL *url.URL // 解析后的URL logger *logs.Logger // 日志记录器 dnsCacheTTL time.Duration // DNS缓存TTL @@ -74,12 +74,11 @@ type Common struct { bufReader *bufio.Reader // 缓冲读取器 tcpBufferPool *sync.Pool // TCP缓冲区池 udpBufferPool *sync.Pool // UDP缓冲区池 - signalChan chan string // 信号通道 + signalChan chan Signal // 信号通道 + writeChan chan []byte // 写入通道 + verifyChan chan struct{} // 证书验证通道 handshakeStart time.Time // 握手开始时间 checkPoint time.Time // 检查点时间 - flushURL *url.URL // 重置信号 - pingURL *url.URL // PING信号 - pongURL *url.URL // PONG信号 slotLimit int32 // 槽位限制 tcpSlot int32 // TCP连接数 udpSlot int32 // UDP连接数 @@ -124,6 +123,14 @@ type TransportPool interface { ResetError() } +// Signal 操作信号结构体 +type Signal struct { + ActionType string `json:"action"` // 操作类型 + RemoteAddr string `json:"remote,omitempty"` // 远程地址 + PoolConnID string `json:"id,omitempty"` // 池连接ID + Fingerprint string `json:"fp,omitempty"` // TLS指纹 +} + // 配置变量,可通过环境变量调整 var ( semaphoreLimit = getEnvAsInt("NP_SEMAPHORE_LIMIT", 65536) // 信号量限制 @@ -956,6 +963,8 @@ func (c *Common) stop() { // 清空通道 drain(c.signalChan) + drain(c.writeChan) + drain(c.verifyChan) // 重置全局限速器 if c.rateLimiter != nil { @@ -1006,6 +1015,24 @@ func (c *Common) setControlConn() error { c.controlConn = poolConn c.bufReader = bufio.NewReader(&conn.TimeoutReader{Conn: c.controlConn, Timeout: 3 * reportInterval}) c.logger.Info("Marking tunnel handshake as complete in %vms", time.Since(c.handshakeStart).Milliseconds()) + + go func() { + for { + select { + case <-c.ctx.Done(): + return + case data := <-c.writeChan: + _, err := c.controlConn.Write(data) + if err != nil { + c.logger.Error("startWriter: write failed: %v", err) + } + } + } + }() + + if c.tlsCode == "1" || c.tlsCode == "2" { + c.logger.Info("TLS certificate fingerprint verifying...") + } return nil } @@ -1046,7 +1073,18 @@ func (c *Common) commonQueue() error { } continue } - signal := string(signalData) + + // 解析JSON信号 + var signal Signal + if err := json.Unmarshal(signalData, &signal); err != nil { + c.logger.Error("commonQueue: unmarshal signal failed: %v", err) + select { + case <-c.ctx.Done(): + return fmt.Errorf("commonQueue: context error: %w", c.ctx.Err()) + case <-time.After(contextCheckInterval): + } + continue + } // 将信号发送到通道 select { @@ -1080,20 +1118,12 @@ func (c *Common) healthCheck() error { } for c.ctx.Err() == nil { - // 尝试获取锁 - if !c.mu.TryLock() { - continue - } - // 连接池健康度检查 if c.tunnelPool.ErrorCount() > c.tunnelPool.Active()/2 { // 发送刷新信号到对端 if c.ctx.Err() == nil && c.controlConn != nil { - _, err := c.controlConn.Write(c.encode([]byte(c.flushURL.String()))) - if err != nil { - c.mu.Unlock() - return fmt.Errorf("healthCheck: write flush signal failed: %w", err) - } + signalData, _ := json.Marshal(Signal{ActionType: "flush"}) + c.writeChan <- c.encode(signalData) } c.tunnelPool.Flush() c.tunnelPool.ResetError() @@ -1110,14 +1140,9 @@ func (c *Common) healthCheck() error { // 发送PING信号 c.checkPoint = time.Now() if c.ctx.Err() == nil && c.controlConn != nil { - _, err := c.controlConn.Write(c.encode([]byte(c.pingURL.String()))) - if err != nil { - c.mu.Unlock() - return fmt.Errorf("healthCheck: write ping signal failed: %w", err) - } + signalData, _ := json.Marshal(Signal{ActionType: "ping"}) + c.writeChan <- c.encode(signalData) } - - c.mu.Unlock() select { case <-c.ctx.Done(): return fmt.Errorf("healthCheck: context error: %w", c.ctx.Err()) @@ -1131,7 +1156,7 @@ func (c *Common) healthCheck() error { // incomingVerify 入口连接验证 func (c *Common) incomingVerify() { for c.ctx.Err() == nil { - if c.tunnelPool.Ready() { + if c.tunnelPool.Ready() && c.tunnelPool.Active() > 0 { break } select { @@ -1141,39 +1166,41 @@ func (c *Common) incomingVerify() { } } - if c.tlsConfig == nil || len(c.tlsConfig.Certificates) == 0 { - return - } - - cert := c.tlsConfig.Certificates[0] - if len(cert.Certificate) == 0 { - return - } - - // 打印证书指纹 - c.logger.Info("TLS cert verified: %v", c.formatCertFingerprint(cert.Certificate[0])) - id, testConn, err := c.tunnelPool.IncomingGet(poolGetTimeout) if err != nil { + c.logger.Error("incomingVerify: incomingGet failed: %v", err) + c.cancel() return } defer testConn.Close() - // 构建并发送验证信号 - verifyURL := &url.URL{ - Scheme: "np", - Host: c.controlConn.RemoteAddr().String(), - Path: url.PathEscape(id), - Fragment: "v", // TLS验证 + // 获取证书指纹 + var fingerprint string + switch c.coreType { + case "server": + if c.tlsConfig != nil && len(c.tlsConfig.Certificates) > 0 { + cert := c.tlsConfig.Certificates[0] + if len(cert.Certificate) > 0 { + fingerprint = c.formatCertFingerprint(cert.Certificate[0]) + } + } + case "client": + if conn, ok := testConn.(interface{ ConnectionState() tls.ConnectionState }); ok { + state := conn.ConnectionState() + if len(state.PeerCertificates) > 0 { + fingerprint = c.formatCertFingerprint(state.PeerCertificates[0].Raw) + } + } } + // 构建并发送验证信号 if c.ctx.Err() == nil && c.controlConn != nil { - c.mu.Lock() - _, err = c.controlConn.Write(c.encode([]byte(verifyURL.String()))) - c.mu.Unlock() - if err != nil { - return - } + signalData, _ := json.Marshal(Signal{ + ActionType: "verify", + PoolConnID: id, + Fingerprint: fingerprint, + }) + c.writeChan <- c.encode(signalData) } c.logger.Debug("TLS verify signal: cid %v -> %v", id, c.controlConn.RemoteAddr()) @@ -1184,6 +1211,15 @@ func (c *Common) commonLoop() { for c.ctx.Err() == nil { // 等待连接池准备就绪 if c.tunnelPool.Ready() { + if c.verifyChan != nil { + select { + case <-c.verifyChan: + // 证书验证完成 + case <-c.ctx.Done(): + return + } + } + if c.targetListener != nil || c.disableTCP != "1" { go c.commonTCPLoop() } @@ -1266,22 +1302,13 @@ func (c *Common) commonTCPLoop() { c.logger.Debug("Tunnel connection: %v <-> %v", remoteConn.LocalAddr(), remoteConn.RemoteAddr()) // 构建并发送启动信号 - launchURL := &url.URL{ - Scheme: "np", - Host: targetConn.RemoteAddr().String(), - Path: url.PathEscape(id), - Fragment: "1", // TCP模式 - } - if c.ctx.Err() == nil && c.controlConn != nil { - c.mu.Lock() - _, err = c.controlConn.Write(c.encode([]byte(launchURL.String()))) - c.mu.Unlock() - - if err != nil { - c.logger.Error("commonTCPLoop: write launch signal failed: %v", err) - return - } + signalData, _ := json.Marshal(Signal{ + ActionType: "tcp", + RemoteAddr: targetConn.RemoteAddr().String(), + PoolConnID: id, + }) + c.writeChan <- c.encode(signalData) } c.logger.Debug("TCP launch signal: cid %v -> %v", id, c.controlConn.RemoteAddr()) @@ -1397,21 +1424,13 @@ func (c *Common) commonUDPLoop() { }(remoteConn, clientAddr, sessionKey, id) // 构建并发送启动信号 - launchURL := &url.URL{ - Scheme: "np", - Host: clientAddr.String(), - Path: url.PathEscape(id), - Fragment: "2", // UDP模式 - } - if c.ctx.Err() == nil && c.controlConn != nil { - c.mu.Lock() - _, err = c.controlConn.Write(c.encode([]byte(launchURL.String()))) - c.mu.Unlock() - if err != nil { - c.logger.Error("commonUDPLoop: write launch signal failed: %v", err) - continue - } + signalData, _ := json.Marshal(Signal{ + ActionType: "udp", + RemoteAddr: clientAddr.String(), + PoolConnID: id, + }) + c.writeChan <- c.encode(signalData) } c.logger.Debug("UDP launch signal: cid %v -> %v", id, c.controlConn.RemoteAddr()) @@ -1453,33 +1472,21 @@ func (c *Common) commonOnce() error { case <-c.ctx.Done(): return fmt.Errorf("commonOnce: context error: %w", c.ctx.Err()) case signal := <-c.signalChan: - // 解析信号URL - signalURL, err := url.Parse(signal) - if err != nil { - c.logger.Error("commonOnce: parse signal failed: %v", err) - select { - case <-c.ctx.Done(): - return fmt.Errorf("commonOnce: context error: %w", c.ctx.Err()) - case <-time.After(contextCheckInterval): - } - continue - } - // 处理信号 - switch signalURL.Fragment { - case "v": // 验证 + switch signal.ActionType { + case "verify": if c.tlsCode == "1" || c.tlsCode == "2" { - go c.outgoingVerify(signalURL) + go c.outgoingVerify(signal) } - case "1": // TCP + case "tcp": if c.disableTCP != "1" { - go c.commonTCPOnce(signalURL) + go c.commonTCPOnce(signal) } - case "2": // UDP + case "udp": if c.disableUDP != "1" { - go c.commonUDPOnce(signalURL) + go c.commonUDPOnce(signal) } - case "f": // 连接池刷新 + case "flush": go func() { c.tunnelPool.Flush() c.tunnelPool.ResetError() @@ -1492,16 +1499,12 @@ func (c *Common) commonOnce() error { c.logger.Debug("Tunnel pool flushed: %v active connections", c.tunnelPool.Active()) }() - case "i": // PING + case "ping": if c.ctx.Err() == nil && c.controlConn != nil { - c.mu.Lock() - _, err := c.controlConn.Write(c.encode([]byte(c.pongURL.String()))) - c.mu.Unlock() - if err != nil { - return fmt.Errorf("commonOnce: write pong signal failed: %w", err) - } + signalData, _ := json.Marshal(Signal{ActionType: "pong"}) + c.writeChan <- c.encode(signalData) } - case "o": // PONG + case "pong": // 发送检查点事件 c.logger.Event("CHECK_POINT|MODE=%v|PING=%vms|POOL=%v|TCPS=%v|UDPS=%v|TCPRX=%v|TCPTX=%v|UDPRX=%v|UDPTX=%v", c.runMode, time.Since(c.checkPoint).Milliseconds(), c.tunnelPool.Active(), @@ -1518,7 +1521,7 @@ func (c *Common) commonOnce() error { } // outgoingVerify 出口连接验证 -func (c *Common) outgoingVerify(signalURL *url.URL) { +func (c *Common) outgoingVerify(signal Signal) { for c.ctx.Err() == nil { if c.tunnelPool.Ready() { break @@ -1530,24 +1533,44 @@ func (c *Common) outgoingVerify(signalURL *url.URL) { } } - id := strings.TrimPrefix(signalURL.Path, "/") - if unescapedID, err := url.PathUnescape(id); err != nil { - c.logger.Error("outgoingVerify: unescape id failed: %v", err) + fingerPrint := signal.Fingerprint + if fingerPrint == "" { + c.logger.Error("outgoingVerify: no fingerprint in signal") + c.cancel() return - } else { - id = unescapedID } + + id := signal.PoolConnID c.logger.Debug("TLS verify signal: cid %v <- %v", id, c.controlConn.RemoteAddr()) testConn, err := c.tunnelPool.OutgoingGet(id, poolGetTimeout) if err != nil { c.logger.Error("outgoingVerify: request timeout: %v", err) - c.tunnelPool.AddError() + c.cancel() return } defer testConn.Close() - if testConn != nil { + // 验证证书指纹 + var serverFingerprint, clientFingerprint string + switch c.coreType { + case "server": + if c.tlsConfig == nil || len(c.tlsConfig.Certificates) == 0 { + c.logger.Error("outgoingVerify: no local certificate") + c.cancel() + return + } + + cert := c.tlsConfig.Certificates[0] + if len(cert.Certificate) == 0 { + c.logger.Error("outgoingVerify: empty local certificate") + c.cancel() + return + } + + serverFingerprint = c.formatCertFingerprint(cert.Certificate[0]) + clientFingerprint = fingerPrint + case "client": conn, ok := testConn.(interface{ ConnectionState() tls.ConnectionState }) if !ok { return @@ -1556,23 +1579,32 @@ func (c *Common) outgoingVerify(signalURL *url.URL) { if len(state.PeerCertificates) == 0 { c.logger.Error("outgoingVerify: no peer certificates found") + c.cancel() return } - // 打印证书指纹 - c.logger.Info("TLS cert verified: %v", c.formatCertFingerprint(state.PeerCertificates[0].Raw)) + clientFingerprint = c.formatCertFingerprint(state.PeerCertificates[0].Raw) + serverFingerprint = fingerPrint + } + + // 验证指纹匹配 + if serverFingerprint != clientFingerprint { + c.logger.Error("outgoingVerify: certificate fingerprint mismatch: server: %v - client: %v", serverFingerprint, clientFingerprint) + c.cancel() + return + } + + c.logger.Info("TLS certificate fingerprint verified: %v", fingerPrint) + + // 通知验证完成 + if c.verifyChan != nil { + c.verifyChan <- struct{}{} } } // commonTCPOnce 共用处理单个TCP请求 -func (c *Common) commonTCPOnce(signalURL *url.URL) { - id := strings.TrimPrefix(signalURL.Path, "/") - if unescapedID, err := url.PathUnescape(id); err != nil { - c.logger.Error("commonTCPOnce: unescape id failed: %v", err) - return - } else { - id = unescapedID - } +func (c *Common) commonTCPOnce(signal Signal) { + id := signal.PoolConnID c.logger.Debug("TCP launch signal: cid %v <- %v", id, c.controlConn.RemoteAddr()) // 从连接池获取连接 @@ -1620,7 +1652,7 @@ func (c *Common) commonTCPOnce(signalURL *url.URL) { c.logger.Debug("Target connection: %v <-> %v", targetConn.LocalAddr(), targetConn.RemoteAddr()) // 发送PROXY v1 - if err := c.sendProxyV1Header(signalURL.Host, targetConn); err != nil { + if err := c.sendProxyV1Header(signal.RemoteAddr, targetConn); err != nil { c.logger.Error("commonTCPOnce: sendProxyV1Header failed: %v", err) return } @@ -1638,14 +1670,9 @@ func (c *Common) commonTCPOnce(signalURL *url.URL) { } // commonUDPOnce 共用处理单个UDP请求 -func (c *Common) commonUDPOnce(signalURL *url.URL) { - id := strings.TrimPrefix(signalURL.Path, "/") - if unescapedID, err := url.PathUnescape(id); err != nil { - c.logger.Error("commonUDPOnce: unescape id failed: %v", err) - return - } else { - id = unescapedID - } +// commonUDPOnce 共用处理单个UDP请求 +func (c *Common) commonUDPOnce(signal Signal) { + id := signal.PoolConnID c.logger.Debug("UDP launch signal: cid %v <- %v", id, c.controlConn.RemoteAddr()) // 获取池连接 @@ -1668,7 +1695,7 @@ func (c *Common) commonUDPOnce(signalURL *url.URL) { }() var targetConn net.Conn - sessionKey := signalURL.Host + sessionKey := signal.RemoteAddr isNewSession := false // 获取或创建目标UDP会话 diff --git a/nodepass/internal/server.go b/nodepass/internal/server.go index 6d9bf25146..d05b1568d2 100644 --- a/nodepass/internal/server.go +++ b/nodepass/internal/server.go @@ -35,7 +35,8 @@ func NewServer(parsedURL *url.URL, tlsCode string, tlsConfig *tls.Config, logger tlsCode: tlsCode, tlsConfig: tlsConfig, logger: logger, - signalChan: make(chan string, semaphoreLimit), + signalChan: make(chan Signal, semaphoreLimit), + writeChan: make(chan []byte, semaphoreLimit), tcpBufferPool: &sync.Pool{ New: func() any { buf := make([]byte, tcpDataBufSize) @@ -48,9 +49,6 @@ func NewServer(parsedURL *url.URL, tlsCode string, tlsConfig *tls.Config, logger return &buf }, }, - flushURL: &url.URL{Scheme: "np", Fragment: "f"}, - pingURL: &url.URL{Scheme: "np", Fragment: "i"}, - pongURL: &url.URL{Scheme: "np", Fragment: "o"}, }, } if err := server.initConfig(); err != nil { @@ -215,6 +213,10 @@ func (s *Server) initTunnelPool() error { // tunnelHandshake 与客户端进行HTTP握手 func (s *Server) tunnelHandshake() error { + if s.tlsCode == "1" || s.tlsCode == "2" { + s.verifyChan = make(chan struct{}) + } + var clientIP string done := make(chan struct{}) diff --git a/openwrt-packages/luci-app-lucky/lucky/Makefile b/openwrt-packages/luci-app-lucky/lucky/Makefile index e76b090d04..c0178f3ada 100644 --- a/openwrt-packages/luci-app-lucky/lucky/Makefile +++ b/openwrt-packages/luci-app-lucky/lucky/Makefile @@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=lucky -PKG_VERSION:=2.20.2 +PKG_VERSION:=2.24.0 PKG_RELEASE:=1 PKGARCH:=all diff --git a/sing-box/common/sniff/quic_capture_test.go b/sing-box/common/sniff/quic_capture_test.go index a6dbb6aa79..4c9eb838a9 100644 --- a/sing-box/common/sniff/quic_capture_test.go +++ b/sing-box/common/sniff/quic_capture_test.go @@ -17,6 +17,7 @@ import ( ) func TestSniffQUICQuicGoFingerprint(t *testing.T) { + t.Parallel() const testSNI = "test.example.com" udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0}) diff --git a/sing-box/docs/changelog.md b/sing-box/docs/changelog.md index 2a88866169..464ae6985e 100644 --- a/sing-box/docs/changelog.md +++ b/sing-box/docs/changelog.md @@ -2,6 +2,11 @@ icon: material/alert-decagram --- +#### 1.13.0-alpha.36 + +* Downgrade quic-go to v0.57.1 +* Fixes and improvements + #### 1.13.0-alpha.35 * Add pre-match support for `auto_redirect` **1** diff --git a/sing-box/docs/configuration/inbound/shadowsocks.md b/sing-box/docs/configuration/inbound/shadowsocks.md index 4072782bc4..e511585756 100644 --- a/sing-box/docs/configuration/inbound/shadowsocks.md +++ b/sing-box/docs/configuration/inbound/shadowsocks.md @@ -9,6 +9,7 @@ "method": "2022-blake3-aes-128-gcm", "password": "8JCsPssfgS8tiRwiMlhARg==", + "managed": false, "multiplex": {} } ``` @@ -86,6 +87,10 @@ Both if empty. | 2022 methods | `sing-box generate rand --base64 ` | | other methods | any string | +#### managed + +Defaults to `false`. Enable this when the inbound is managed by the [SSM API](/configuration/service/ssm-api) for dynamic user. + #### multiplex See [Multiplex](/configuration/shared/multiplex#inbound) for details. diff --git a/sing-box/docs/configuration/inbound/shadowsocks.zh.md b/sing-box/docs/configuration/inbound/shadowsocks.zh.md index d48bc11963..c97e9bef5e 100644 --- a/sing-box/docs/configuration/inbound/shadowsocks.zh.md +++ b/sing-box/docs/configuration/inbound/shadowsocks.zh.md @@ -9,6 +9,7 @@ "method": "2022-blake3-aes-128-gcm", "password": "8JCsPssfgS8tiRwiMlhARg==", + "managed": false, "multiplex": {} } ``` @@ -86,6 +87,10 @@ | 2022 methods | `sing-box generate rand --base64 <密钥长度>` | | other methods | 任意字符串 | +#### managed + +默认为 `false`。当该入站需要由 [SSM API](/zh/configuration/service/ssm-api) 管理用户时必须启用此字段。 + #### multiplex 参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。 diff --git a/sing-box/go.mod b/sing-box/go.mod index 31a56706f5..f904a7c5b4 100644 --- a/sing-box/go.mod +++ b/sing-box/go.mod @@ -34,7 +34,7 @@ require ( github.com/sagernet/quic-go v0.58.0-sing-box-mod.1 github.com/sagernet/sing v0.8.0-beta.8 github.com/sagernet/sing-mux v0.3.3 - github.com/sagernet/sing-quic v0.6.0-beta.7 + github.com/sagernet/sing-quic v0.6.0-beta.8 github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks2 v0.2.1 github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 diff --git a/sing-box/go.sum b/sing-box/go.sum index c9346e8722..ce06c4552a 100644 --- a/sing-box/go.sum +++ b/sing-box/go.sum @@ -210,8 +210,8 @@ github.com/sagernet/sing v0.8.0-beta.8 h1:hUo0wZ2HGTieV1flEIai96HFhF34mMHVnduRqJ github.com/sagernet/sing v0.8.0-beta.8/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing-mux v0.3.3 h1:YFgt9plMWzH994BMZLmyKL37PdIVaIilwP0Jg+EcLfw= github.com/sagernet/sing-mux v0.3.3/go.mod h1:pht8iFY4c9Xltj7rhVd208npkNaeCxzyXCgulDPLUDA= -github.com/sagernet/sing-quic v0.6.0-beta.7 h1:Sh6KltQ6nB69S9ZdDKs5oARqkyY99gOaHe1JPxNV7ag= -github.com/sagernet/sing-quic v0.6.0-beta.7/go.mod h1:0NodMFjlAvfLp87Enpx46fQPrGRvmbUsmy5hRLzomtM= +github.com/sagernet/sing-quic v0.6.0-beta.8 h1:Y0P8WTqWpfg80rLFsDfF22QumM+HEAjRQ2o+8Dv+vDs= +github.com/sagernet/sing-quic v0.6.0-beta.8/go.mod h1:Y3YVjPutLHLQvYjGtUFH+w8YmM4MTd19NDzxLZGNGIs= github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE= github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI= github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo= diff --git a/sing-box/option/options.go b/sing-box/option/options.go index c964998b23..8bebd48fc6 100644 --- a/sing-box/option/options.go +++ b/sing-box/option/options.go @@ -5,6 +5,7 @@ import ( "context" E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/json" ) @@ -60,37 +61,40 @@ func checkOptions(options *Options) error { func checkInbounds(inbounds []Inbound) error { seen := make(map[string]bool) - for _, inbound := range inbounds { - if inbound.Tag == "" { - continue + for i, inbound := range inbounds { + tag := inbound.Tag + if tag == "" { + tag = F.ToString(i) } - if seen[inbound.Tag] { - return E.New("duplicate inbound tag: ", inbound.Tag) + if seen[tag] { + return E.New("duplicate inbound tag: ", tag) } - seen[inbound.Tag] = true + seen[tag] = true } return nil } func checkOutbounds(outbounds []Outbound, endpoints []Endpoint) error { seen := make(map[string]bool) - for _, outbound := range outbounds { - if outbound.Tag == "" { - continue + for i, outbound := range outbounds { + tag := outbound.Tag + if tag == "" { + tag = F.ToString(i) } - if seen[outbound.Tag] { - return E.New("duplicate outbound/endpoint tag: ", outbound.Tag) + if seen[tag] { + return E.New("duplicate outbound/endpoint tag: ", tag) } - seen[outbound.Tag] = true + seen[tag] = true } - for _, endpoint := range endpoints { - if endpoint.Tag == "" { - continue + for i, endpoint := range endpoints { + tag := endpoint.Tag + if tag == "" { + tag = F.ToString(i) } - if seen[endpoint.Tag] { - return E.New("duplicate outbound/endpoint tag: ", endpoint.Tag) + if seen[tag] { + return E.New("duplicate outbound/endpoint tag: ", tag) } - seen[endpoint.Tag] = true + seen[tag] = true } return nil } diff --git a/small/v2ray-geodata/Makefile b/small/v2ray-geodata/Makefile index c171103a14..1470d53042 100644 --- a/small/v2ray-geodata/Makefile +++ b/small/v2ray-geodata/Makefile @@ -21,13 +21,13 @@ define Download/geoip HASH:=6878dbacfb1fcb1ee022f63ed6934bcefc95a3c4ba10c88f1131fb88dbf7c337 endef -GEOSITE_VER:=20260101111132 +GEOSITE_VER:=20260102111739 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:=f8ba540481e5aab5b5f18224331b5981296def00cdb61481aed85c9fca078a04 + HASH:=a535bde603510e96f2623ba081865a63ea2aae6d23c14c3a4430805bcd300f4f endef GEOSITE_IRAN_VER:=202512290048 diff --git a/v2ray-core/go.mod b/v2ray-core/go.mod index 138b7816f1..9ff1d55f53 100644 --- a/v2ray-core/go.mod +++ b/v2ray-core/go.mod @@ -9,7 +9,7 @@ require ( github.com/apernet/quic-go v0.48.2-0.20241104191913-cb103fcecfe7 github.com/go-chi/chi/v5 v5.2.3 github.com/go-chi/render v1.0.3 - github.com/go-playground/validator/v10 v10.28.0 + github.com/go-playground/validator/v10 v10.30.1 github.com/golang-collections/go-datastructures v0.0.0-20150211160725-59788d5eb259 github.com/golang/mock v1.6.0 github.com/golang/protobuf v1.5.4 @@ -41,7 +41,7 @@ require ( golang.org/x/net v0.48.0 golang.org/x/sync v0.19.0 golang.org/x/sys v0.39.0 - google.golang.org/grpc v1.77.0 + google.golang.org/grpc v1.78.0 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 gvisor.dev/gvisor v0.0.0-20231020174304-b8a429915ff1 @@ -60,7 +60,7 @@ require ( github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect github.com/ebfe/bcrypt_pbkdf v0.0.0-20140212075826-3c8d2dcb253a // indirect - github.com/gabriel-vasile/mimetype v1.4.10 // indirect + github.com/gabriel-vasile/mimetype v1.4.12 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect @@ -91,6 +91,6 @@ require ( golang.org/x/text v0.32.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.39.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect nhooyr.io/websocket v1.8.6 // indirect ) diff --git a/v2ray-core/go.sum b/v2ray-core/go.sum index f5a435f3bc..b05fdc62e0 100644 --- a/v2ray-core/go.sum +++ b/v2ray-core/go.sum @@ -107,8 +107,8 @@ github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVB github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= -github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= +github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= @@ -140,8 +140,8 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= -github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= -github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= @@ -807,8 +807,8 @@ google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvx google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20210126160654-44e461bb6506/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= @@ -823,8 +823,8 @@ google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= -google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= +google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= +google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/v2ray-core/transport/internet/request/roundtripper/httprt/httprt.go b/v2ray-core/transport/internet/request/roundtripper/httprt/httprt.go index 3dacadcde9..4886e22317 100644 --- a/v2ray-core/transport/internet/request/roundtripper/httprt/httprt.go +++ b/v2ray-core/transport/internet/request/roundtripper/httprt/httprt.go @@ -77,6 +77,9 @@ func (h *httpTripperClient) RoundTrip(ctx context.Context, req request.Request, return resp, err } defer httpResp.Body.Close() + if httpResp.StatusCode != http.StatusOK { + newError("non-200 response: ", httpResp.Status).AtInfo().WriteToLog() + } if streamingWriter == nil { result, err := io.ReadAll(httpResp.Body) if err != nil { diff --git a/v2ray-core/transport/internet/tlsmirror/mirrorenrollment/roundtripperenrollmentconfirmation/client.go b/v2ray-core/transport/internet/tlsmirror/mirrorenrollment/roundtripperenrollmentconfirmation/client.go index 24fac65dc8..b5ffaebefe 100644 --- a/v2ray-core/transport/internet/tlsmirror/mirrorenrollment/roundtripperenrollmentconfirmation/client.go +++ b/v2ray-core/transport/internet/tlsmirror/mirrorenrollment/roundtripperenrollmentconfirmation/client.go @@ -90,7 +90,11 @@ func (c *Client) Dial(ctx context.Context) (net.Conn, error) { return nil, newError("failed to dial to destination").Base(err).AtError() } if c.config.SecurityConfig != nil { - securityEngine, err := common.CreateObject(c.ctx, c.config.SecurityConfig) + securityConfigSetting, err := serial.GetInstanceOf(c.config.SecurityConfig) + if err != nil { + return nil, newError("unable to get security config instance").Base(err) + } + securityEngine, err := common.CreateObject(c.ctx, securityConfigSetting) if err != nil { return nil, newError("unable to create security engine from security settings").Base(err) } diff --git a/v2ray-core/transport/internet/tlsmirror/mirrorenrollment/roundtripperenrollmentconfirmation/serverinverserole.go b/v2ray-core/transport/internet/tlsmirror/mirrorenrollment/roundtripperenrollmentconfirmation/serverinverserole.go index 9bef9d4663..d9b36a4694 100644 --- a/v2ray-core/transport/internet/tlsmirror/mirrorenrollment/roundtripperenrollmentconfirmation/serverinverserole.go +++ b/v2ray-core/transport/internet/tlsmirror/mirrorenrollment/roundtripperenrollmentconfirmation/serverinverserole.go @@ -99,7 +99,11 @@ func (c *ServerInverseRole) Dial(ctx context.Context) (net.Conn, error) { return nil, newError("failed to dial to destination").Base(err).AtError() } if c.config.SecurityConfig != nil { - securityEngine, err := common.CreateObject(ctx, c.config.SecurityConfig) + securityConfigSetting, err := serial.GetInstanceOf(c.config.SecurityConfig) + if err != nil { + return nil, newError("unable to get security config instance").Base(err) + } + securityEngine, err := common.CreateObject(c.ctx, securityConfigSetting) if err != nil { return nil, newError("unable to create security engine from security settings").Base(err) } diff --git a/yt-dlp/.github/workflows/release-nightly.yml b/yt-dlp/.github/workflows/release-nightly.yml index a6026a199a..9bbcc75a78 100644 --- a/yt-dlp/.github/workflows/release-nightly.yml +++ b/yt-dlp/.github/workflows/release-nightly.yml @@ -2,6 +2,7 @@ name: Release (nightly) on: schedule: - cron: '23 23 * * *' + workflow_dispatch: permissions: {} @@ -19,6 +20,22 @@ jobs: with: fetch-depth: 0 persist-credentials: false + + - name: Retrieve HEAD commit hash + id: head + shell: bash + run: echo "head=$(git rev-parse HEAD)" | tee -a "${GITHUB_OUTPUT}" + + - name: Cache nightly commit hash + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + env: + SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1 + with: + path: .nightly_commit_hash + key: release-nightly-${{ steps.head.outputs.head }} + restore-keys: | + release-nightly- + - name: Check for new commits id: check_for_new_commits shell: bash @@ -35,7 +52,22 @@ jobs: ".github/workflows/release.yml" ".github/workflows/release-nightly.yml" ) - echo "commit=$(git log --format=%H -1 --since="24 hours ago" -- "${relevant_files[@]}")" | tee "$GITHUB_OUTPUT" + if [[ -f .nightly_commit_hash ]]; then + limit_args=( + "$(cat .nightly_commit_hash)..HEAD" + ) + else + limit_args=( + --since="24 hours ago" + ) + fi + echo "commit=$(git log --format=%H -1 "${limit_args[@]}" -- "${relevant_files[@]}")" | tee -a "${GITHUB_OUTPUT}" + + - name: Record new nightly commit hash + env: + HEAD: ${{ steps.head.outputs.head }} + shell: bash + run: echo "${HEAD}" | tee .nightly_commit_hash release: name: Publish Github release @@ -66,6 +98,7 @@ jobs: with: path: dist name: build-pypi + - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 with: diff --git a/yt-dlp/.github/workflows/release.yml b/yt-dlp/.github/workflows/release.yml index 584ac13be9..1ef565723d 100644 --- a/yt-dlp/.github/workflows/release.yml +++ b/yt-dlp/.github/workflows/release.yml @@ -215,7 +215,7 @@ jobs: python -m build --no-isolation . - name: Upload artifacts - if: github.event_name != 'workflow_dispatch' + if: github.event.workflow != '.github/workflows/release.yml' # Reusable workflow_call uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: build-pypi @@ -224,7 +224,7 @@ jobs: compression-level: 0 - name: Publish to PyPI - if: github.event_name == 'workflow_dispatch' + if: github.event.workflow == '.github/workflows/release.yml' # Direct workflow_dispatch uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 with: verbose: true diff --git a/yt-dlp/yt_dlp/extractor/_extractors.py b/yt-dlp/yt_dlp/extractor/_extractors.py index fd30b161ab..0f8b65d34e 100644 --- a/yt-dlp/yt_dlp/extractor/_extractors.py +++ b/yt-dlp/yt_dlp/extractor/_extractors.py @@ -2007,6 +2007,11 @@ from .taptap import ( TapTapMomentIE, TapTapPostIntlIE, ) +from .tarangplus import ( + TarangPlusEpisodesIE, + TarangPlusPlaylistIE, + TarangPlusVideoIE, +) from .tass import TassIE from .tbs import TBSIE from .tbsjp import ( diff --git a/yt-dlp/yt_dlp/extractor/tarangplus.py b/yt-dlp/yt_dlp/extractor/tarangplus.py new file mode 100644 index 0000000000..6adb5fb90b --- /dev/null +++ b/yt-dlp/yt_dlp/extractor/tarangplus.py @@ -0,0 +1,224 @@ +import base64 +import binascii +import functools +import re + +from .common import InfoExtractor +from ..dependencies import Cryptodome +from ..utils import ( + ExtractorError, + OnDemandPagedList, + clean_html, + extract_attributes, + urljoin, +) +from ..utils.traversal import ( + find_element, + find_elements, + require, + traverse_obj, +) + + +class TarangPlusBaseIE(InfoExtractor): + _BASE_URL = 'https://tarangplus.in' + + +class TarangPlusVideoIE(TarangPlusBaseIE): + IE_NAME = 'tarangplus:video' + _VALID_URL = r'https?://(?:www\.)?tarangplus\.in/(?:movies|[^#?/]+/[^#?/]+)/(?!episodes)(?P[^#?/]+)' + _TESTS = [{ + 'url': 'https://tarangplus.in/tarangaplus-originals/khitpit/khitpit-ep-10', + 'md5': '78ce056cee755687b8a48199909ecf53', + 'info_dict': { + 'id': '67b8206719521d054c0059b7', + 'display_id': 'khitpit-ep-10', + 'ext': 'mp4', + 'title': 'Khitpit Ep-10', + 'description': 'md5:a45b805cb628e15c853d78b0406eab48', + 'thumbnail': r're:https?://.*\.jpg', + 'duration': 756.0, + 'timestamp': 1740355200, + 'upload_date': '20250224', + 'media_type': 'episode', + 'categories': ['Originals'], + }, + }, { + 'url': 'https://tarangplus.in/tarang-serials/bada-bohu/bada-bohu-ep-233', + 'md5': 'b4f9beb15172559bb362203b4f48382e', + 'info_dict': { + 'id': '680b9d6c19521d054c007782', + 'display_id': 'bada-bohu-ep-233', + 'ext': 'mp4', + 'title': 'Bada Bohu | Ep -233', + 'description': 'md5:e6b8e7edc9e60b92c1b390f8789ecd69', + 'thumbnail': r're:https?://.*\.jpg', + 'duration': 1392.0, + 'timestamp': 1745539200, + 'upload_date': '20250425', + 'media_type': 'episode', + 'categories': ['Prime'], + }, + }, { + 'url': 'https://tarangplus.in/short/ai-maa/ai-maa', + 'only_matching': True, + }, { + 'url': 'https://tarangplus.in/shows/tarang-cine-utsav-2024/tarang-cine-utsav-2024-seg-1', + 'only_matching': True, + }, { + 'url': 'https://tarangplus.in/music-videos/chori-chori-bohu-chori-songs/nijara-laguchu-dhire-dhire', + 'only_matching': True, + }, { + 'url': 'https://tarangplus.in/kids-shows/chhota-jaga/chhota-jaga-ep-33-jamidar-ra-khajana-adaya', + 'only_matching': True, + }, { + 'url': 'https://tarangplus.in/movies/swayambara', + 'only_matching': True, + }] + + def decrypt(self, data, key): + if not Cryptodome.AES: + raise ExtractorError('pycryptodomex not found. Please install', expected=True) + iv = binascii.unhexlify('00000000000000000000000000000000') + cipher = Cryptodome.AES.new(base64.b64decode(key), Cryptodome.AES.MODE_CBC, iv) + return cipher.decrypt(base64.b64decode(data)).decode('utf-8') + + def _real_extract(self, url): + display_id = self._match_id(url) + webpage = self._download_webpage(url, display_id) + hidden_inputs_data = self._hidden_inputs(webpage) + json_ld_data = self._search_json_ld(webpage, display_id) + json_ld_data.pop('url', None) + + iframe_url = traverse_obj(webpage, ( + {find_element(tag='iframe', attr='src', value=r'.+[?&]contenturl=.+', html=True, regex=True)}, + {extract_attributes}, 'src', {require('iframe URL')})) + # Can't use parse_qs here since it would decode the encrypted base64 `+` chars to spaces + content = self._search_regex(r'[?&]contenturl=(.+)', iframe_url, 'content') + encrypted_data, _, attrs = content.partition('|') + metadata = { + m.group('k'): m.group('v') + for m in re.finditer(r'(?:^|\|)(?P[a-z_]+)=(?P(?:(?!\|[a-z_]+=).)+)', attrs) + } + m3u8_url = self.decrypt(encrypted_data, metadata['key']) + + return { + 'id': display_id, # Fallback + 'display_id': display_id, + **json_ld_data, + **traverse_obj(metadata, { + 'id': ('content_id', {str}), + 'title': ('title', {str}), + 'thumbnail': ('image', {str}), + }), + **traverse_obj(hidden_inputs_data, { + 'id': ('content_id', {str}), + 'media_type': ('theme_type', {str}), + 'categories': ('genre', {str}, filter, all, filter), + }), + 'formats': self._extract_m3u8_formats(m3u8_url, display_id), + } + + +class TarangPlusEpisodesIE(TarangPlusBaseIE): + IE_NAME = 'tarangplus:episodes' + _VALID_URL = r'https?://(?:www\.)?tarangplus\.in/(?P[^#?/]+)/(?P[^#?/]+)/episodes/?(?:$|[?#])' + _TESTS = [{ + 'url': 'https://tarangplus.in/tarangaplus-originals/balijatra/episodes', + 'info_dict': { + 'id': 'balijatra', + 'title': 'Balijatra', + }, + 'playlist_mincount': 7, + }, { + 'url': 'https://tarangplus.in/tarang-serials/bada-bohu/episodes', + 'info_dict': { + 'id': 'bada-bohu', + 'title': 'Bada Bohu', + }, + 'playlist_mincount': 236, + }, { + 'url': 'https://tarangplus.in/shows/dr-nonsense/episodes', + 'info_dict': { + 'id': 'dr-nonsense', + 'title': 'Dr. Nonsense', + }, + 'playlist_mincount': 15, + }] + _PAGE_SIZE = 20 + + def _entries(self, playlist_url, playlist_id, page): + data = self._download_json( + playlist_url, playlist_id, f'Downloading playlist JSON page {page + 1}', + query={'page_no': page}) + for item in traverse_obj(data, ('items', ..., {str})): + yield self.url_result( + urljoin(self._BASE_URL, item.split('$')[3]), TarangPlusVideoIE) + + def _real_extract(self, url): + url_type, display_id = self._match_valid_url(url).group('type', 'id') + series_url = f'{self._BASE_URL}/{url_type}/{display_id}' + webpage = self._download_webpage(series_url, display_id) + + entries = OnDemandPagedList( + functools.partial(self._entries, f'{series_url}/episodes', display_id), + self._PAGE_SIZE) + return self.playlist_result( + entries, display_id, self._hidden_inputs(webpage).get('title')) + + +class TarangPlusPlaylistIE(TarangPlusBaseIE): + IE_NAME = 'tarangplus:playlist' + _VALID_URL = r'https?://(?:www\.)?tarangplus\.in/(?P[^#?/]+)/all/?(?:$|[?#])' + _TESTS = [{ + 'url': 'https://tarangplus.in/chhota-jaga/all', + 'info_dict': { + 'id': 'chhota-jaga', + 'title': 'Chhota Jaga', + }, + 'playlist_mincount': 33, + }, { + 'url': 'https://tarangplus.in/kids-yali-show/all', + 'info_dict': { + 'id': 'kids-yali-show', + 'title': 'Yali', + }, + 'playlist_mincount': 10, + }, { + 'url': 'https://tarangplus.in/trailer/all', + 'info_dict': { + 'id': 'trailer', + 'title': 'Trailer', + }, + 'playlist_mincount': 57, + }, { + 'url': 'https://tarangplus.in/latest-songs/all', + 'info_dict': { + 'id': 'latest-songs', + 'title': 'Latest Songs', + }, + 'playlist_mincount': 46, + }, { + 'url': 'https://tarangplus.in/premium-serials-episodes/all', + 'info_dict': { + 'id': 'premium-serials-episodes', + 'title': 'Primetime Latest Episodes', + }, + 'playlist_mincount': 100, + }] + + def _entries(self, webpage): + for url_path in traverse_obj(webpage, ( + {find_elements(cls='item')}, ..., + {find_elements(tag='a', attr='href', value='/.+', html=True, regex=True)}, + ..., {extract_attributes}, 'href', + )): + yield self.url_result(urljoin(self._BASE_URL, url_path), TarangPlusVideoIE) + + def _real_extract(self, url): + display_id = self._match_id(url) + webpage = self._download_webpage(url, display_id) + + return self.playlist_result( + self._entries(webpage), display_id, + traverse_obj(webpage, ({find_element(id='al_title')}, {clean_html})))