Update On Fri Jan 2 19:40:16 CET 2026

This commit is contained in:
github-action[bot]
2026-01-02 19:40:17 +01:00
parent f775862833
commit 7b95cb7ecd
36 changed files with 743 additions and 289 deletions
+1
View File
@@ -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
+60 -20
View File
@@ -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 {
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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=
+2 -2
View File
@@ -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, \
+25 -15
View File
@@ -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
}
+60 -20
View File
@@ -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 {
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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=
+8 -2
View File
@@ -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
+8 -2
View File
@@ -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 收藏。
## 🤝 赞助商
+31 -16
View File
@@ -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 `//<connection_id>#1` signal
- For UDP: Generates a `//<connection_id>#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: `#<tls>`
- 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: `//<connection_id>#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: `//<connection_id>#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
+31 -16
View File
@@ -73,8 +73,8 @@ NodePass 通过其隧道架构建立双向数据流,支持 TCP 和 UDP 协议
```
[服务端] → [生成唯一连接 ID] → [通过未加密的 TCP 隧道向客户端发送信号]
```
- 对于 TCP:生成 `//<connection_id>#1` 信号
- 对于 UDP:生成 `//<connection_id>#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>`
- 目的:通知客户端 TLS 代号
- 时机:在隧道握手时发送
1. **刷新信号**
- 格式:`{"action":"flush"}`
- 目的:刷新连接池并重置错误计数
- 时机:当连接池健康检查失败时发送
2. **TCP 启动信号**
- 格式:`//<connection_id>#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 启动信号**
- 格式:`//<connection_id>#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` 字段分派到相应的处理逻辑
- 连接启动信号触发相应的处理方法建立连接
### 信号弹性
- 具有可配置容量的缓冲通道防止在高负载下信号丢失
+2 -2
View File
@@ -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
+4 -6
View File
@@ -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=
+5 -4
View File
@@ -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)
+160 -133
View File
@@ -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会话
+6 -4
View File
@@ -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{})
@@ -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
@@ -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})
+5
View File
@@ -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**
@@ -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 <Key Length>` |
| 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.
@@ -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)。
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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=
+22 -18
View File
@@ -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
}
+2 -2
View File
@@ -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
+4 -4
View File
@@ -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
)
+8 -8
View File
@@ -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=
@@ -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 {
@@ -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)
}
@@ -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)
}
+34 -1
View File
@@ -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:
+2 -2
View File
@@ -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
+5
View File
@@ -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 (
+224
View File
@@ -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<id>[^#?/]+)'
_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<k>[a-z_]+)=(?P<v>(?:(?!\|[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<type>[^#?/]+)/(?P<id>[^#?/]+)/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<id>[^#?/]+)/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})))