mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-08-06 22:05:20 +03:00
Remove client metadata from AnyTLS requests by default
This commit is contained in:
@@ -17,6 +17,7 @@ icon: material/new-box
|
|||||||
"idle_session_check_interval": "30s",
|
"idle_session_check_interval": "30s",
|
||||||
"idle_session_timeout": "30s",
|
"idle_session_timeout": "30s",
|
||||||
"min_idle_session": 5,
|
"min_idle_session": 5,
|
||||||
|
"client_metadata": "",
|
||||||
"tls": {},
|
"tls": {},
|
||||||
|
|
||||||
... // Dial Fields
|
... // Dial Fields
|
||||||
@@ -55,6 +56,12 @@ In the check, close sessions that have been idle for longer than this. Default:
|
|||||||
|
|
||||||
In the check, at least the first `n` idle sessions are kept open. Default value: `n`=0
|
In the check, at least the first `n` idle sessions are kept open. Default value: `n`=0
|
||||||
|
|
||||||
|
#### client_metadata
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.13.16"
|
||||||
|
|
||||||
|
Check [AnyTLS client metadata](/manual/misc/anytls-client-metadata/).
|
||||||
|
|
||||||
#### tls
|
#### tls
|
||||||
|
|
||||||
==Required==
|
==Required==
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ icon: material/new-box
|
|||||||
"idle_session_check_interval": "30s",
|
"idle_session_check_interval": "30s",
|
||||||
"idle_session_timeout": "30s",
|
"idle_session_timeout": "30s",
|
||||||
"min_idle_session": 5,
|
"min_idle_session": 5,
|
||||||
|
"client_metadata": "",
|
||||||
"tls": {},
|
"tls": {},
|
||||||
|
|
||||||
... // 拨号字段
|
... // 拨号字段
|
||||||
@@ -55,6 +56,12 @@ AnyTLS 密码。
|
|||||||
|
|
||||||
在检查中,至少前 `n` 个空闲会话保持打开状态。默认值:`n`=0
|
在检查中,至少前 `n` 个空闲会话保持打开状态。默认值:`n`=0
|
||||||
|
|
||||||
|
#### client_metadata
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.13.16 起"
|
||||||
|
|
||||||
|
参阅 [AnyTLS 客户端元数据](/zh/manual/misc/anytls-client-metadata/)。
|
||||||
|
|
||||||
#### tls
|
#### tls
|
||||||
|
|
||||||
==必填==
|
==必填==
|
||||||
|
|||||||
67
docs/manual/misc/anytls-client-metadata.md
Normal file
67
docs/manual/misc/anytls-client-metadata.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
icon: material/incognito
|
||||||
|
---
|
||||||
|
|
||||||
|
# AnyTLS client metadata
|
||||||
|
|
||||||
|
The AnyTLS protocol has a design flaw: its settings frame requires the client
|
||||||
|
to send its software name and version to the server, and the protocol
|
||||||
|
specification requires that clients not disguise this information.
|
||||||
|
|
||||||
|
This field serves no protocol purpose — AnyTLS already has a separate version
|
||||||
|
field for compatibility negotiation, and the open-source server implementation
|
||||||
|
does not use client metadata. However, the field allows vendors to collect and track client types, and for
|
||||||
|
platform-specific clients, potentially infer private information such as the operating system type and version range —
|
||||||
|
something that should not, and is not expected by users to, appear in an
|
||||||
|
anti-censorship protocol. We have received reports that
|
||||||
|
commercial proxy providers use this information to identify and block
|
||||||
|
connections from the official library provided by AnyTLS for sing-box
|
||||||
|
integration, reportedly because abusive users connect to their servers with
|
||||||
|
sing-box or with clients using the same official library. This indicates that
|
||||||
|
client metadata is being collected and used for discrimination in practice.
|
||||||
|
|
||||||
|
The protocol specification states that "disguising it has no value." We
|
||||||
|
disagree: the situation is analogous to browsers implementing TLS ECH GREASE —
|
||||||
|
without it, privacy-protecting clients can be fingerprinted and treated
|
||||||
|
differently.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
### 2025-02-20
|
||||||
|
|
||||||
|
We merged the
|
||||||
|
[pull request adding this protocol](https://github.com/SagerNet/sing-box/pull/2615).
|
||||||
|
Since the metadata was fixed at `sing-anytls/<library version>` in the
|
||||||
|
implementation provided for our use, and we did not carefully review the
|
||||||
|
protocol specification and other implementations, we wrongly believed that it
|
||||||
|
was not private information.
|
||||||
|
|
||||||
|
### 2025-04-05
|
||||||
|
|
||||||
|
The protocol document
|
||||||
|
[added](https://github.com/anytls/anytls-go/commit/8812aae7ab29dd88bb89067b9ca676e2e7e29171)
|
||||||
|
the requirement that third-party implementations fill in the real software
|
||||||
|
name and version, claiming that "disguising it has no value".
|
||||||
|
|
||||||
|
### 2026-07-18
|
||||||
|
|
||||||
|
A [pull request submitted to sing-box](https://github.com/SagerNet/sing-box/pull/4311)
|
||||||
|
was found to additionally upload the `sing-box` name and the actual version;
|
||||||
|
the change was subsequently reverted and was never released.
|
||||||
|
|
||||||
|
### 2026-08-03
|
||||||
|
|
||||||
|
sing-box 1.13.16 and 1.14.0-beta.5 have been released; the client metadata in
|
||||||
|
AnyTLS requests is now empty by default. For compatibility, the
|
||||||
|
[client_metadata](/configuration/outbound/anytls/#client_metadata) outbound
|
||||||
|
option allows users to set a custom value.
|
||||||
|
|
||||||
|
Since the open-source server implementation does not use this information and
|
||||||
|
it has no legitimate use, this is not considered a breaking change.
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
|
||||||
|
We recommend that the AnyTLS protocol remove the client metadata, or replace
|
||||||
|
it with an option that is not sent by default and can be customized by the
|
||||||
|
user; and that other client implementations also take action, to jointly stop
|
||||||
|
statistics collection and discrimination based on client metadata.
|
||||||
35
docs/manual/misc/anytls-client-metadata.zh.md
Normal file
35
docs/manual/misc/anytls-client-metadata.zh.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
icon: material/incognito
|
||||||
|
---
|
||||||
|
|
||||||
|
# AnyTLS 客户端元数据
|
||||||
|
|
||||||
|
AnyTLS 协议具有设计缺陷:其 settings 帧要求客户端向服务器发送软件名称和版本,且协议规范要求客户端不得伪装此信息。
|
||||||
|
|
||||||
|
此字段不承担协议功能——AnyTLS 已有独立的版本字段用于兼容性协商,且开源服务端实现不使用客户端元数据。然而,此字段使得供应商可以收集并统计客户端类型,对于某些平台特定的客户端,还可能推断出操作系统类型与版本范围等隐私信息,而这不应该,也不是被用户预期的,在一个反审查协议中出现。我们收到报告,有商业代理提供商利用此信息识别和阻止来自 sing-box 使用的、由 AnyTLS 提供的用于 sing-box 集成的官方代码库的连接,据传原因是恶意用户使用 sing-box 或使用相同官方代码库的客户端连接到服务器,这表明客户端元数据在实践中已被用于收集和区别对待。
|
||||||
|
|
||||||
|
协议规范称「伪装它没有任何意义」。我们不同意:这类似于浏览器实现 TLS ECH GREASE——如果没有这一机制,保护隐私的客户端会被识别并受到差别对待。
|
||||||
|
|
||||||
|
## 状态
|
||||||
|
|
||||||
|
### 2025-02-20
|
||||||
|
|
||||||
|
我们合并了[添加此协议的 PR](https://github.com/SagerNet/sing-box/pull/2615)。由于在供我们使用的实现中,metadata 被固定在 `sing-anytls/<library version>`,且我们没有仔细审查协议规范和其他实现,我们错误地认为这不是隐私信息。
|
||||||
|
|
||||||
|
### 2025-04-05
|
||||||
|
|
||||||
|
协议文档[加入](https://github.com/anytls/anytls-go/commit/8812aae7ab29dd88bb89067b9ca676e2e7e29171)了要求第三方实现填写真实软件名称与版本号的条款,并声称「伪装它没有任何意义」。
|
||||||
|
|
||||||
|
### 2026-07-18
|
||||||
|
|
||||||
|
[向 sing-box 提出的 PR](https://github.com/SagerNet/sing-box/pull/4311) 被发现额外上传了 `sing-box` 和实际版本的字符串,随后此更改被回退,没有发布。
|
||||||
|
|
||||||
|
### 2026-08-03
|
||||||
|
|
||||||
|
发布了 sing-box 1.13.16 和 1.14.0-beta.5,现在 AnyTLS 请求中的客户端元数据默认为空。出于兼容性考虑,[client_metadata](/zh/configuration/outbound/anytls/#client_metadata) 出站选项允许用户自定义此值。
|
||||||
|
|
||||||
|
由于开源服务端实现不使用此信息,且它没有合理用途,这不被视为破坏性更改。
|
||||||
|
|
||||||
|
## 建议
|
||||||
|
|
||||||
|
我们建议 AnyTLS 协议移除客户端元数据,或将其替换为非默认提供、且用户可以自定义的选项;并建议其他客户端实现也采取行动,共同阻止基于客户端元数据的统计和区别对待。
|
||||||
@@ -75,6 +75,7 @@ nav:
|
|||||||
- Hysteria 2: manual/proxy-protocol/hysteria2.md
|
- Hysteria 2: manual/proxy-protocol/hysteria2.md
|
||||||
- Misc:
|
- Misc:
|
||||||
- TunnelVision: manual/misc/tunnelvision.md
|
- TunnelVision: manual/misc/tunnelvision.md
|
||||||
|
- AnyTLS client metadata: manual/misc/anytls-client-metadata.md
|
||||||
- Configuration:
|
- Configuration:
|
||||||
- configuration/index.md
|
- configuration/index.md
|
||||||
- Log:
|
- Log:
|
||||||
|
|||||||
@@ -22,4 +22,5 @@ type AnyTLSOutboundOptions struct {
|
|||||||
IdleSessionCheckInterval badoption.Duration `json:"idle_session_check_interval,omitempty"`
|
IdleSessionCheckInterval badoption.Duration `json:"idle_session_check_interval,omitempty"`
|
||||||
IdleSessionTimeout badoption.Duration `json:"idle_session_timeout,omitempty"`
|
IdleSessionTimeout badoption.Duration `json:"idle_session_timeout,omitempty"`
|
||||||
MinIdleSession int `json:"min_idle_session,omitempty"`
|
MinIdleSession int `json:"min_idle_session,omitempty"`
|
||||||
|
ClientMetadata string `json:"client_metadata,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
64
protocol/anytls/client_metadata.go
Normal file
64
protocol/anytls/client_metadata.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package anytls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"net"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
|
||||||
|
anytls "github.com/anytls/sing-anytls"
|
||||||
|
"github.com/anytls/sing-anytls/session"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
commandSettings = 4
|
||||||
|
frameHeaderSize = 7
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
clientSessionField, _ = reflect.TypeFor[anytls.Client]().FieldByName("sessionClient")
|
||||||
|
streamSessionField, _ = reflect.TypeFor[session.Stream]().FieldByName("sess")
|
||||||
|
sessionConnLockField, _ = reflect.TypeFor[session.Session]().FieldByName("connLock")
|
||||||
|
sessionBufferField, _ = reflect.TypeFor[session.Session]().FieldByName("buffer")
|
||||||
|
)
|
||||||
|
|
||||||
|
func sessionClientOf(client *anytls.Client) *session.Client {
|
||||||
|
return *(**session.Client)(unsafe.Add(unsafe.Pointer(client), clientSessionField.Offset))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Outbound) rewriteClientMetadata(conn net.Conn) {
|
||||||
|
sess := *(**session.Session)(unsafe.Add(unsafe.Pointer(conn.(*session.Stream)), streamSessionField.Offset))
|
||||||
|
connLock := (*sync.Mutex)(unsafe.Add(unsafe.Pointer(sess), sessionConnLockField.Offset))
|
||||||
|
bufferPointer := (*[]byte)(unsafe.Add(unsafe.Pointer(sess), sessionBufferField.Offset))
|
||||||
|
connLock.Lock()
|
||||||
|
defer connLock.Unlock()
|
||||||
|
buffer := *bufferPointer
|
||||||
|
offset := 0
|
||||||
|
for offset+frameHeaderSize <= len(buffer) {
|
||||||
|
dataLength := int(binary.BigEndian.Uint16(buffer[offset+5 : offset+7]))
|
||||||
|
frameEnd := offset + frameHeaderSize + dataLength
|
||||||
|
if frameEnd > len(buffer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if buffer[offset] == commandSettings {
|
||||||
|
data := []byte(strings.Join(common.Map(strings.Split(string(buffer[offset+frameHeaderSize:frameEnd]), "\n"), func(line string) string {
|
||||||
|
if strings.HasPrefix(line, "client=") {
|
||||||
|
return "client=" + h.clientMetadata
|
||||||
|
}
|
||||||
|
return line
|
||||||
|
}), "\n"))
|
||||||
|
newBuffer := make([]byte, 0, offset+frameHeaderSize+len(data)+len(buffer)-frameEnd)
|
||||||
|
newBuffer = append(newBuffer, buffer[:offset+5]...)
|
||||||
|
newBuffer = binary.BigEndian.AppendUint16(newBuffer, uint16(len(data)))
|
||||||
|
newBuffer = append(newBuffer, data...)
|
||||||
|
newBuffer = append(newBuffer, buffer[frameEnd:]...)
|
||||||
|
*bufferPointer = newBuffer
|
||||||
|
return
|
||||||
|
}
|
||||||
|
offset = frameEnd
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"github.com/sagernet/sing/common/uot"
|
"github.com/sagernet/sing/common/uot"
|
||||||
|
|
||||||
anytls "github.com/anytls/sing-anytls"
|
anytls "github.com/anytls/sing-anytls"
|
||||||
|
"github.com/anytls/sing-anytls/session"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RegisterOutbound(registry *outbound.Registry) {
|
func RegisterOutbound(registry *outbound.Registry) {
|
||||||
@@ -27,12 +28,14 @@ func RegisterOutbound(registry *outbound.Registry) {
|
|||||||
|
|
||||||
type Outbound struct {
|
type Outbound struct {
|
||||||
outbound.Adapter
|
outbound.Adapter
|
||||||
dialer tls.Dialer
|
dialer tls.Dialer
|
||||||
server M.Socksaddr
|
server M.Socksaddr
|
||||||
tlsConfig tls.Config
|
tlsConfig tls.Config
|
||||||
client *anytls.Client
|
clientMetadata string
|
||||||
uotClient *uot.Client
|
client *anytls.Client
|
||||||
logger log.ContextLogger
|
sessionClient *session.Client
|
||||||
|
uotClient *uot.Client
|
||||||
|
logger log.ContextLogger
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.AnyTLSOutboundOptions) (adapter.Outbound, error) {
|
func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.AnyTLSOutboundOptions) (adapter.Outbound, error) {
|
||||||
@@ -81,14 +84,30 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
outbound.client = client
|
outbound.client = client
|
||||||
|
outbound.clientMetadata = options.ClientMetadata
|
||||||
|
outbound.sessionClient = sessionClientOf(client)
|
||||||
|
|
||||||
outbound.uotClient = &uot.Client{
|
outbound.uotClient = &uot.Client{
|
||||||
Dialer: (anytlsDialer)(client.CreateProxy),
|
Dialer: (anytlsDialer)(outbound.createProxy),
|
||||||
Version: uot.Version,
|
Version: uot.Version,
|
||||||
}
|
}
|
||||||
return outbound, nil
|
return outbound, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Outbound) createProxy(ctx context.Context, destination M.Socksaddr) (net.Conn, error) {
|
||||||
|
conn, err := h.sessionClient.CreateStream(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
h.rewriteClientMetadata(conn)
|
||||||
|
err = M.SocksaddrSerializer.WriteAddrPort(conn, destination)
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
type anytlsDialer func(ctx context.Context, destination M.Socksaddr) (net.Conn, error)
|
type anytlsDialer func(ctx context.Context, destination M.Socksaddr) (net.Conn, error)
|
||||||
|
|
||||||
func (d anytlsDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
func (d anytlsDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||||
@@ -110,7 +129,7 @@ func (h *Outbound) DialContext(ctx context.Context, network string, destination
|
|||||||
switch N.NetworkName(network) {
|
switch N.NetworkName(network) {
|
||||||
case N.NetworkTCP:
|
case N.NetworkTCP:
|
||||||
h.logger.InfoContext(ctx, "outbound connection to ", destination)
|
h.logger.InfoContext(ctx, "outbound connection to ", destination)
|
||||||
return h.client.CreateProxy(ctx, destination)
|
return h.createProxy(ctx, destination)
|
||||||
case N.NetworkUDP:
|
case N.NetworkUDP:
|
||||||
h.logger.InfoContext(ctx, "outbound UoT packet connection to ", destination)
|
h.logger.InfoContext(ctx, "outbound UoT packet connection to ", destination)
|
||||||
return h.uotClient.DialContext(ctx, network, destination)
|
return h.uotClient.DialContext(ctx, network, destination)
|
||||||
|
|||||||
Reference in New Issue
Block a user