Remove client metadata from AnyTLS requests by default

This commit is contained in:
世界
2026-08-03 04:35:19 +08:00
parent 7da573f187
commit cb88d94d6d
8 changed files with 209 additions and 8 deletions

View File

@@ -17,6 +17,7 @@ icon: material/new-box
"idle_session_check_interval": "30s",
"idle_session_timeout": "30s",
"min_idle_session": 5,
"client_metadata": "",
"tls": {},
... // 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
#### client_metadata
!!! question "Since sing-box 1.13.16"
Check [AnyTLS client metadata](/manual/misc/anytls-client-metadata/).
#### tls
==Required==

View File

@@ -17,6 +17,7 @@ icon: material/new-box
"idle_session_check_interval": "30s",
"idle_session_timeout": "30s",
"min_idle_session": 5,
"client_metadata": "",
"tls": {},
... // 拨号字段
@@ -55,6 +56,12 @@ AnyTLS 密码。
在检查中,至少前 `n` 个空闲会话保持打开状态。默认值:`n`=0
#### client_metadata
!!! question "自 sing-box 1.13.16 起"
参阅 [AnyTLS 客户端元数据](/zh/manual/misc/anytls-client-metadata/)。
#### tls
==必填==

View 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.

View 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 协议移除客户端元数据,或将其替换为非默认提供、且用户可以自定义的选项;并建议其他客户端实现也采取行动,共同阻止基于客户端元数据的统计和区别对待。

View File

@@ -75,6 +75,7 @@ nav:
- Hysteria 2: manual/proxy-protocol/hysteria2.md
- Misc:
- TunnelVision: manual/misc/tunnelvision.md
- AnyTLS client metadata: manual/misc/anytls-client-metadata.md
- Configuration:
- configuration/index.md
- Log:

View File

@@ -22,4 +22,5 @@ type AnyTLSOutboundOptions struct {
IdleSessionCheckInterval badoption.Duration `json:"idle_session_check_interval,omitempty"`
IdleSessionTimeout badoption.Duration `json:"idle_session_timeout,omitempty"`
MinIdleSession int `json:"min_idle_session,omitempty"`
ClientMetadata string `json:"client_metadata,omitempty"`
}

View 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
}
}

View File

@@ -19,6 +19,7 @@ import (
"github.com/sagernet/sing/common/uot"
anytls "github.com/anytls/sing-anytls"
"github.com/anytls/sing-anytls/session"
)
func RegisterOutbound(registry *outbound.Registry) {
@@ -27,12 +28,14 @@ func RegisterOutbound(registry *outbound.Registry) {
type Outbound struct {
outbound.Adapter
dialer tls.Dialer
server M.Socksaddr
tlsConfig tls.Config
client *anytls.Client
uotClient *uot.Client
logger log.ContextLogger
dialer tls.Dialer
server M.Socksaddr
tlsConfig tls.Config
clientMetadata string
client *anytls.Client
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) {
@@ -81,14 +84,30 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
return nil, err
}
outbound.client = client
outbound.clientMetadata = options.ClientMetadata
outbound.sessionClient = sessionClientOf(client)
outbound.uotClient = &uot.Client{
Dialer: (anytlsDialer)(client.CreateProxy),
Dialer: (anytlsDialer)(outbound.createProxy),
Version: uot.Version,
}
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)
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) {
case N.NetworkTCP:
h.logger.InfoContext(ctx, "outbound connection to ", destination)
return h.client.CreateProxy(ctx, destination)
return h.createProxy(ctx, destination)
case N.NetworkUDP:
h.logger.InfoContext(ctx, "outbound UoT packet connection to ", destination)
return h.uotClient.DialContext(ctx, network, destination)